use crate::errors::QlResult;
use crate::handle::Handle;
use crate::math::solver1d::Solver1D;
use crate::math::solvers1d::brent::Brent;
use crate::patterns::lazyobject::LazyObject;
use crate::patterns::observable::Observer;
use crate::pricingengine::PricingEngine;
use crate::quotes::Quote;
use crate::shared::{SharedMut, shared_mut};
use crate::termstructures::volatility::VolatilityType;
use crate::types::Real;
pub trait CalibrationHelper {
fn calibration_error(&mut self) -> QlResult<Real>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CalibrationErrorType {
RelativePriceError,
PriceError,
ImpliedVolError,
}
struct Updater {
lazy: SharedMut<LazyObject>,
}
impl Observer for Updater {
fn update(&mut self) {
if let Some(update) = LazyObject::deferred_update(&self.lazy) {
update.notify_observers();
}
}
fn defer_reentrant_update(&self) -> bool {
false
}
}
pub struct BlackCalibrationHelperBase {
lazy: SharedMut<LazyObject>,
updater: SharedMut<Updater>,
volatility: Handle<dyn Quote>,
volatility_type: VolatilityType,
shift: Real,
calibration_error_type: CalibrationErrorType,
market_value: Real,
engine: Option<SharedMut<dyn PricingEngine>>,
}
impl BlackCalibrationHelperBase {
pub fn new(
volatility: Handle<dyn Quote>,
calibration_error_type: CalibrationErrorType,
volatility_type: VolatilityType,
shift: Real,
) -> Self {
let lazy = shared_mut(LazyObject::new(true));
let updater = shared_mut(Updater {
lazy: SharedMut::clone(&lazy),
});
volatility.register_observer(&(SharedMut::clone(&updater) as SharedMut<dyn Observer>));
BlackCalibrationHelperBase {
lazy,
updater,
volatility,
volatility_type,
shift,
calibration_error_type,
market_value: 0.0,
engine: None,
}
}
pub fn volatility(&self) -> &Handle<dyn Quote> {
&self.volatility
}
pub fn volatility_type(&self) -> VolatilityType {
self.volatility_type
}
pub fn shift(&self) -> Real {
self.shift
}
pub fn set_pricing_engine(&mut self, engine: SharedMut<dyn PricingEngine>) {
self.engine = Some(engine);
}
pub fn pricing_engine(&self) -> Option<&SharedMut<dyn PricingEngine>> {
self.engine.as_ref()
}
pub fn is_calculated(&self) -> bool {
self.lazy.borrow().is_calculated()
}
pub fn observer(&self) -> SharedMut<dyn Observer> {
SharedMut::clone(&self.updater) as SharedMut<dyn Observer>
}
pub fn register_observer(&self, observer: &SharedMut<dyn Observer>) -> bool {
self.lazy.borrow().register_observer(observer)
}
}
pub trait BlackCalibrationHelper {
fn base(&self) -> &BlackCalibrationHelperBase;
fn base_mut(&mut self) -> &mut BlackCalibrationHelperBase;
fn model_value(&self) -> QlResult<Real>;
fn black_price(&self, volatility: Real) -> QlResult<Real>;
fn perform_calculations(&mut self) -> QlResult<()> {
let volatility = self.base().volatility.current_link()?.value()?;
let market_value = self.black_price(volatility)?;
self.base_mut().market_value = market_value;
Ok(())
}
fn calculate(&mut self) -> QlResult<()> {
let lazy = SharedMut::clone(&self.base().lazy);
if !lazy.borrow_mut().start_calculation() {
return Ok(());
}
let result = self.perform_calculations();
lazy.borrow_mut().finish_calculation(&result);
result
}
fn market_value(&mut self) -> QlResult<Real> {
self.calculate()?;
Ok(self.base().market_value)
}
fn implied_volatility(
&self,
target_value: Real,
accuracy: Real,
max_evaluations: usize,
min_vol: Real,
max_vol: Real,
) -> QlResult<Real> {
let guess = self.base().volatility.current_link()?.value()?;
let mut solver = Brent::new().with_max_evaluations(max_evaluations);
solver.solve_bracketed(
|x| target_value - self.black_price(x).unwrap_or(Real::NAN),
accuracy,
guess,
min_vol,
max_vol,
)
}
}
impl<T: BlackCalibrationHelper> CalibrationHelper for T {
fn calibration_error(&mut self) -> QlResult<Real> {
match self.base().calibration_error_type {
CalibrationErrorType::RelativePriceError => {
let market = self.market_value()?;
let model = self.model_value()?;
Ok((market - model).abs() / market)
}
CalibrationErrorType::PriceError => {
let market = self.market_value()?;
let model = self.model_value()?;
Ok(market - model)
}
CalibrationErrorType::ImpliedVolError => {
let (min_vol, max_vol) = match self.base().volatility_type {
VolatilityType::ShiftedLognormal => (0.0010, 10.0),
VolatilityType::Normal => (0.00005, 0.50),
};
let lower_price = self.black_price(min_vol)?;
let upper_price = self.black_price(max_vol)?;
let model_price = self.model_value()?;
let implied = if model_price <= lower_price {
min_vol
} else if model_price >= upper_price {
max_vol
} else {
self.implied_volatility(model_price, 1e-12, 5000, min_vol, max_vol)?
};
let market_vol = self.base().volatility.current_link()?.value()?;
Ok(implied - market_vol)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::quotes::SimpleQuote;
use crate::shared::{Shared, shared};
use std::cell::Cell;
struct StubHelper {
base: BlackCalibrationHelperBase,
model: Cell<Real>,
}
impl StubHelper {
fn new(
vol: Shared<SimpleQuote>,
error_type: CalibrationErrorType,
model: Real,
) -> StubHelper {
let handle: Handle<dyn Quote> = Handle::new(vol as Shared<dyn Quote>);
StubHelper {
base: BlackCalibrationHelperBase::new(
handle,
error_type,
VolatilityType::ShiftedLognormal,
0.0,
),
model: Cell::new(model),
}
}
}
impl BlackCalibrationHelper for StubHelper {
fn base(&self) -> &BlackCalibrationHelperBase {
&self.base
}
fn base_mut(&mut self) -> &mut BlackCalibrationHelperBase {
&mut self.base
}
fn model_value(&self) -> QlResult<Real> {
Ok(self.model.get())
}
fn black_price(&self, volatility: Real) -> QlResult<Real> {
Ok(volatility)
}
}
#[test]
fn relative_price_error_is_the_relative_gap() {
let vol = shared(SimpleQuote::new(0.20));
let mut helper = StubHelper::new(vol, CalibrationErrorType::RelativePriceError, 0.18);
let error = helper.calibration_error().unwrap();
assert!((error - (0.20 - 0.18) / 0.20).abs() < 1e-12);
}
#[test]
fn price_error_is_the_signed_gap() {
let vol = shared(SimpleQuote::new(0.20));
let mut helper = StubHelper::new(vol, CalibrationErrorType::PriceError, 0.18);
let error = helper.calibration_error().unwrap();
assert!((error - (0.20 - 0.18)).abs() < 1e-12);
}
#[test]
fn implied_vol_error_brent_recovers_the_interior_vol() {
let vol = shared(SimpleQuote::new(0.20));
let mut helper = StubHelper::new(vol, CalibrationErrorType::ImpliedVolError, 0.18);
let error = helper.calibration_error().unwrap();
assert!((error - (0.18 - 0.20)).abs() < 1e-9);
}
#[test]
fn implied_vol_error_clamps_below_the_lower_price() {
let vol = shared(SimpleQuote::new(0.20));
let mut helper = StubHelper::new(vol, CalibrationErrorType::ImpliedVolError, 0.0005);
let error = helper.calibration_error().unwrap();
assert!((error - (0.0010 - 0.20)).abs() < 1e-12);
}
#[test]
fn implied_vol_error_clamps_above_the_upper_price() {
let vol = shared(SimpleQuote::new(0.20));
let mut helper = StubHelper::new(vol, CalibrationErrorType::ImpliedVolError, 20.0);
let error = helper.calibration_error().unwrap();
assert!((error - (10.0 - 0.20)).abs() < 1e-12);
}
#[test]
fn market_value_recomputes_when_the_vol_quote_changes() {
let vol = shared(SimpleQuote::new(0.20));
let mut helper =
StubHelper::new(Shared::clone(&vol), CalibrationErrorType::PriceError, 0.0);
assert_eq!(helper.market_value().unwrap(), 0.20);
assert!(helper.base().is_calculated());
vol.set_value(0.30);
assert!(
!helper.base().is_calculated(),
"a quote change must invalidate the cached market value"
);
assert_eq!(helper.market_value().unwrap(), 0.30);
}
#[test]
fn errors_vanish_when_model_matches_market_and_move_when_perturbed() {
for error_type in [
CalibrationErrorType::RelativePriceError,
CalibrationErrorType::PriceError,
CalibrationErrorType::ImpliedVolError,
] {
let vol = shared(SimpleQuote::new(0.20));
let mut helper = StubHelper::new(Shared::clone(&vol), error_type, 0.20);
assert!(
helper.calibration_error().unwrap().abs() < 1e-9,
"{error_type:?} must vanish when the model matches the market"
);
helper.model.set(0.25);
assert!(
helper.calibration_error().unwrap().abs() > 1e-6,
"{error_type:?} must move once the model is perturbed"
);
}
}
}