use super::coupon::Coupon;
use super::yoyinflationcoupon::{YoYInflationCoupon, YoYInflationCouponPricer};
use crate::errors::QlResult;
use crate::handle::Handle;
use crate::option::OptionType;
use crate::patterns::observable::{AsObservable, Observable, Observer, ResetThenNotify};
use crate::pricingengines::inflation::yoy_optionlet_price;
use crate::shared::{Shared, SharedMut};
use crate::termstructures::volatility::YoYOptionletVolatilitySurface;
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::time::date::Date;
use crate::time::period::Period;
use crate::time::timeunit::TimeUnit;
use crate::types::{Rate, Real, Spread, Time};
use crate::{fail, require};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum YoYOptionletDistribution {
Black,
UnitDisplaced,
Bachelier,
}
pub struct YoYInflationOptionletCouponPricer {
distribution: YoYOptionletDistribution,
caplet_vol: Handle<dyn YoYOptionletVolatilitySurface>,
nominal_term_structure: Handle<dyn YieldTermStructure>,
gearing: Real,
spread: Spread,
accrual_period: Time,
index_fixing: Option<QlResult<Rate>>,
fixing_date: Option<Date>,
discount: Option<QlResult<Real>>,
observable: Shared<Observable>,
forwarder: SharedMut<ResetThenNotify>,
}
impl YoYInflationOptionletCouponPricer {
fn new(
distribution: YoYOptionletDistribution,
caplet_vol: Handle<dyn YoYOptionletVolatilitySurface>,
nominal_term_structure: Handle<dyn YieldTermStructure>,
) -> YoYInflationOptionletCouponPricer {
let (observable, forwarder) = ResetThenNotify::forwarder();
let mut pricer = YoYInflationOptionletCouponPricer {
distribution,
caplet_vol: Handle::empty(),
nominal_term_structure: Handle::empty(),
gearing: 0.0,
spread: 0.0,
accrual_period: 0.0,
index_fixing: None,
fixing_date: None,
discount: None,
observable,
forwarder,
};
let observer = pricer.forwarder.clone() as SharedMut<dyn Observer>;
caplet_vol.register_observer(&observer);
nominal_term_structure.register_observer(&observer);
pricer.caplet_vol = caplet_vol;
pricer.nominal_term_structure = nominal_term_structure;
pricer
}
pub fn black(
caplet_vol: Handle<dyn YoYOptionletVolatilitySurface>,
nominal_term_structure: Handle<dyn YieldTermStructure>,
) -> YoYInflationOptionletCouponPricer {
Self::new(
YoYOptionletDistribution::Black,
caplet_vol,
nominal_term_structure,
)
}
pub fn unit_displaced(
caplet_vol: Handle<dyn YoYOptionletVolatilitySurface>,
nominal_term_structure: Handle<dyn YieldTermStructure>,
) -> YoYInflationOptionletCouponPricer {
Self::new(
YoYOptionletDistribution::UnitDisplaced,
caplet_vol,
nominal_term_structure,
)
}
pub fn bachelier(
caplet_vol: Handle<dyn YoYOptionletVolatilitySurface>,
nominal_term_structure: Handle<dyn YieldTermStructure>,
) -> YoYInflationOptionletCouponPricer {
Self::new(
YoYOptionletDistribution::Bachelier,
caplet_vol,
nominal_term_structure,
)
}
pub fn distribution(&self) -> YoYOptionletDistribution {
self.distribution
}
pub fn caplet_volatility(&self) -> &Handle<dyn YoYOptionletVolatilitySurface> {
&self.caplet_vol
}
pub fn nominal_term_structure(&self) -> &Handle<dyn YieldTermStructure> {
&self.nominal_term_structure
}
pub fn optionlet_rate(&self, option_type: OptionType, eff_strike: Rate) -> QlResult<Rate> {
let Some(fixing_date) = self.fixing_date else {
fail!("pricer not initialized: no coupon captured");
};
require!(!self.caplet_vol.is_empty(), "missing optionlet volatility");
let surface = self.caplet_vol.current_link()?;
let forward = self.index_fixing()?;
if fixing_date <= surface.base_date()? {
let (a, b) = match option_type {
OptionType::Call => (forward, eff_strike),
OptionType::Put => (eff_strike, forward),
};
return Ok((a - b).max(0.0));
}
let std_dev = surface
.total_variance(fixing_date, eff_strike, Period::new(0, TimeUnit::Days))?
.sqrt();
yoy_optionlet_price(
self.distribution,
option_type,
eff_strike,
forward,
std_dev,
1.0,
)
}
fn index_fixing(&self) -> QlResult<Rate> {
let Some(index_fixing) = &self.index_fixing else {
fail!("pricer not initialized: no coupon captured");
};
index_fixing.clone()
}
fn discount_at(&self, payment_date: Date) -> QlResult<Real> {
let curve = self.nominal_term_structure.current_link()?;
if payment_date > curve.reference_date()? {
curve.discount_date(payment_date, false)
} else {
Ok(1.0)
}
}
}
impl AsObservable for YoYInflationOptionletCouponPricer {
fn observable(&self) -> &Observable {
&self.observable
}
}
impl YoYInflationCouponPricer for YoYInflationOptionletCouponPricer {
fn initialize(&mut self, coupon: &YoYInflationCoupon) {
self.gearing = coupon.gearing();
self.spread = coupon.spread();
self.accrual_period = coupon.accrual_period();
self.index_fixing = Some(coupon.index_fixing());
self.fixing_date = Some(coupon.fixing_date());
self.discount = if self.nominal_term_structure.is_empty() {
None
} else {
Some(self.discount_at(coupon.coupon_base().payment_date()))
};
}
fn swaplet_rate(&self) -> QlResult<Rate> {
Ok(self.gearing * self.index_fixing()? + self.spread)
}
fn swaplet_price(&self) -> QlResult<Real> {
let Some(discount) = &self.discount else {
fail!("no nominal term structure provided");
};
Ok(self.swaplet_rate()? * self.accrual_period * discount.clone()?)
}
fn caplet_rate(&self, effective_cap: Rate) -> QlResult<Rate> {
Ok(self.gearing * self.optionlet_rate(OptionType::Call, effective_cap)?)
}
fn floorlet_rate(&self, effective_floor: Rate) -> QlResult<Rate> {
Ok(self.gearing * self.optionlet_rate(OptionType::Put, effective_floor)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cashflows::yoyinflationcoupon::SwapletYoYInflationCouponPricer;
#[test]
fn the_swaplet_pricer_refuses_an_optionlet() {
let pricer = SwapletYoYInflationCouponPricer::new();
let capped = pricer.caplet_rate(0.03).expect_err("no volatility is held");
assert!(
capped.message().contains("needs a volatility"),
"err was: {capped}"
);
let floored = pricer
.floorlet_rate(0.03)
.expect_err("no volatility is held");
assert!(
floored.message().contains("needs a volatility"),
"err was: {floored}"
);
}
#[test]
fn an_optionlet_before_initialize_is_an_error() {
let pricer = YoYInflationOptionletCouponPricer::black(Handle::empty(), Handle::empty());
let err = pricer
.optionlet_rate(OptionType::Call, 0.03)
.expect_err("no coupon was captured");
assert!(
err.message().contains("pricer not initialized"),
"err was: {err}"
);
}
#[test]
fn an_optionlet_without_a_surface_is_an_error() {
let mut pricer = YoYInflationOptionletCouponPricer::black(Handle::empty(), Handle::empty());
pricer.fixing_date = Some(Date::new(1, crate::time::date::Month::June, 2026));
pricer.index_fixing = Some(Ok(0.02));
let err = pricer
.optionlet_rate(OptionType::Call, 0.03)
.expect_err("no volatility surface is linked");
assert!(
err.message().contains("missing optionlet volatility"),
"err was: {err}"
);
}
}