use std::cell::RefCell;
use std::marker::PhantomData;
use crate::currency::Currency;
use crate::errors::{QlError, QlResult};
use crate::handle::{Handle, RelinkableHandle};
use crate::indexes::inflation::YyGenericCpi;
use crate::indexes::inflationindex::CpiInterpolationType;
use crate::instrument::Instrument;
use crate::instruments::{CapFloorType, MakeYoYInflationCapFloor};
use crate::math::interpolations::Interpolator;
use crate::math::interpolations::linear::Linear;
use crate::math::solver1d::Solver1D;
use crate::math::solvers1d::brent::Brent;
use crate::pricingengine::PricingEngine;
use crate::pricingengines::inflation::YoYInflationCapFloorEngine;
use crate::quotes::{Quote, SimpleQuote};
use crate::shared::{Shared, SharedMut, shared};
use crate::termstructures::inflation::yoycapfloortermpricesurface::YoYCapFloorTermPriceSurface;
use crate::time::calendars::target::Target;
use crate::time::date::Date;
use crate::time::daycounters::actual365fixed::Actual365Fixed;
use crate::time::period::Period;
use crate::time::timeunit::TimeUnit;
use crate::types::{Rate, Real, Size, Volatility};
use crate::{fail, require};
use super::yoyoptionlethelpers::{YoYOptionletHelper, YoYOptionletVolatilityHelper};
use super::{
ConstantYoYOptionletVolatility, InterpolatedYoYOptionletVolatilityCurve,
PiecewiseYoYOptionletVolatilityCurve, YoYOptionletVolatilitySurface,
};
pub trait YoYOptionletStripper {
fn initialize(
&self,
surface: &Shared<dyn YoYCapFloorTermPriceSurface>,
pricer: &SharedMut<YoYInflationCapFloorEngine>,
vol_handle: &RelinkableHandle<dyn YoYOptionletVolatilitySurface>,
slope: Real,
) -> QlResult<()>;
fn min_strike(&self) -> QlResult<Rate>;
fn max_strike(&self) -> QlResult<Rate>;
fn strikes(&self) -> QlResult<Vec<Rate>>;
fn slice(&self, d: Date) -> QlResult<(Vec<Rate>, Vec<Volatility>)>;
}
struct StripperState {
surface: Shared<dyn YoYCapFloorTermPriceSurface>,
lag: Period,
vol_curves: Vec<Shared<dyn YoYOptionletVolatilitySurface>>,
}
pub struct InterpolatedYoYOptionletStripper<I: Interpolator> {
state: RefCell<Option<StripperState>>,
_interpolator: PhantomData<I>,
}
impl InterpolatedYoYOptionletStripper<Linear> {
pub fn new() -> InterpolatedYoYOptionletStripper<Linear> {
InterpolatedYoYOptionletStripper {
state: RefCell::new(None),
_interpolator: PhantomData,
}
}
}
impl Default for InterpolatedYoYOptionletStripper<Linear> {
fn default() -> Self {
InterpolatedYoYOptionletStripper::new()
}
}
impl<I: Interpolator> InterpolatedYoYOptionletStripper<I> {
fn with_state<R>(&self, f: impl FnOnce(&StripperState) -> QlResult<R>) -> QlResult<R> {
match self.state.borrow().as_ref() {
Some(state) => f(state),
None => fail!("stripper not initialized: no price surface set"),
}
}
}
impl YoYOptionletStripper for InterpolatedYoYOptionletStripper<Linear> {
fn initialize(
&self,
surface: &Shared<dyn YoYCapFloorTermPriceSurface>,
pricer: &SharedMut<YoYInflationCapFloorEngine>,
vol_handle: &RelinkableHandle<dyn YoYOptionletVolatilitySurface>,
slope: Real,
) -> QlResult<()> {
let lag = surface.observation_lag();
let frequency = surface.frequency();
let index_is_interpolated = surface.index_is_interpolated();
let fixing_days = surface.fixing_days();
let settlement_days = 0;
let Some(calendar) = surface.calendar() else {
fail!("the price surface holds a calendar by construction");
};
let bdc = surface.business_day_convention();
let day_counter = surface.require_day_counter()?;
let settings = Shared::clone(surface.surface_base().settings());
let reference = surface.reference_date()?;
let max_floor = *surface
.floor_strikes()
.last()
.expect("the surface carries floor strikes");
let mut use_type = CapFloorType::Floor;
let tp_min = surface.maturities()[0];
let h_yoy = Handle::new(surface.yoy_ts()?);
let an_index = shared(
YyGenericCpi::new(
frequency,
false,
lag,
Currency::new("", "", 0, "", "", 0),
Shared::clone(&settings),
)
.with_term_structure(h_yoy),
);
let n = (surface.time_from_reference(surface.min_maturity()?)? + 0.5).floor() as Size;
require!(n > 0, "first maturity in price surface not > 0: {n}");
let objective_lag = surface.observation_lag();
let d0 = surface.base_date()?;
let d1 = surface.min_maturity()? + Period::new(7, TimeUnit::Days);
let t0 = day_counter.year_fraction(reference, d0);
let t1 = day_counter.year_fraction(reference, d1);
let mut vol_curves: Vec<Shared<dyn YoYOptionletVolatilitySurface>> = Vec::new();
for &k in surface.strikes() {
if k > max_floor {
use_type = CapFloorType::Cap;
}
let solver_tolerance = 1e-7;
let (lo, hi) = (0.00001, 0.08);
let guess = (hi + lo) / 2.0;
let price_to_match = match use_type {
CapFloorType::Cap => surface.cap_price_by_tenor(tp_min, k)?,
_ => surface.floor_price_by_tenor(tp_min, k)?,
};
let mut capfloor = MakeYoYInflationCapFloor::new(
use_type,
Shared::clone(&an_index),
n,
calendar.clone(),
lag,
CpiInterpolationType::Flat,
Shared::clone(&settings),
)
.with_nominal(10_000.0)
.with_strike(k)
.build()?;
capfloor
.base_mut()
.set_pricing_engine(SharedMut::clone(pricer) as SharedMut<dyn PricingEngine>);
let error_slot: RefCell<Option<QlError>> = RefCell::new(None);
let mut evaluate = |guess: Volatility| -> QlResult<Real> {
let v1 = guess;
let v0 = guess - slope * (t1 - t0) * guess;
let curve = InterpolatedYoYOptionletVolatilityCurve::new(
0,
Target::new(),
crate::time::businessdayconvention::BusinessDayConvention::ModifiedFollowing,
Actual365Fixed::new(),
objective_lag,
frequency,
false,
vec![d0, d1],
vec![v0, v1],
-1.0,
3.0,
Linear,
Shared::clone(&settings),
)?;
vol_handle.link_to(shared(curve) as Shared<dyn YoYOptionletVolatilitySurface>);
Ok(price_to_match - capfloor.npv()?)
};
let objective = |guess: Volatility| -> Real {
match evaluate(guess) {
Ok(value) => value,
Err(error) => {
*error_slot.borrow_mut() = Some(error);
Real::NAN
}
}
};
let found =
match Brent::new().solve_bracketed(objective, solver_tolerance, guess, lo, hi) {
Ok(found) => found,
Err(solver_error) => {
let message = match error_slot.into_inner() {
Some(inner) => inner.message().to_string(),
None => solver_error.message().to_string(),
};
fail!("failed to find solution here because: {message}");
}
};
let notional = 10_000.0;
let mut helper_instruments: Vec<Shared<dyn YoYOptionletVolatilityHelper>> = Vec::new();
for &tp in surface.maturities() {
let next_price = match use_type {
CapFloorType::Cap => surface.cap_price_by_tenor(tp, k)?,
_ => surface.floor_price_by_tenor(tp, k)?,
};
let quote =
Handle::new(shared(SimpleQuote::new(Some(next_price))) as Shared<dyn Quote>);
let n_t = (surface.time_from_reference(surface.yoy_option_date_from_tenor(tp)?)?
+ 0.5)
.floor() as Size;
let helper = YoYOptionletHelper::new(
quote,
notional,
use_type,
lag,
day_counter.clone(),
calendar.clone(),
fixing_days,
&an_index,
CpiInterpolationType::Flat,
k,
n_t,
SharedMut::clone(pricer),
vol_handle.clone(),
Shared::clone(&settings),
)?;
let yoy_vol_black = shared(ConstantYoYOptionletVolatility::new(
found,
settlement_days,
calendar.clone(),
bdc,
day_counter.clone(),
lag,
frequency,
false,
-1.0,
3.0,
Shared::clone(&settings),
)) as Shared<dyn YoYOptionletVolatilitySurface>;
YoYOptionletVolatilityHelper::set_term_structure(helper.as_ref(), &yoy_vol_black);
helper_instruments.push(helper as Shared<dyn YoYOptionletVolatilityHelper>);
}
let t_min = surface.time_from_reference(surface.yoy_option_date_from_tenor(tp_min)?)?;
let base_yoy_volatility = found - slope * t_min * found;
let eps = k.max(0.02) / 1000.0;
let curve = PiecewiseYoYOptionletVolatilityCurve::new(
settlement_days,
calendar.clone(),
bdc,
day_counter.clone(),
lag,
frequency,
index_is_interpolated,
k - eps,
k + eps,
base_yoy_volatility,
helper_instruments,
Shared::clone(&settings),
)?;
curve.calculate()?;
vol_curves.push(curve as Shared<dyn YoYOptionletVolatilitySurface>);
}
*self.state.borrow_mut() = Some(StripperState {
surface: Shared::clone(surface),
lag,
vol_curves,
});
Ok(())
}
fn min_strike(&self) -> QlResult<Rate> {
self.with_state(|state| Ok(state.surface.strikes()[0]))
}
fn max_strike(&self) -> QlResult<Rate> {
self.with_state(|state| {
Ok(*state
.surface
.strikes()
.last()
.expect("the surface carries strikes"))
})
}
fn strikes(&self) -> QlResult<Vec<Rate>> {
self.with_state(|state| Ok(state.surface.strikes().to_vec()))
}
fn slice(&self, d: Date) -> QlResult<(Vec<Rate>, Vec<Volatility>)> {
self.with_state(|state| {
let strikes = state.surface.strikes();
let mut volatilities = Vec::with_capacity(strikes.len());
for (curve, &k) in state.vol_curves.iter().zip(strikes) {
volatilities.push(curve.volatility(d, k, state.lag)?);
}
Ok((strikes.to_vec(), volatilities))
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::handle::Handle;
use crate::instrument::Instrument;
use crate::interestrate::Compounding;
use crate::math::interpolations::linear::Linear;
use crate::settings::Settings;
use crate::shared::shared_mut;
use crate::termstructures::inflation::inflationtermstructure::YoYInflationTermStructure;
use crate::termstructures::inflation::interpolatedyoyinflationcurve::InterpolatedYoYInflationCurve;
use crate::termstructures::yields::FlatForward;
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::date::Month::{April, June};
use crate::time::frequency::Frequency;
use crate::types::Natural;
#[test]
fn relinking_the_engines_volatility_handle_moves_the_npv() {
let settings = shared(Settings::<Date>::new());
let eval = Date::new(15, June, 2026);
settings.set_evaluation_date(eval);
let yoy_curve = shared(
InterpolatedYoYInflationCurve::<Linear>::new(
eval,
vec![Date::new(1, April, 2026), Date::new(1, June, 2032)],
vec![0.02, 0.02],
Frequency::Monthly,
Actual365Fixed::new(),
Linear,
None,
)
.expect("two well-ordered nodes"),
) as Shared<dyn YoYInflationTermStructure>;
let index = shared(
YyGenericCpi::new(
Frequency::Monthly,
false,
Period::new(2, TimeUnit::Months),
Currency::eur(),
Shared::clone(&settings),
)
.with_term_structure(Handle::new(yoy_curve)),
);
let nominal_curve = Handle::new(shared(FlatForward::with_rate(
eval,
0.05,
Actual365Fixed::new(),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>);
let flat_at = |vol: Volatility| -> Shared<dyn YoYOptionletVolatilitySurface> {
shared(ConstantYoYOptionletVolatility::new(
vol,
0 as Natural,
Target::new(),
BusinessDayConvention::ModifiedFollowing,
Actual365Fixed::new(),
Period::new(0, TimeUnit::Days),
Frequency::Monthly,
false,
-1.0,
3.0,
Shared::clone(&settings),
))
};
let vol_handle = RelinkableHandle::<dyn YoYOptionletVolatilitySurface>::empty();
vol_handle.link_to(flat_at(0.01));
let pricer = shared_mut(YoYInflationCapFloorEngine::unit_displaced(
Shared::clone(&index),
vol_handle.handle(),
nominal_curve,
));
let mut capfloor = MakeYoYInflationCapFloor::new(
CapFloorType::Cap,
Shared::clone(&index),
3,
Target::new(),
Period::new(0, TimeUnit::Days),
CpiInterpolationType::Flat,
Shared::clone(&settings),
)
.with_nominal(10_000.0)
.with_strike(0.02)
.with_pricing_engine(SharedMut::clone(&pricer) as SharedMut<dyn PricingEngine>)
.build()
.expect("a well-formed cap");
let quiet = capfloor.npv().expect("the 1 % surface prices it");
assert!(capfloor.base().is_calculated());
vol_handle.link_to(flat_at(0.10));
let noisy = capfloor.npv().expect("the 10 % surface prices it");
assert!(
(noisy - quiet).abs() > 1e-4,
"the relink did not move the NPV ({quiet} vs {noisy}): a cached value \
would let the stripper's Brent solve converge on a constant"
);
assert!(
noisy > quiet,
"an at-the-money cap must gain value with volatility ({quiet} -> {noisy})"
);
}
#[test]
fn the_accessors_refuse_before_initialize() {
let stripper = InterpolatedYoYOptionletStripper::<Linear>::new();
for message in [
stripper
.min_strike()
.expect_err("no surface yet")
.message()
.to_string(),
stripper
.max_strike()
.expect_err("no surface yet")
.message()
.to_string(),
stripper
.strikes()
.expect_err("no surface yet")
.message()
.to_string(),
stripper
.slice(Date::new(15, June, 2027))
.expect_err("no surface yet")
.message()
.to_string(),
] {
assert!(message.contains("stripper not initialized"), "{message}");
}
}
}