use std::cell::RefCell;
use std::rc::Weak;
use crate::errors::QlResult;
use crate::math::interpolations::linear::Linear;
use crate::math::interpolations::{Interpolation, Interpolator};
use crate::patterns::lazyobject::LazyObject;
use crate::patterns::observable::{AsObservable, Observable, Observer};
use crate::require;
use crate::settings::Settings;
use crate::shared::{Shared, SharedMut, shared_mut};
use crate::termstructures::bootstraptraits::{BootstrapTraits, CurveData};
use crate::termstructures::iterativebootstrap::{IterativeBootstrap, PiecewiseCurve};
use crate::termstructures::volatility::VolatilityTermStructure;
use crate::termstructures::{TermStructure, TermStructureBase};
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::calendar::Calendar;
use crate::time::date::Date;
use crate::time::daycounter::DayCounter;
use crate::time::frequency::Frequency;
use crate::time::period::Period;
use crate::time::timeunit::TimeUnit;
use crate::types::{Natural, Rate, Real, Size, Time, Volatility};
use super::yoyoptionlethelpers::YoYOptionletVolatilityHelper;
use super::{YoYOptionletVolatilitySurface, YoYOptionletVolatilitySurfaceBase};
pub struct YoYInflationVolatilityTraits;
impl BootstrapTraits for YoYInflationVolatilityTraits {
fn initial_value() -> Real {
0.0
}
fn guess(i: Size, _times: &[Time], data: &[Real], valid_data: bool) -> Real {
if valid_data {
return data[i];
}
if i == 1 {
return 0.005;
}
0.002
}
fn min_value_after(i: Size, _times: &[Time], data: &[Real], _valid_data: bool) -> Real {
(data[i - 1] - 0.02).max(0.0)
}
fn max_value_after(i: Size, _times: &[Time], data: &[Real], _valid_data: bool) -> Real {
data[i - 1] + 0.02
}
fn update_guess(data: &mut [Real], value: Real, i: Size) {
data[i] = value;
}
fn max_iterations() -> Size {
25
}
}
struct CurveUpdater {
lazy: SharedMut<LazyObject>,
}
impl Observer for CurveUpdater {
fn update(&mut self) {
if let Some(update) = LazyObject::deferred_update(&self.lazy) {
update.notify_observers();
}
}
}
pub struct PiecewiseYoYOptionletVolatilityCurve<I: Interpolator> {
vol_base: YoYOptionletVolatilitySurfaceBase,
min_strike: Rate,
max_strike: Rate,
instruments: Vec<Shared<dyn YoYOptionletVolatilityHelper>>,
interpolator: I,
data: RefCell<CurveData<I>>,
lazy: SharedMut<LazyObject>,
observable: Shared<Observable>,
updater: SharedMut<CurveUpdater>,
bootstrap: IterativeBootstrap,
accuracy: Real,
self_weak: Weak<dyn YoYOptionletVolatilitySurface>,
}
impl PiecewiseYoYOptionletVolatilityCurve<Linear> {
#[allow(clippy::too_many_arguments)]
pub fn new(
settlement_days: Natural,
calendar: Calendar,
business_day_convention: BusinessDayConvention,
day_counter: DayCounter,
observation_lag: Period,
frequency: Frequency,
index_is_interpolated: bool,
min_strike: Rate,
max_strike: Rate,
base_yoy_volatility: Volatility,
instruments: Vec<Shared<dyn YoYOptionletVolatilityHelper>>,
settings: Shared<Settings<Date>>,
) -> QlResult<Shared<PiecewiseYoYOptionletVolatilityCurve<Linear>>> {
require!(!instruments.is_empty(), "no bootstrap helpers given");
let curve = Shared::new_cyclic(
|weak: &Weak<PiecewiseYoYOptionletVolatilityCurve<Linear>>| {
let self_weak: Weak<dyn YoYOptionletVolatilitySurface> = weak.clone();
let lazy = shared_mut(LazyObject::new(true));
let observable = lazy.borrow().observable_handle();
let updater = shared_mut(CurveUpdater {
lazy: SharedMut::clone(&lazy),
});
let vol_base = YoYOptionletVolatilitySurfaceBase::new(
settlement_days,
calendar,
business_day_convention,
day_counter,
observation_lag,
frequency,
index_is_interpolated,
settings,
);
vol_base.set_base_level(base_yoy_volatility);
PiecewiseYoYOptionletVolatilityCurve {
vol_base,
min_strike,
max_strike,
instruments,
interpolator: Linear,
data: RefCell::new(CurveData::new()),
lazy,
observable,
updater,
bootstrap: IterativeBootstrap::new(),
accuracy: 1.0e-12,
self_weak,
}
},
);
let observer = SharedMut::clone(&curve.updater) as SharedMut<dyn Observer>;
for helper in &curve.instruments {
helper.observable().register_observer(&observer);
}
curve
.vol_base
.term_structure_base()
.observable()
.register_observer(&observer);
Ok(curve)
}
}
impl<I: Interpolator + 'static> PiecewiseYoYOptionletVolatilityCurve<I> {
pub fn calculate(&self) -> QlResult<()> {
if self.lazy.borrow().is_calculated() {
return Ok(());
}
if !self.lazy.borrow_mut().start_calculation() {
return Ok(());
}
let result = self.bootstrap.calculate(self);
self.lazy.borrow_mut().finish_calculation(&result);
result
}
pub fn times(&self) -> QlResult<Vec<Time>> {
self.calculate()?;
Ok(self.data.borrow().times().to_vec())
}
pub fn dates(&self) -> QlResult<Vec<Date>> {
self.calculate()?;
Ok(self.data.borrow().dates().to_vec())
}
pub fn data(&self) -> QlResult<Vec<Real>> {
self.calculate()?;
Ok(self.data.borrow().data().to_vec())
}
pub fn nodes(&self) -> QlResult<Vec<(Date, Real)>> {
self.calculate()?;
Ok(self.data.borrow().nodes())
}
pub fn register_observer(&self, observer: &SharedMut<dyn Observer>) -> bool {
self.observable.register_observer(observer)
}
fn volatility_impl(&self, t: Time) -> QlResult<Volatility> {
self.calculate()?;
let data = self.data.borrow();
data.interpolation()?.value(t)
}
}
impl<I: Interpolator> AsObservable for PiecewiseYoYOptionletVolatilityCurve<I> {
fn observable(&self) -> &Observable {
&self.observable
}
}
impl<I: Interpolator + 'static> TermStructure for PiecewiseYoYOptionletVolatilityCurve<I> {
fn base(&self) -> &TermStructureBase {
self.vol_base.term_structure_base()
}
fn max_date(&self) -> Date {
let _ = self.calculate();
let t_max = self.data.borrow().times().last().copied();
match t_max {
Some(t_max) => self
.option_date_from_tenor(Period::new(t_max.ceil() as i32, TimeUnit::Years))
.unwrap_or_else(|_| Date::null()),
None => self
.vol_base
.term_structure_base()
.reference_date()
.unwrap_or_else(|_| Date::null()),
}
}
}
impl<I: Interpolator + 'static> VolatilityTermStructure
for PiecewiseYoYOptionletVolatilityCurve<I>
{
fn business_day_convention(&self) -> BusinessDayConvention {
self.vol_base.business_day_convention()
}
fn min_strike(&self) -> Rate {
self.min_strike
}
fn max_strike(&self) -> Rate {
self.max_strike
}
}
impl<I: Interpolator + 'static> YoYOptionletVolatilitySurface
for PiecewiseYoYOptionletVolatilityCurve<I>
{
fn base_date(&self) -> QlResult<Date> {
self.calculate()?;
self.vol_base.base_date()
}
fn volatility(&self, date: Date, strike: Rate, obs_lag: Period) -> QlResult<Volatility> {
let observed = self.vol_base.observed(date - obs_lag)?;
self.vol_base.check_range(
observed,
strike,
self.min_strike,
self.max_strike,
TermStructure::max_date(self),
)?;
self.volatility_impl(TermStructure::time_from_reference(self, observed)?)
}
fn total_variance(&self, date: Date, strike: Rate, obs_lag: Period) -> QlResult<Real> {
let volatility = self.volatility(date, strike, obs_lag)?;
Ok(volatility * volatility * self.vol_base.time_from_base(date, obs_lag)?)
}
fn base_level(&self) -> QlResult<Volatility> {
self.vol_base.base_level()
}
}
impl<I: Interpolator + 'static> PiecewiseCurve for PiecewiseYoYOptionletVolatilityCurve<I> {
type Traits = YoYInflationVolatilityTraits;
type Interp = I;
type TS = dyn YoYOptionletVolatilitySurface;
type Helper = dyn YoYOptionletVolatilityHelper;
fn instruments(&self) -> &[Shared<dyn YoYOptionletVolatilityHelper>] {
&self.instruments
}
fn interpolator(&self) -> &I {
&self.interpolator
}
fn curve_data(&self) -> &RefCell<CurveData<I>> {
&self.data
}
fn accuracy(&self) -> Real {
self.accuracy
}
fn reference_date(&self) -> QlResult<Date> {
self.vol_base.term_structure_base().reference_date()
}
fn initial_date(&self) -> QlResult<Date> {
self.vol_base.base_date()
}
fn initial_value(&self) -> QlResult<Real> {
self.vol_base.base_level()
}
fn time_from_reference(&self, date: Date) -> QlResult<Time> {
TermStructure::time_from_reference(self, date)
}
fn term_structure_shared(&self) -> QlResult<Shared<dyn YoYOptionletVolatilitySurface>> {
match self.self_weak.upgrade() {
Some(curve) => Ok(curve),
None => crate::fail!("curve dropped before bootstrap"),
}
}
}
#[cfg(test)]
mod tests {
use super::super::yoyoptionlethelpers::YoYOptionletVolHelperBase;
use super::*;
use crate::handle::Handle;
use crate::quotes::{Quote, SimpleQuote};
use crate::shared::shared;
use crate::time::calendars::target::Target;
use crate::time::date::Month::{April, June};
use crate::time::daycounters::actual365fixed::Actual365Fixed;
const BASE_LEVEL: Volatility = 0.01;
const QUOTES: [Real; 3] = [0.012, 0.013, 0.011];
const MATURITY_YEARS: [i32; 3] = [1, 2, 3];
fn zero_lag() -> Period {
Period::new(0, TimeUnit::Days)
}
struct MeanVolHelper {
base: YoYOptionletVolHelperBase,
}
impl MeanVolHelper {
fn new(quote: &Shared<SimpleQuote>, pillar: Date) -> Shared<MeanVolHelper> {
let base = YoYOptionletVolHelperBase::new(Handle::new(
Shared::clone(quote) as Shared<dyn Quote>
));
base.set_pillar_date(pillar);
base.set_latest_relevant_date(pillar);
base.set_maturity_date(pillar);
shared(MeanVolHelper { base })
}
}
impl AsObservable for MeanVolHelper {
fn observable(&self) -> &Observable {
self.base.observable()
}
}
impl YoYOptionletVolatilityHelper for MeanVolHelper {
fn base(&self) -> &YoYOptionletVolHelperBase {
&self.base
}
fn implied_quote(&self) -> QlResult<Real> {
let surface = self.base.term_structure()?;
let at_pillar = surface.volatility(self.base.pillar_date(), 0.02, zero_lag())?;
let at_base = surface.volatility(surface.base_date()?, 0.02, zero_lag())?;
Ok(0.5 * (at_pillar + at_base))
}
}
struct Fixture {
helpers: Vec<Shared<dyn YoYOptionletVolatilityHelper>>,
curve: Shared<PiecewiseYoYOptionletVolatilityCurve<Linear>>,
quotes: Vec<Shared<SimpleQuote>>,
}
fn a_curve() -> Fixture {
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(Date::new(15, June, 2026));
let quotes: Vec<Shared<SimpleQuote>> = QUOTES
.iter()
.map(|quote| shared(SimpleQuote::new(Some(*quote))))
.collect();
let helpers: Vec<Shared<dyn YoYOptionletVolatilityHelper>> = quotes
.iter()
.zip(MATURITY_YEARS)
.map(|(quote, years)| {
MeanVolHelper::new(
quote,
Date::new(15, June, 2026) + Period::new(years, TimeUnit::Years),
) as Shared<dyn YoYOptionletVolatilityHelper>
})
.collect();
let curve = PiecewiseYoYOptionletVolatilityCurve::new(
0,
Target::new(),
BusinessDayConvention::ModifiedFollowing,
Actual365Fixed::new(),
Period::new(2, TimeUnit::Months),
Frequency::Monthly,
true,
0.018,
0.022,
BASE_LEVEL,
helpers.clone(),
settings,
)
.unwrap();
Fixture {
helpers,
curve,
quotes,
}
}
#[test]
fn the_bootstrapped_curve_reproduces_every_helpers_quote() {
let fixture = a_curve();
fixture.curve.calculate().unwrap();
for (helper, quote) in fixture.helpers.iter().zip(QUOTES) {
let error = helper.quote_error().unwrap();
assert!(
error.abs() < 1.0e-10,
"quote error {error} on the {quote} helper"
);
}
let dates = fixture.curve.dates().unwrap();
assert_eq!(dates.len(), 4);
assert_eq!(dates[0], Date::new(15, April, 2026));
for (i, helper) in fixture.helpers.iter().enumerate() {
assert_eq!(dates[i + 1], helper.pillar_date());
}
assert!(fixture.curve.times().unwrap()[0] < 0.0);
}
#[test]
fn the_base_node_keeps_the_curves_own_base_level_through_the_bootstrap() {
let fixture = a_curve();
let data = fixture.curve.data().unwrap();
assert_eq!(data[0], BASE_LEVEL);
for (i, quote) in QUOTES.iter().enumerate() {
let expected = 2.0 * quote - BASE_LEVEL;
assert!(
(data[i + 1] - expected).abs() < 1.0e-10,
"node {} solved to {} against {expected}",
i + 1,
data[i + 1]
);
}
assert_eq!(
fixture.curve.base_level().unwrap(),
BASE_LEVEL,
"the surface reports the level it was seeded with"
);
}
#[test]
fn the_traits_statics_match_the_cpp_header() {
let times = [0.0, 1.0, 2.0];
let data = [0.01, 0.005, 0.0];
assert_eq!(
YoYInflationVolatilityTraits::guess(1, ×, &data, false),
0.005
);
assert_eq!(
YoYInflationVolatilityTraits::guess(2, ×, &data, false),
0.002
);
assert_eq!(
YoYInflationVolatilityTraits::guess(2, ×, &data, true),
0.0
);
assert_eq!(
YoYInflationVolatilityTraits::min_value_after(1, ×, &data, false),
0.0,
"0.01 - 0.02 floors at zero"
);
assert_eq!(
YoYInflationVolatilityTraits::max_value_after(1, ×, &data, false),
0.03
);
let mut nodes = [0.01, 0.0, 0.0];
YoYInflationVolatilityTraits::update_guess(&mut nodes, 0.007, 1);
assert_eq!(nodes, [0.01, 0.007, 0.0]);
assert_eq!(YoYInflationVolatilityTraits::max_iterations(), 25);
}
#[test]
fn the_bootstrap_is_lazy_and_reruns_on_a_quote_change() {
let fixture = a_curve();
assert!(!fixture.curve.lazy.borrow().is_calculated());
let first = fixture.curve.data().unwrap()[1];
assert!(fixture.curve.lazy.borrow().is_calculated());
fixture.quotes[0].set_value(Some(0.015));
assert!(!fixture.curve.lazy.borrow().is_calculated());
assert!(
fixture.curve.data().unwrap()[1] > first,
"a higher quoted price must lift the curve"
);
}
#[test]
fn an_empty_helper_set_is_rejected() {
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(Date::new(15, June, 2026));
let built = PiecewiseYoYOptionletVolatilityCurve::new(
0,
Target::new(),
BusinessDayConvention::ModifiedFollowing,
Actual365Fixed::new(),
Period::new(2, TimeUnit::Months),
Frequency::Monthly,
true,
0.018,
0.022,
BASE_LEVEL,
Vec::new(),
settings,
);
let err = match built {
Ok(_) => panic!("expected a construction error"),
Err(err) => err,
};
assert!(err.message().contains("no bootstrap helpers"));
}
}