use std::any::Any;
use super::{CallableFixedRateBond, CallableZeroCouponBond};
use crate::errors::{QlError, QlResult};
use crate::handle::Handle;
use crate::instrument::Instrument;
use crate::instruments::{BondPrice, BondResults};
use crate::math::solver1d::Solver1D;
use crate::math::solvers1d::brent::Brent;
use crate::pricingengine::PricingEngine;
use crate::pricingengines::bond::BlackCallableFixedRateBondEngine;
use crate::quotes::{Quote, SimpleQuote};
use crate::settings::Settings;
use crate::shared::{Shared, shared};
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::time::date::Date;
use crate::types::{Real, Size, Volatility};
use crate::{fail, require};
impl CallableFixedRateBond {
#[allow(clippy::too_many_arguments)]
pub fn implied_volatility(
&self,
target_price: BondPrice,
discount_curve: Handle<dyn YieldTermStructure>,
accuracy: Real,
max_evaluations: Size,
min_vol: Volatility,
max_vol: Volatility,
) -> QlResult<Volatility> {
let settlement = self.checked_settlement_date()?;
let accrued = match target_price {
BondPrice::Clean(_) => self.bond().accrued_amount(Some(settlement))?,
BondPrice::Dirty(_) => 0.0,
};
implied_volatility(
self,
target_price.amount() + accrued,
self.face_amount,
discount_curve,
Shared::clone(&self.settings),
accuracy,
max_evaluations,
min_vol,
max_vol,
)
}
}
impl CallableZeroCouponBond {
#[allow(clippy::too_many_arguments)]
pub fn implied_volatility(
&self,
target_price: BondPrice,
discount_curve: Handle<dyn YieldTermStructure>,
accuracy: Real,
max_evaluations: Size,
min_vol: Volatility,
max_vol: Volatility,
) -> QlResult<Volatility> {
implied_volatility(
self,
target_price.amount(),
self.face_amount,
discount_curve,
Shared::clone(&self.settings),
accuracy,
max_evaluations,
min_vol,
max_vol,
)
}
}
#[allow(clippy::too_many_arguments)]
fn implied_volatility(
instrument: &dyn Instrument,
dirty_target: Real,
face_amount: Real,
discount_curve: Handle<dyn YieldTermStructure>,
settings: Shared<Settings<Date>>,
accuracy: Real,
max_evaluations: Size,
min_vol: Volatility,
max_vol: Volatility,
) -> QlResult<Volatility> {
require!(!instrument.is_expired()?, "instrument expired");
require!(
dirty_target.is_finite() && dirty_target >= 0.0,
"invalid dirty target price"
);
require!(
accuracy.is_finite() && accuracy > 0.0,
"accuracy must be finite and positive"
);
require!(max_evaluations > 0, "maximum evaluations must be positive");
require!(
min_vol.is_finite() && min_vol >= 0.0,
"invalid minimum volatility"
);
require!(
max_vol.is_finite() && max_vol > min_vol,
"invalid maximum volatility"
);
let target = dirty_target / 100.0 * face_amount;
require!(target.is_finite(), "target settlement value overflow");
let quote = shared(SimpleQuote::new(min_vol));
let mut engine = BlackCallableFixedRateBondEngine::new(
Handle::new(Shared::clone("e) as Shared<dyn Quote>),
discount_curve,
settings,
);
instrument.setup_arguments(engine.arguments_mut())?;
engine.arguments_mut().validate()?;
let mut failure = None;
let mut evaluations = 0;
let mut objective = |vol| -> Real {
if failure.is_some() {
return Real::NAN;
}
let value = (|| -> QlResult<Real> {
require!(
evaluations < max_evaluations,
"maximum number of function evaluations ({max_evaluations}) exceeded"
);
evaluations += 1;
quote.set_value(vol);
engine.calculate()?;
let Some(results) = (engine.results() as &dyn Any).downcast_ref::<BondResults>() else {
fail!("wrong callable bond result type");
};
let value = results
.settlement_value
.ok_or_else(|| QlError::new("missing settlement value", file!(), line!()))?;
let residual = value / face_amount * 100.0 - dirty_target;
require!(residual.is_finite(), "nonfinite settlement price residual");
Ok(residual)
})();
match value {
Ok(residual) => residual,
Err(error) => {
failure = Some(error);
Real::NAN
}
}
};
let guess = min_vol + (max_vol - min_vol) / 2.0;
let root = Brent::new()
.with_max_evaluations(max_evaluations)
.solve_bracketed(&mut objective, accuracy, guess, min_vol, max_vol);
match failure {
Some(error) => Err(error),
None => root,
}
}