use std::cell::{Ref, RefCell, RefMut};
use std::rc::Weak;
use crate::errors::QlResult;
use crate::handle::{Handle, RelinkableHandle};
use crate::indexes::Index;
use crate::indexes::inflationindex::{
CpiInterpolationType, InflationIndex, ZeroInflationIndex, inflation_period,
};
use crate::instrument::Instrument;
use crate::instruments::{SwapType, ZeroCouponInflationSwap};
use crate::interestrate::Compounding;
use crate::patterns::observable::AsObservable;
use crate::pricingengine::PricingEngine;
use crate::pricingengines::DiscountingSwapEngine;
use crate::quotes::Quote;
use crate::require;
use crate::settings::Settings;
use crate::shared::{Shared, SharedMut, shared, shared_mut};
use crate::termstructures::bootstraphelper::{BootstrapHelperBase, BootstrapHelperShared};
use crate::termstructures::inflation::inflationtermstructure::ZeroInflationTermStructure;
use crate::termstructures::yields::{FlatForward, Pillar};
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::calendar::Calendar;
use crate::time::calendars::NullCalendar;
use crate::time::date::Date;
use crate::time::daycounter::DayCounter;
use crate::time::frequency::Frequency;
use crate::time::period::Period;
use crate::types::Real;
pub type ZeroInflationHelperBase = BootstrapHelperBase<dyn ZeroInflationTermStructure>;
pub trait ZeroInflationHelper: AsObservable {
fn base(&self) -> &ZeroInflationHelperBase;
fn implied_quote(&self) -> QlResult<Real>;
fn quote(&self) -> &Handle<dyn Quote> {
self.base().quote()
}
fn quote_error(&self) -> QlResult<Real> {
Ok(self.base().quote_value()? - self.implied_quote()?)
}
fn set_term_structure(&self, term_structure: &Shared<dyn ZeroInflationTermStructure>) {
self.base().set_term_structure(term_structure);
}
fn earliest_date(&self) -> Date {
self.base().earliest_date()
}
fn maturity_date(&self) -> Date {
self.base().maturity_date()
}
fn latest_relevant_date(&self) -> Date {
self.base().latest_relevant_date()
}
fn pillar_date(&self) -> Date {
self.base().pillar_date()
}
fn latest_date(&self) -> Date {
self.base().latest_date()
}
}
pub trait RelativeDateZeroInflationHelper: ZeroInflationHelper {
fn initialize_dates(&self);
}
impl BootstrapHelperShared for dyn ZeroInflationHelper {
type TS = dyn ZeroInflationTermStructure;
fn set_term_structure(&self, term_structure: &Shared<dyn ZeroInflationTermStructure>) {
ZeroInflationHelper::set_term_structure(self, term_structure);
}
fn quote_value(&self) -> QlResult<Real> {
self.base().quote_value()
}
fn quote_error(&self) -> QlResult<Real> {
ZeroInflationHelper::quote_error(self)
}
fn pillar_date(&self) -> Date {
ZeroInflationHelper::pillar_date(self)
}
fn latest_relevant_date(&self) -> Date {
ZeroInflationHelper::latest_relevant_date(self)
}
fn maturity_date(&self) -> Date {
ZeroInflationHelper::maturity_date(self)
}
}
pub struct ZeroCouponInflationSwapHelper {
base: ZeroInflationHelperBase,
swap_obs_lag: Period,
maturity: Date,
calendar: Calendar,
payment_convention: BusinessDayConvention,
day_counter: DayCounter,
index: Shared<ZeroInflationIndex>,
observation_interpolation: CpiInterpolationType,
nominal_term_structure: Handle<dyn YieldTermStructure>,
term_structure_handle: RelinkableHandle<dyn ZeroInflationTermStructure>,
settings: Shared<Settings<Date>>,
swap: RefCell<QlResult<ZeroCouponInflationSwap>>,
}
impl ZeroCouponInflationSwapHelper {
#[allow(clippy::too_many_arguments)]
pub fn new(
quote: Handle<dyn Quote>,
swap_obs_lag: Period,
maturity: Date,
calendar: Calendar,
payment_convention: BusinessDayConvention,
day_counter: DayCounter,
zii: &Shared<ZeroInflationIndex>,
observation_interpolation: CpiInterpolationType,
pillar: Pillar,
settings: Shared<Settings<Date>>,
) -> QlResult<Shared<ZeroCouponInflationSwapHelper>> {
let fixing_period = inflation_period(maturity - swap_obs_lag, zii.frequency())?;
let (earliest_date, latest_date, pillar_date) = Self::dates(
fixing_period,
maturity,
zii,
observation_interpolation,
pillar,
)?;
if observation_interpolation == CpiInterpolationType::Linear {
let period_shift = Period::try_from(zii.frequency())?;
let availability_lag = zii.availability_lag();
let excess = swap_obs_lag - period_shift;
require!(
excess
.partial_cmp(&availability_lag)
.is_some_and(std::cmp::Ordering::is_ge),
"inconsistency between swap observation lag {swap_obs_lag}, index period \
{period_shift} and index availability {availability_lag}: need (obsLag-index \
period) >= availLag"
);
}
let nominal_term_structure = Handle::new(shared(FlatForward::moving_with_rate(
0,
NullCalendar::new(),
0.0,
day_counter.clone(),
Compounding::Continuous,
Frequency::Annual,
Shared::clone(&settings),
)) as Shared<dyn YieldTermStructure>);
let helper = Shared::new_cyclic(|weak: &Weak<ZeroCouponInflationSwapHelper>| {
let weak = weak.clone();
let on_eval_change = Box::new(move || {
if let Some(helper) = weak.upgrade() {
helper.initialize_dates();
}
});
let base = ZeroInflationHelperBase::new_relative(
quote,
Shared::clone(&settings),
true,
on_eval_change,
);
let term_structure_handle = RelinkableHandle::empty();
let index = shared(zii.clone_linked_to(term_structure_handle.handle()));
term_structure_handle
.handle()
.unregister_observer(&index.inflation_base().observer());
index.observable().register_observer(&base.observer());
nominal_term_structure.register_observer(&base.observer());
base.set_earliest_date(earliest_date);
base.set_latest_date(latest_date);
if let Some(pillar_date) = pillar_date {
base.set_pillar_date(pillar_date);
}
let helper = ZeroCouponInflationSwapHelper {
base,
swap_obs_lag,
maturity,
calendar,
payment_convention,
day_counter,
index,
observation_interpolation,
nominal_term_structure,
term_structure_handle,
settings,
swap: RefCell::new(Err(crate::errors::QlError::new(
"the helper's swap is built by initialize_dates",
file!(),
line!(),
))),
};
helper.initialize_dates();
helper
});
if let Err(error) = helper.swap.borrow().as_ref() {
return Err(error.clone());
}
Ok(helper)
}
fn dates(
fixing_period: (Date, Date),
maturity: Date,
zii: &Shared<ZeroInflationIndex>,
observation_interpolation: CpiInterpolationType,
pillar: Pillar,
) -> QlResult<(Date, Date, Option<Date>)> {
match observation_interpolation {
CpiInterpolationType::Flat => {
Ok((fixing_period.0, fixing_period.0, Some(fixing_period.0)))
}
CpiInterpolationType::Linear => {
let latest_date = fixing_period.1 + 1;
let pillar_date = match pillar {
Pillar::MaturityDate => Some(latest_date),
Pillar::LastRelevantDate => {
let weight_period = inflation_period(maturity, zii.frequency())?;
let dp = Real::from(weight_period.1 + 1 - weight_period.0);
let dt = Real::from(maturity - weight_period.0);
(dt / dp <= 0.5).then_some(fixing_period.0)
}
};
Ok((fixing_period.0, latest_date, pillar_date))
}
}
}
pub fn swap(&self) -> Ref<'_, QlResult<ZeroCouponInflationSwap>> {
self.swap.borrow()
}
pub fn swap_mut(&self) -> RefMut<'_, QlResult<ZeroCouponInflationSwap>> {
self.swap.borrow_mut()
}
pub fn inflation_index(&self) -> &Shared<ZeroInflationIndex> {
&self.index
}
pub fn nominal_term_structure(&self) -> &Handle<dyn YieldTermStructure> {
&self.nominal_term_structure
}
fn build_swap(&self) -> QlResult<ZeroCouponInflationSwap> {
let start_date = match self.base.evaluation_date() {
Some(date) => date,
None => crate::fail!("no evaluation date set: the helper's swap starts at it"),
};
let mut swap = ZeroCouponInflationSwap::new(
SwapType::Payer,
1.0,
start_date,
self.maturity,
self.calendar.clone(),
self.payment_convention,
self.day_counter.clone(),
0.0,
Shared::clone(&self.index),
self.swap_obs_lag,
self.observation_interpolation,
None,
None,
Shared::clone(&self.settings),
)?;
let engine = DiscountingSwapEngine::new(
self.nominal_term_structure.clone(),
None,
None,
None,
Shared::clone(&self.settings),
);
swap.base_mut()
.set_pricing_engine(shared_mut(engine) as SharedMut<dyn PricingEngine>);
Ok(swap)
}
}
impl AsObservable for ZeroCouponInflationSwapHelper {
fn observable(&self) -> &crate::patterns::observable::Observable {
self.base.observable()
}
}
impl ZeroInflationHelper for ZeroCouponInflationSwapHelper {
fn base(&self) -> &ZeroInflationHelperBase {
&self.base
}
fn implied_quote(&self) -> QlResult<Real> {
let mut swap = self.swap.borrow_mut();
let swap = swap.as_mut().map_err(|error| error.clone())?;
swap.swap_mut().deep_update();
swap.fair_rate()
}
fn set_term_structure(&self, term_structure: &Shared<dyn ZeroInflationTermStructure>) {
self.term_structure_handle
.link_to_weak(Shared::downgrade(term_structure));
self.base.set_term_structure(term_structure);
}
}
impl RelativeDateZeroInflationHelper for ZeroCouponInflationSwapHelper {
fn initialize_dates(&self) {
*self.swap.borrow_mut() = self.build_swap();
}
}
#[cfg(test)]
mod tests {
use std::cell::Cell;
use super::*;
use crate::patterns::observable::Observable;
use crate::quotes::SimpleQuote;
use crate::shared::shared;
use crate::termstructures::inflation::interpolatedzeroinflationcurve::ZeroInflationCurve;
use crate::time::date::Month;
use crate::time::daycounters::actual360::Actual360;
use crate::time::frequency::Frequency;
struct StubHelper {
base: ZeroInflationHelperBase,
curve_set: Cell<bool>,
error_called: Cell<bool>,
}
impl StubHelper {
fn new() -> Shared<StubHelper> {
let quote = shared(SimpleQuote::new(Some(0.03)));
let base = ZeroInflationHelperBase::new(Handle::new(quote));
base.set_pillar_date(Date::new(1, Month::June, 2030));
base.set_latest_relevant_date(Date::new(1, Month::July, 2030));
base.set_maturity_date(Date::new(1, Month::June, 2030));
shared(StubHelper {
base,
curve_set: Cell::new(false),
error_called: Cell::new(false),
})
}
}
impl AsObservable for StubHelper {
fn observable(&self) -> &Observable {
self.base.observable()
}
}
impl ZeroInflationHelper for StubHelper {
fn base(&self) -> &ZeroInflationHelperBase {
&self.base
}
fn implied_quote(&self) -> QlResult<Real> {
Ok(0.01)
}
fn quote_error(&self) -> QlResult<Real> {
self.error_called.set(true);
Ok(-1.0)
}
fn set_term_structure(&self, term_structure: &Shared<dyn ZeroInflationTermStructure>) {
self.curve_set.set(true);
self.base.set_term_structure(term_structure);
}
}
fn curve() -> Shared<dyn ZeroInflationTermStructure> {
let reference = Date::new(27, Month::January, 2026);
let dates = vec![
Date::new(1, Month::December, 2025),
Date::new(1, Month::December, 2030),
];
let curve = ZeroInflationCurve::new(
reference,
dates,
vec![0.02, 0.02],
Frequency::Monthly,
Actual360::new(),
crate::math::interpolations::linear::Linear,
None,
)
.unwrap();
shared(curve) as Shared<dyn ZeroInflationTermStructure>
}
#[test]
fn inflation_helpers_satisfy_the_driver_bound() {
fn accepts_driver_helper<H>()
where
H: BootstrapHelperShared<TS = dyn ZeroInflationTermStructure> + ?Sized,
{
}
accepts_driver_helper::<dyn ZeroInflationHelper>();
}
struct DriverView {
quote_value: Real,
quote_error: Real,
pillar_date: Date,
latest_relevant_date: Date,
maturity_date: Date,
}
fn drive<H>(helper: &Shared<H>, curve: &Shared<dyn ZeroInflationTermStructure>) -> DriverView
where
H: BootstrapHelperShared<TS = dyn ZeroInflationTermStructure> + ?Sized,
{
helper.set_term_structure(curve);
DriverView {
quote_value: helper.quote_value().unwrap(),
quote_error: helper.quote_error().unwrap(),
pillar_date: helper.pillar_date(),
latest_relevant_date: helper.latest_relevant_date(),
maturity_date: helper.maturity_date(),
}
}
#[test]
fn the_driver_bound_routes_through_the_trait_so_overrides_run() {
let helper = StubHelper::new();
let driver: Shared<dyn ZeroInflationHelper> = Shared::clone(&helper) as _;
let curve = curve();
let view = drive(&driver, &curve);
assert!(
helper.curve_set.get(),
"set_term_structure override skipped"
);
assert!(helper.base.term_structure().is_ok());
assert!(helper.error_called.get(), "quote_error override skipped");
assert_eq!(view.quote_error, -1.0);
assert_eq!(view.quote_value, 0.03);
}
#[test]
fn the_driver_bound_reports_the_bases_dates() {
let helper = StubHelper::new();
let driver: Shared<dyn ZeroInflationHelper> = Shared::clone(&helper) as _;
let curve = curve();
let view = drive(&driver, &curve);
assert_eq!(view.pillar_date, Date::new(1, Month::June, 2030));
assert_eq!(view.latest_relevant_date, Date::new(1, Month::July, 2030));
assert_eq!(view.maturity_date, Date::new(1, Month::June, 2030));
}
mod zero_coupon_swap_helper {
use super::*;
use crate::indexes::Index;
use crate::indexes::inflation::UkRpi;
use crate::instrument::Instrument;
use crate::math::interpolations::linear::Linear;
use crate::test_support::{Flag, as_observer};
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::calendars::unitedkingdom::{Market, UnitedKingdom};
use crate::time::date::Month::{August, June, May};
use crate::time::daycounters::actual365fixed::Actual365Fixed;
use crate::time::period::Period;
use crate::time::timeunit::TimeUnit;
use crate::types::Rate;
const BASE_FIXING: Real = 195.0;
const CURVE_BASE_FIXING: Real = 200.0;
fn today() -> Date {
Date::new(13, Month::August, 2007)
}
fn maturity() -> Date {
Date::new(13, August, 2008)
}
fn curve_base_date() -> Date {
Date::new(1, June, 2007)
}
fn lag() -> Period {
Period::new(3, TimeUnit::Months)
}
fn settings_today() -> Shared<Settings<Date>> {
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(today());
settings
}
fn a_curve(rates: Vec<Rate>) -> Shared<dyn ZeroInflationTermStructure> {
shared(
ZeroInflationCurve::new(
today(),
vec![
curve_base_date(),
Date::new(1, May, 2008),
Date::new(1, June, 2012),
],
rates,
Frequency::Monthly,
Actual360::new(),
Linear,
None,
)
.expect("a well-formed zero inflation curve"),
) as Shared<dyn ZeroInflationTermStructure>
}
fn an_index(settings: &Shared<Settings<Date>>) -> Shared<ZeroInflationIndex> {
let index = shared(UkRpi::new(Shared::clone(settings)));
index
.add_fixing(Date::new(1, May, 2007), BASE_FIXING)
.expect("a published figure");
index
.add_fixing(curve_base_date(), CURVE_BASE_FIXING)
.expect("a published figure");
index
}
fn a_helper(
settings: &Shared<Settings<Date>>,
interpolation: CpiInterpolationType,
) -> QlResult<Shared<ZeroCouponInflationSwapHelper>> {
a_helper_with(
settings,
interpolation,
Pillar::LastRelevantDate,
maturity(),
lag(),
)
}
fn a_helper_with(
settings: &Shared<Settings<Date>>,
interpolation: CpiInterpolationType,
pillar: Pillar,
maturity: Date,
obs_lag: Period,
) -> QlResult<Shared<ZeroCouponInflationSwapHelper>> {
ZeroCouponInflationSwapHelper::new(
Handle::new(shared(SimpleQuote::new(Some(0.03))) as Shared<dyn Quote>),
obs_lag,
maturity,
UnitedKingdom::new(Market::Settlement),
BusinessDayConvention::ModifiedFollowing,
Actual365Fixed::new(),
&an_index(settings),
interpolation,
pillar,
Shared::clone(settings),
)
}
#[test]
fn the_dates_collapse_onto_the_observed_fixing_period() {
let helper = a_helper(&settings_today(), CpiInterpolationType::Flat)
.expect("a three-month lag covers UK RPI's availability");
let period_start = Date::new(1, May, 2008);
assert_eq!(helper.earliest_date(), period_start);
assert_eq!(helper.latest_date(), period_start);
assert_eq!(helper.pillar_date(), period_start);
assert_eq!(helper.latest_relevant_date(), period_start);
assert_eq!(helper.maturity_date(), period_start);
let swap = helper.swap();
let swap = swap.as_ref().expect("the swap builds");
assert_eq!(swap.obs_date(), Date::new(13, May, 2008));
assert_eq!(swap.maturity_date(), maturity());
assert_eq!(swap.fixed_rate(), 0.0);
assert_eq!(swap.nominal(), 1.0);
}
#[test]
fn moving_the_evaluation_date_rebuilds_the_swap() {
let settings = settings_today();
let helper = a_helper(&settings, CpiInterpolationType::Flat).expect("a valid lag");
assert_eq!(
helper
.swap()
.as_ref()
.expect("the swap builds")
.start_date(),
today()
);
let moved = Date::new(14, August, 2007);
settings.set_evaluation_date(moved);
assert_eq!(
helper
.swap()
.as_ref()
.expect("the swap rebuilds")
.start_date(),
moved
);
assert_eq!(helper.pillar_date(), Date::new(1, May, 2008));
}
#[test]
fn the_implied_quote_is_the_swaps_fair_rate_on_the_curve_it_is_given() {
let settings = settings_today();
let helper = a_helper(&settings, CpiInterpolationType::Flat).expect("a valid lag");
let curve = a_curve(vec![0.02, 0.03, 0.04]);
assert!(
helper.implied_quote().is_err(),
"no curve has been handed over yet"
);
ZeroInflationHelper::set_term_structure(helper.as_ref(), &curve);
let by_hand = ZeroCouponInflationSwap::new(
SwapType::Payer,
1.0,
today(),
maturity(),
UnitedKingdom::new(Market::Settlement),
BusinessDayConvention::ModifiedFollowing,
Actual365Fixed::new(),
0.0,
shared(an_index(&settings).clone_linked_to(Handle::new(Shared::clone(&curve)))),
lag(),
CpiInterpolationType::Flat,
None,
None,
Shared::clone(&settings),
)
.expect("a valid lag");
let implied = helper.implied_quote().expect("the curve forecasts");
assert!(implied > 0.0);
assert!(
(implied - by_hand.fair_rate().expect("the curve forecasts")).abs() < 1e-14,
"implied {implied}"
);
}
#[test]
fn the_cached_swap_does_not_price_to_zero_at_the_implied_quote() {
let settings = settings_today();
let helper = a_helper(&settings, CpiInterpolationType::Flat).expect("a valid lag");
let curve = a_curve(vec![0.02, 0.03, 0.04]);
ZeroInflationHelper::set_term_structure(helper.as_ref(), &curve);
let implied = helper.implied_quote().expect("the curve forecasts");
let mut swap = helper.swap_mut();
let npv = swap
.as_mut()
.expect("the swap builds")
.npv()
.expect("the flat nominal curve discounts");
assert!(implied.is_finite() && implied > 0.0);
assert!(npv.abs() > 0.01, "npv was {npv}");
}
#[test]
fn the_relink_reaches_neither_the_helper_nor_its_index_copy() {
let helper =
a_helper(&settings_today(), CpiInterpolationType::Flat).expect("a valid lag");
let curve = a_curve(vec![0.02, 0.03, 0.04]);
let on_helper = Flag::new();
helper
.observable()
.register_observer(&as_observer(&on_helper));
let on_index = Flag::new();
helper
.inflation_index()
.observable()
.register_observer(&as_observer(&on_index));
ZeroInflationHelper::set_term_structure(helper.as_ref(), &curve);
assert!(
!Flag::is_up(&on_index),
"the copy still observes the handle"
);
assert!(!Flag::is_up(&on_helper), "the relink reached the helper");
curve.observable().notify_observers();
assert!(
!Flag::is_up(&on_helper),
"the helper must not observe the curve it is bootstrapped against"
);
}
#[test]
fn the_interpolated_dates_straddle_the_observed_fixing_period() {
let helper = a_helper(&settings_today(), CpiInterpolationType::Linear)
.expect("a three-month lag leaves a month over UK RPI's availability");
assert_eq!(helper.earliest_date(), Date::new(1, May, 2008));
assert_eq!(helper.latest_date(), Date::new(1, June, 2008));
assert_eq!(helper.latest_relevant_date(), Date::new(1, June, 2008));
assert_eq!(helper.maturity_date(), Date::new(1, June, 2008));
}
#[test]
fn the_maturity_date_pillar_is_the_windows_far_end() {
let helper = a_helper_with(
&settings_today(),
CpiInterpolationType::Linear,
Pillar::MaturityDate,
maturity(),
lag(),
)
.expect("a valid lag");
assert_eq!(helper.pillar_date(), Date::new(1, June, 2008));
}
#[test]
fn the_weighted_pillar_takes_the_windows_start_early_in_the_month() {
let helper =
a_helper(&settings_today(), CpiInterpolationType::Linear).expect("a valid lag");
assert_eq!(helper.pillar_date(), Date::new(1, May, 2008));
}
#[test]
fn the_weighted_pillar_takes_the_windows_far_end_late_in_the_month() {
let helper = a_helper_with(
&settings_today(),
CpiInterpolationType::Linear,
Pillar::LastRelevantDate,
Date::new(25, August, 2008),
lag(),
)
.expect("a valid lag");
assert_eq!(helper.earliest_date(), Date::new(1, May, 2008));
assert_eq!(helper.pillar_date(), Date::new(1, June, 2008));
}
#[test]
fn an_interpolated_lag_short_of_an_index_period_is_rejected() {
let settings = settings_today();
let short_lag = Period::new(1, TimeUnit::Months);
let error = a_helper_with(
&settings,
CpiInterpolationType::Linear,
Pillar::LastRelevantDate,
maturity(),
short_lag,
)
.err()
.expect("one month of lag cannot cover the interpolation");
assert!(
error
.message()
.contains("need (obsLag-index period) >= availLag"),
"err was: {error}"
);
assert!(
a_helper_with(
&settings,
CpiInterpolationType::Flat,
Pillar::LastRelevantDate,
maturity(),
short_lag,
)
.is_ok(),
"the flat path has no such requirement"
);
}
}
}