use std::cell::RefCell;
use super::coupon::{Coupon, CouponBase};
use crate::errors::QlResult;
use crate::fail;
use crate::handle::Handle;
use crate::indexes::index::Index;
use crate::indexes::inflationindex::{Cpi, CpiInterpolationType, YoYInflationIndex};
use crate::patterns::observable::{AsObservable, Observable, Observer, ResetThenNotify};
use crate::shared::{Shared, SharedMut};
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::date::Date;
use crate::time::daycounter::DayCounter;
use crate::time::period::Period;
use crate::time::timeunit::TimeUnit;
use crate::types::{Integer, Natural, Rate, Real, Spread, Time};
pub struct YoYInflationCoupon {
base: CouponBase,
yoy_index: Shared<YoYInflationIndex>,
observation_lag: Period,
interpolation: CpiInterpolationType,
fixing_days: Natural,
day_counter: DayCounter,
gearing: Real,
spread: Spread,
pricer: RefCell<Option<SharedMut<dyn YoYInflationCouponPricer>>>,
observable: Shared<Observable>,
forwarder: SharedMut<ResetThenNotify>,
}
impl YoYInflationCoupon {
#[allow(clippy::too_many_arguments)]
pub fn new(
payment_date: Date,
nominal: Real,
accrual_start_date: Date,
accrual_end_date: Date,
fixing_days: Natural,
yoy_index: Shared<YoYInflationIndex>,
observation_lag: Period,
interpolation: CpiInterpolationType,
day_counter: DayCounter,
gearing: Real,
spread: Spread,
ref_period_start: Option<Date>,
ref_period_end: Option<Date>,
) -> YoYInflationCoupon {
let (observable, forwarder) = ResetThenNotify::forwarder();
let observer = forwarder.clone() as SharedMut<dyn Observer>;
Index::observable(&*yoy_index).register_observer(&observer);
Index::settings(&*yoy_index).register_eval_date_observer(&observer);
YoYInflationCoupon {
base: CouponBase::new(
payment_date,
nominal,
accrual_start_date,
accrual_end_date,
ref_period_start,
ref_period_end,
None,
),
yoy_index,
observation_lag,
interpolation,
fixing_days,
day_counter,
gearing,
spread,
pricer: RefCell::new(None),
observable,
forwarder,
}
}
pub fn yoy_index(&self) -> &Shared<YoYInflationIndex> {
&self.yoy_index
}
pub fn observation_lag(&self) -> Period {
self.observation_lag
}
pub fn interpolation(&self) -> CpiInterpolationType {
self.interpolation
}
pub fn fixing_days(&self) -> Natural {
self.fixing_days
}
pub fn gearing(&self) -> Real {
self.gearing
}
pub fn spread(&self) -> Spread {
self.spread
}
pub fn fixing_date(&self) -> Date {
Index::fixing_calendar(&*self.yoy_index).advance(
self.reference_period_end() - self.observation_lag,
-(self.fixing_days as Integer),
TimeUnit::Days,
BusinessDayConvention::ModifiedPreceding,
false,
)
}
pub fn index_fixing(&self) -> QlResult<Rate> {
Cpi::lagged_yoy_rate(
&self.yoy_index,
self.accrual_end_date(),
self.observation_lag,
self.interpolation,
)
}
pub fn pricer(&self) -> Option<SharedMut<dyn YoYInflationCouponPricer>> {
self.pricer.borrow().clone()
}
pub fn set_pricer(&self, pricer: SharedMut<dyn YoYInflationCouponPricer>) {
let observer = self.forwarder.clone() as SharedMut<dyn Observer>;
{
let mut slot = self.pricer.borrow_mut();
if let Some(old) = slot.as_ref() {
old.borrow().observable().unregister_observer(&observer);
}
pricer.borrow().observable().register_observer(&observer);
*slot = Some(pricer);
}
self.observable.notify_observers();
}
}
impl AsObservable for YoYInflationCoupon {
fn observable(&self) -> &Observable {
&self.observable
}
}
impl Coupon for YoYInflationCoupon {
fn coupon_base(&self) -> &CouponBase {
&self.base
}
fn amount(&self) -> QlResult<Real> {
Ok(self.rate()? * self.accrual_period() * self.nominal())
}
fn rate(&self) -> QlResult<Rate> {
let slot = self.pricer.borrow();
let Some(pricer) = slot.as_ref() else {
fail!("pricer not set");
};
pricer.borrow_mut().initialize(self);
pricer.borrow().swaplet_rate()
}
fn day_counter(&self) -> DayCounter {
self.day_counter.clone()
}
fn accrued_amount(&self, date: Date) -> QlResult<Real> {
if date <= self.accrual_start_date() || date > self.coupon_base().payment_date() {
Ok(0.0)
} else {
Ok(self.nominal() * self.rate()? * self.accrued_period(date))
}
}
}
pub trait YoYInflationCouponPricer: AsObservable {
fn initialize(&mut self, coupon: &YoYInflationCoupon);
fn swaplet_rate(&self) -> QlResult<Rate>;
fn swaplet_price(&self) -> QlResult<Real>;
fn caplet_rate(&self, _effective_cap: Rate) -> QlResult<Rate> {
fail!("this pricer needs a volatility surface to rate an optionlet");
}
fn floorlet_rate(&self, _effective_floor: Rate) -> QlResult<Rate> {
fail!("this pricer needs a volatility surface to rate an optionlet");
}
}
pub struct SwapletYoYInflationCouponPricer {
nominal_term_structure: Handle<dyn YieldTermStructure>,
gearing: Real,
spread: Spread,
accrual_period: Time,
index_fixing: Option<QlResult<Rate>>,
discount: Option<QlResult<Real>>,
observable: Shared<Observable>,
forwarder: SharedMut<ResetThenNotify>,
}
impl SwapletYoYInflationCouponPricer {
pub fn new() -> Self {
let (observable, forwarder) = ResetThenNotify::forwarder();
SwapletYoYInflationCouponPricer {
nominal_term_structure: Handle::empty(),
gearing: 0.0,
spread: 0.0,
accrual_period: 0.0,
index_fixing: None,
discount: None,
observable,
forwarder,
}
}
pub fn with_nominal_term_structure(
nominal_term_structure: Handle<dyn YieldTermStructure>,
) -> Self {
let mut pricer = Self::new();
let observer = pricer.forwarder.clone() as SharedMut<dyn Observer>;
nominal_term_structure.register_observer(&observer);
pricer.nominal_term_structure = nominal_term_structure;
pricer
}
pub fn nominal_term_structure(&self) -> &Handle<dyn YieldTermStructure> {
&self.nominal_term_structure
}
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 Default for SwapletYoYInflationCouponPricer {
fn default() -> Self {
Self::new()
}
}
impl AsObservable for SwapletYoYInflationCouponPricer {
fn observable(&self) -> &Observable {
&self.observable
}
}
impl YoYInflationCouponPricer for SwapletYoYInflationCouponPricer {
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.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> {
let Some(index_fixing) = &self.index_fixing else {
fail!("pricer not initialized: no coupon captured");
};
Ok(self.gearing * index_fixing.clone()? + 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()?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::currency::Currency;
use crate::indexes::Region;
use crate::interestrate::Compounding;
use crate::patterns::observable::Observer;
use crate::settings::Settings;
use crate::shared::{shared, shared_mut};
use crate::termstructures::yields::FlatForward;
use crate::time::date::Month::{
August, December, February, January, July, June, March, November, September,
};
use crate::time::daycounters::actual360::Actual360;
use crate::time::frequency::Frequency;
const NOMINAL: Real = 1_000_000.0;
const GEARING: Real = 2.5;
const SPREAD: Spread = 0.0035;
fn lag() -> Period {
Period::new(3, TimeUnit::Months)
}
fn published_index(rates: &[(Date, Rate)]) -> Shared<YoYInflationIndex> {
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(Date::new(10, February, 2022));
let index = shared(YoYInflationIndex::new(
"YY_RPI".into(),
Region::uk(),
false,
Frequency::Monthly,
Period::new(1, TimeUnit::Months),
Currency::gbp(),
settings,
));
for &(date, rate) in rates {
index
.add_fixing(date, rate)
.expect("adding a published rate");
}
index
}
fn rates_2021() -> Vec<(Date, Rate)> {
vec![
(Date::new(1, November, 2020), 0.02935),
(Date::new(1, December, 2020), 0.02954),
(Date::new(1, January, 2021), 0.02946),
(Date::new(1, February, 2021), 0.02960),
(Date::new(1, March, 2021), 0.02969),
]
}
fn coupon_ending(
index: &Shared<YoYInflationIndex>,
accrual_end: Date,
payment_date: Date,
) -> YoYInflationCoupon {
YoYInflationCoupon::new(
payment_date,
NOMINAL,
accrual_end - Period::new(1, TimeUnit::Years),
accrual_end,
0,
Shared::clone(index),
lag(),
CpiInterpolationType::Flat,
Actual360::new(),
GEARING,
SPREAD,
None,
None,
)
}
fn swaplet_pricer(coupon: &YoYInflationCoupon) -> SharedMut<SwapletYoYInflationCouponPricer> {
let pricer = shared_mut(SwapletYoYInflationCouponPricer::new());
coupon.set_pricer(pricer.clone() as SharedMut<dyn YoYInflationCouponPricer>);
pricer
}
#[derive(Default)]
struct Flag {
up: bool,
}
impl Observer for Flag {
fn update(&mut self) {
self.up = true;
}
}
#[test]
fn the_amount_gears_and_spreads_the_lagged_yoy_rate() {
let index = published_index(&rates_2021());
let accrual_end = Date::new(10, February, 2021);
let coupon = coupon_ending(&index, accrual_end, Date::new(12, February, 2021));
swaplet_pricer(&coupon);
let fixing = Cpi::lagged_yoy_rate(&index, accrual_end, lag(), CpiInterpolationType::Flat)
.expect("November 2020 is on record");
assert!((fixing - 0.02935).abs() < 1e-12, "fixing was {fixing}");
let expected = NOMINAL * coupon.accrual_period() * (GEARING * fixing + SPREAD);
let amount = coupon.amount().expect("the observed period is published");
assert!((amount - expected).abs() < 1e-10, "amount was {amount}");
}
#[test]
fn the_fixing_date_lags_the_reference_period_end() {
let index = published_index(&[]);
let fixing_date_with = |ref_period_end: Date| {
YoYInflationCoupon::new(
Date::new(13, August, 2008),
NOMINAL,
Date::new(13, August, 2007),
Date::new(13, August, 2008),
0,
Shared::clone(&index),
Period::new(2, TimeUnit::Months),
CpiInterpolationType::Flat,
Actual360::new(),
1.0,
0.0,
Some(Date::new(13, August, 2007)),
Some(ref_period_end),
)
.fixing_date()
};
assert_eq!(
fixing_date_with(Date::new(13, August, 2008)),
Date::new(13, June, 2008)
);
assert_eq!(
fixing_date_with(Date::new(13, September, 2008)),
Date::new(13, July, 2008)
);
}
#[test]
fn a_fixing_published_after_construction_is_read_live() {
let rates = rates_2021();
let index = published_index(&rates[..4]);
let coupon = coupon_ending(&index, Date::new(10, June, 2021), Date::new(10, June, 2021));
swaplet_pricer(&coupon);
let missing = coupon
.amount()
.expect_err("March 2021 is not published yet");
assert!(
missing.to_string().contains("Missing UK YY_RPI fixing"),
"err was: {missing}"
);
let (date, rate) = rates[4];
index
.add_fixing(date, rate)
.expect("March 2021 is published");
let expected = NOMINAL * coupon.accrual_period() * (GEARING * rate + SPREAD);
let amount = coupon
.amount()
.expect("the observed period is now published");
assert!((amount - expected).abs() < 1e-10, "amount was {amount}");
}
#[test]
fn a_published_fixing_notifies_the_coupons_observers() {
let rates = rates_2021();
let index = published_index(&rates[..4]);
let coupon = coupon_ending(&index, Date::new(10, June, 2021), Date::new(10, June, 2021));
let flag = shared_mut(Flag::default());
coupon
.observable()
.register_observer(&(flag.clone() as SharedMut<dyn Observer>));
let (date, rate) = rates[4];
index
.add_fixing(date, rate)
.expect("March 2021 is published");
assert!(flag.borrow().up, "the index reaches the coupon's observers");
}
#[test]
fn a_swaplet_price_accrues_and_discounts_the_rate() {
let index = published_index(&rates_2021());
let payment_date = Date::new(10, March, 2022);
let coupon = coupon_ending(&index, Date::new(10, February, 2021), payment_date);
let curve: Handle<dyn YieldTermStructure> = Handle::new(shared(FlatForward::with_rate(
Date::new(10, February, 2022),
0.03,
Actual360::new(),
Compounding::Continuous,
Frequency::Annual,
))
as Shared<dyn YieldTermStructure>);
let pricer =
shared_mut(SwapletYoYInflationCouponPricer::with_nominal_term_structure(curve.clone()));
coupon.set_pricer(pricer.clone() as SharedMut<dyn YoYInflationCouponPricer>);
let rate = coupon.rate().expect("the observed period is published");
let discount = curve
.current_link()
.expect("the curve is linked")
.discount_date(payment_date, false)
.expect("the payment is on the curve");
assert!(discount < 1.0, "the payment discounts, discount {discount}");
let price = pricer.borrow().swaplet_price().expect("the curve prices");
let expected = rate * coupon.accrual_period() * discount;
assert!((price - expected).abs() < 1e-12, "price was {price}");
}
#[test]
fn a_rate_without_a_pricer_is_an_error() {
let index = published_index(&rates_2021());
let coupon = coupon_ending(
&index,
Date::new(10, February, 2021),
Date::new(10, February, 2021),
);
let err = coupon.rate().expect_err("no pricer is attached");
assert!(err.message().contains("pricer not set"), "err was: {err}");
}
#[test]
fn a_swaplet_price_without_a_nominal_curve_is_an_error() {
let index = published_index(&rates_2021());
let coupon = coupon_ending(
&index,
Date::new(10, February, 2021),
Date::new(10, February, 2021),
);
let pricer = swaplet_pricer(&coupon);
coupon.rate().expect("a rate needs no curve");
let err = pricer
.borrow()
.swaplet_price()
.expect_err("prices need a nominal curve");
assert!(
err.message().contains("no nominal term structure provided"),
"err was: {err}"
);
}
#[test]
fn a_swaplet_rate_before_initialize_is_an_error() {
let pricer = SwapletYoYInflationCouponPricer::new();
let err = pricer.swaplet_rate().expect_err("no coupon was captured");
assert!(
err.message().contains("pricer not initialized"),
"err was: {err}"
);
}
}