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::shared::{Shared, SharedMut, shared_mut};
use crate::termstructures::bootstraptraits::CurveData;
use crate::termstructures::inflation::inflationhelpers::YoYInflationHelper;
use crate::termstructures::inflation::inflationtermstructure::{
InflationTermStructure, InflationTermStructureBase, YoYInflationTermStructure,
};
use crate::termstructures::inflation::inflationtraits::YoYInflationTraits;
use crate::termstructures::inflation::seasonality::Seasonality;
use crate::termstructures::iterativebootstrap::{IterativeBootstrap, PiecewiseCurve};
use crate::termstructures::{TermStructure, TermStructureBase};
use crate::time::date::Date;
use crate::time::daycounter::DayCounter;
use crate::time::frequency::Frequency;
use crate::types::{Rate, Real, Time};
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 PiecewiseYoYInflationCurve<I: Interpolator> {
inflation: InflationTermStructureBase,
instruments: Vec<Shared<dyn YoYInflationHelper>>,
interpolator: I,
data: RefCell<CurveData<I>>,
lazy: SharedMut<LazyObject>,
observable: Shared<Observable>,
updater: SharedMut<CurveUpdater>,
bootstrap: IterativeBootstrap,
accuracy: Real,
self_weak: Weak<dyn YoYInflationTermStructure>,
}
impl PiecewiseYoYInflationCurve<Linear> {
#[allow(clippy::too_many_arguments)]
pub fn new(
reference_date: Date,
base_date: Date,
base_yoy_rate: Rate,
frequency: Frequency,
day_counter: DayCounter,
instruments: Vec<Shared<dyn YoYInflationHelper>>,
seasonality: Option<Shared<dyn Seasonality>>,
) -> QlResult<Shared<PiecewiseYoYInflationCurve<Linear>>> {
require!(!instruments.is_empty(), "no bootstrap helpers given");
let curve = Shared::new_cyclic(|weak: &Weak<PiecewiseYoYInflationCurve<Linear>>| {
let self_weak: Weak<dyn YoYInflationTermStructure> = weak.clone();
let lazy = shared_mut(LazyObject::new(true));
let observable = lazy.borrow().observable_handle();
let updater = shared_mut(CurveUpdater {
lazy: SharedMut::clone(&lazy),
});
PiecewiseYoYInflationCurve {
inflation: InflationTermStructureBase::with_reference_date(
reference_date,
base_date,
frequency,
Some(day_counter),
Some(base_yoy_rate),
seasonality,
),
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.check_seasonality()?;
Ok(curve)
}
}
impl<I: Interpolator + 'static> PiecewiseYoYInflationCurve<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)
}
}
impl<I: Interpolator> AsObservable for PiecewiseYoYInflationCurve<I> {
fn observable(&self) -> &Observable {
&self.observable
}
}
impl<I: Interpolator + 'static> TermStructure for PiecewiseYoYInflationCurve<I> {
fn base(&self) -> &TermStructureBase {
self.inflation.term_structure_base()
}
fn max_date(&self) -> Date {
let _ = self.calculate();
self.data
.borrow()
.max_date()
.or_else(|| self.inflation.term_structure_base().reference_date().ok())
.unwrap_or_else(Date::null)
}
}
impl<I: Interpolator + 'static> InflationTermStructure for PiecewiseYoYInflationCurve<I> {
fn inflation_base(&self) -> &InflationTermStructureBase {
&self.inflation
}
fn as_inflation_term_structure(&self) -> &dyn InflationTermStructure {
self
}
fn update_after_seasonality_change(&self) {
if let Some(update) = LazyObject::deferred_update(&self.lazy) {
update.notify_observers();
}
}
}
impl<I: Interpolator + 'static> YoYInflationTermStructure for PiecewiseYoYInflationCurve<I> {
fn yoy_rate_impl(&self, t: Time) -> QlResult<Rate> {
self.calculate()?;
let data = self.data.borrow();
data.interpolation()?.value(t)
}
}
impl<I: Interpolator + 'static> PiecewiseCurve for PiecewiseYoYInflationCurve<I> {
type Traits = YoYInflationTraits;
type Interp = I;
type TS = dyn YoYInflationTermStructure;
type Helper = dyn YoYInflationHelper;
fn instruments(&self) -> &[Shared<dyn YoYInflationHelper>] {
&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.inflation.term_structure_base().reference_date()
}
fn initial_date(&self) -> QlResult<Date> {
Ok(InflationTermStructure::base_date(self))
}
fn initial_value(&self) -> QlResult<Real> {
InflationTermStructure::base_rate(self)
}
fn time_from_reference(&self, date: Date) -> QlResult<Time> {
TermStructure::time_from_reference(self, date)
}
fn term_structure_shared(&self) -> QlResult<Shared<dyn YoYInflationTermStructure>> {
match self.self_weak.upgrade() {
Some(curve) => Ok(curve),
None => crate::fail!("curve dropped before bootstrap"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::handle::Handle;
use crate::patterns::observable::AsObservable;
use crate::quotes::{Quote, SimpleQuote};
use crate::shared::shared;
use crate::termstructures::inflation::inflationhelpers::YoYInflationHelperBase;
use crate::time::date::Month::{August, July};
use crate::time::daycounters::thirty360::{Convention, Thirty360};
use crate::time::period::Period;
use crate::time::timeunit::TimeUnit;
const BASE_YOY_RATE: Rate = 0.029;
const QUOTES: [Real; 3] = [0.026, 0.028, 0.030];
const MATURITY_YEARS: [i32; 3] = [1, 3, 5];
fn today() -> Date {
Date::new(13, August, 2007)
}
fn base_date() -> Date {
Date::new(1, July, 2007)
}
fn day_counter() -> DayCounter {
Thirty360::with_convention(Convention::BondBasis)
}
struct MeanRateHelper {
base: YoYInflationHelperBase,
}
impl MeanRateHelper {
fn new(quote: &Shared<SimpleQuote>, pillar: Date) -> Shared<MeanRateHelper> {
let base =
YoYInflationHelperBase::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(MeanRateHelper { base })
}
}
impl AsObservable for MeanRateHelper {
fn observable(&self) -> &Observable {
self.base.observable()
}
}
impl YoYInflationHelper for MeanRateHelper {
fn base(&self) -> &YoYInflationHelperBase {
&self.base
}
fn implied_quote(&self) -> QlResult<Real> {
let curve = self.base.term_structure()?;
let at_pillar = curve.time_from_reference(self.base.pillar_date())?;
let at_base = curve.time_from_reference(curve.base_date())?;
Ok(0.5 * (curve.yoy_rate(at_pillar, true)? + curve.yoy_rate(at_base, true)?))
}
}
struct Fixture {
helpers: Vec<Shared<dyn YoYInflationHelper>>,
curve: Shared<PiecewiseYoYInflationCurve<Linear>>,
}
fn a_curve() -> Fixture {
let helpers: Vec<Shared<dyn YoYInflationHelper>> = QUOTES
.iter()
.zip(MATURITY_YEARS)
.map(|(quote, years)| {
MeanRateHelper::new(
&shared(SimpleQuote::new(Some(*quote))),
today() + Period::new(years, TimeUnit::Years),
) as Shared<dyn YoYInflationHelper>
})
.collect();
let curve = PiecewiseYoYInflationCurve::new(
today(),
base_date(),
BASE_YOY_RATE,
Frequency::Monthly,
day_counter(),
helpers.clone(),
None,
)
.unwrap();
Fixture { helpers, curve }
}
#[test]
fn the_bootstrapped_curve_reproduces_every_helpers_quote() {
let fixture = a_curve();
let (helpers, curve) = (&fixture.helpers, &fixture.curve);
curve.calculate().unwrap();
for (helper, quote) in 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 = curve.dates().unwrap();
assert_eq!(
dates.len(),
helpers.len() + 1,
"one node per helper, plus the base-date node"
);
assert_eq!(dates[0], base_date());
for (i, helper) in helpers.iter().enumerate() {
assert_eq!(dates[i + 1], helper.pillar_date());
}
}
#[test]
fn the_base_node_keeps_the_curves_own_base_rate_through_the_bootstrap() {
let curve = a_curve().curve;
let data = curve.data().unwrap();
assert_eq!(data[0], BASE_YOY_RATE);
for (i, quote) in QUOTES.iter().enumerate() {
let expected = 2.0 * quote - BASE_YOY_RATE;
assert!(
(data[i + 1] - expected).abs() < 1.0e-10,
"node {} solved to {} against {expected}",
i + 1,
data[i + 1]
);
assert_ne!(data[i + 1], data[0], "the fixture must not be degenerate");
}
assert_ne!(
data[0], 0.02,
"a 0.02 base rate would hide a seeding mis-wire"
);
}
#[test]
fn the_curve_interpolates_linearly_between_the_solved_pillars() {
let curve = a_curve().curve;
let (times, data) = (curve.times().unwrap(), curve.data().unwrap());
let midpoint = 0.5 * (times[1] + times[2]);
let expected = 0.5 * (data[1] + data[2]);
assert!((curve.yoy_rate(midpoint, false).unwrap() - expected).abs() < 1.0e-12);
assert!((curve.yoy_rate(times[0], false).unwrap() - BASE_YOY_RATE).abs() < 1.0e-12);
}
#[test]
fn the_bootstrap_is_lazy_and_reruns_on_a_quote_change() {
let quote = shared(SimpleQuote::new(Some(0.026)));
let helper = MeanRateHelper::new("e, today() + Period::new(5, TimeUnit::Years));
let curve = PiecewiseYoYInflationCurve::new(
today(),
base_date(),
BASE_YOY_RATE,
Frequency::Monthly,
day_counter(),
vec![Shared::clone(&helper) as Shared<dyn YoYInflationHelper>],
None,
)
.unwrap();
assert!(!curve.lazy.borrow().is_calculated());
let first = curve.data().unwrap()[1];
assert!(curve.lazy.borrow().is_calculated());
assert!((first - (2.0 * 0.026 - BASE_YOY_RATE)).abs() < 1.0e-10);
quote.set_value(Some(0.04));
assert!(!curve.lazy.borrow().is_calculated());
assert!(
curve.data().unwrap()[1] > first,
"a higher quoted rate must lift the curve"
);
}
#[test]
fn an_empty_helper_set_is_rejected() {
let built = PiecewiseYoYInflationCurve::new(
today(),
base_date(),
BASE_YOY_RATE,
Frequency::Monthly,
day_counter(),
Vec::new(),
None,
);
let err = match built {
Ok(_) => panic!("expected a construction error"),
Err(err) => err,
};
assert!(err.message().contains("no bootstrap helpers"));
}
}
#[cfg(test)]
mod yy_term_structure_oracle {
use super::*;
use crate::handle::{Handle, RelinkableHandle};
use crate::indexes::Index;
use crate::indexes::inflation::UkRpi;
use crate::indexes::inflationindex::{
CpiInterpolationType, InflationIndex, YoYInflationIndex, ZeroInflationIndex,
};
use crate::instrument::Instrument;
use crate::instruments::{SwapType, YearOnYearInflationSwap};
use crate::interestrate::Compounding;
use crate::pricingengine::PricingEngine;
use crate::pricingengines::DiscountingSwapEngine;
use crate::quotes::SimpleQuote;
use crate::settings::Settings;
use crate::shared::{SharedMut, shared};
use crate::termstructures::inflation::inflationhelpers::YearOnYearInflationSwapHelper;
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::unitedkingdom::{self, UnitedKingdom};
use crate::time::date::Day;
use crate::time::date::Month::{August, January, July, June};
use crate::time::daycounters::actual360::Actual360;
use crate::time::daycounters::thirty360::{Convention, Thirty360};
use crate::time::period::Period;
use crate::time::schedule::MakeSchedule;
use crate::time::timeunit::TimeUnit;
use crate::types::Real;
const FIX_DATA: [Real; 31] = [
189.9, 189.9, 189.6, 190.5, 191.6, 192.0, 192.2, 192.2, 192.6, 193.1, 193.3, 193.6, 194.1,
193.4, 194.2, 195.0, 196.5, 197.7, 198.5, 198.5, 199.2, 200.1, 200.4, 201.1, 202.7, 201.6,
203.1, 204.4, 205.4, 206.2, 207.3,
];
const YY_DATA: [(Day, crate::time::date::Month, crate::time::date::Year, Real); 15] = [
(13, August, 2008, 2.95),
(13, August, 2009, 2.95),
(13, August, 2010, 2.93),
(15, August, 2011, 2.955),
(13, August, 2012, 2.945),
(13, August, 2013, 2.985),
(13, August, 2014, 3.01),
(13, August, 2015, 3.035),
(13, August, 2016, 3.055),
(13, August, 2017, 3.075),
(13, August, 2019, 3.105),
(15, August, 2022, 3.135),
(13, August, 2027, 3.155),
(13, August, 2032, 3.145),
(13, August, 2037, 3.145),
];
fn uk() -> Calendar {
UnitedKingdom::new(unitedkingdom::Market::Settlement)
}
fn day_counter() -> DayCounter {
Thirty360::with_convention(Convention::BondBasis)
}
fn observation_lag() -> Period {
Period::new(2, TimeUnit::Months)
}
fn maturity(row: usize) -> Date {
let (day, month, year, _) = YY_DATA[row];
Date::new(day, month, year)
}
fn quoted_rate(row: usize) -> Real {
YY_DATA[row].3 / 100.0
}
fn today() -> Date {
uk().adjust(
Date::new(13, August, 2007),
BusinessDayConvention::Following,
)
}
fn a_published_rpi(settings: &Shared<Settings<Date>>) -> Shared<ZeroInflationIndex> {
let schedule = MakeSchedule::new()
.from(Date::new(1, January, 2005))
.to(Date::new(1, July, 2007))
.with_tenor(Period::new(1, TimeUnit::Months))
.with_calendar(uk())
.with_convention(BusinessDayConvention::ModifiedFollowing)
.build();
let rpi = shared(UkRpi::new(Shared::clone(settings)));
for (date, &figure) in schedule.dates().iter().zip(FIX_DATA.iter()) {
rpi.add_fixing(*date, figure).expect("a published figure");
}
rpi
}
fn a_nominal_curve() -> Handle<dyn YieldTermStructure> {
Handle::new(shared(FlatForward::with_rate(
Date::new(13, August, 2007),
0.05,
Actual360::new(),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>)
}
struct Fixture {
settings: Shared<Settings<Date>>,
index: Shared<YoYInflationIndex>,
nominal: Handle<dyn YieldTermStructure>,
helpers: Vec<Shared<YearOnYearInflationSwapHelper>>,
curve: Shared<PiecewiseYoYInflationCurve<Linear>>,
handle: RelinkableHandle<dyn YoYInflationTermStructure>,
}
fn a_bootstrapped_curve() -> Fixture {
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(today());
let rpi = a_published_rpi(&settings);
let handle = RelinkableHandle::<dyn YoYInflationTermStructure>::empty();
let index = shared(
YoYInflationIndex::from_underlying(Shared::clone(&rpi))
.with_term_structure(handle.handle()),
);
let nominal = a_nominal_curve();
let helpers: Vec<Shared<YearOnYearInflationSwapHelper>> = (0..YY_DATA.len())
.map(|row| {
YearOnYearInflationSwapHelper::new(
Handle::new(shared(SimpleQuote::new(Some(quoted_rate(row))))),
observation_lag(),
maturity(row),
uk(),
BusinessDayConvention::ModifiedFollowing,
day_counter(),
&index,
CpiInterpolationType::Flat,
nominal.clone(),
Pillar::LastRelevantDate,
Shared::clone(&settings),
)
.expect("a well-formed helper")
})
.collect();
let curve = PiecewiseYoYInflationCurve::<Linear>::new(
today(),
rpi.last_fixing_date().expect("RPI has history"),
quoted_rate(0),
index.frequency(),
day_counter(),
helpers
.iter()
.map(|helper| Shared::clone(helper) as Shared<dyn YoYInflationHelper>)
.collect(),
None,
)
.expect("fifteen helpers");
handle.link_to(Shared::clone(&curve) as Shared<dyn YoYInflationTermStructure>);
Fixture {
settings,
index,
nominal,
helpers,
curve,
handle,
}
}
#[test]
fn the_front_coupon_of_the_first_swap_fixes_on_the_quantlib_date() {
let fixture = a_bootstrapped_curve();
let swap = fixture.helpers[0].swap();
let swap = swap.as_ref().expect("the contract was built");
assert_eq!(
swap.yoy_coupons()[0].fixing_date(),
Date::new(13, June, 2008)
);
}
#[test]
fn every_pillar_swap_reprices_to_zero() {
let fixture = a_bootstrapped_curve();
let reference = fixture
.nominal
.current_link()
.unwrap()
.reference_date()
.unwrap();
for row in 1..YY_DATA.len() {
let schedule = MakeSchedule::new()
.from(reference)
.to(maturity(row))
.with_convention(BusinessDayConvention::Unadjusted)
.with_calendar(uk())
.with_tenor(Period::new(1, TimeUnit::Years))
.backwards()
.build();
let mut swap = YearOnYearInflationSwap::new(
SwapType::Payer,
1_000_000.0,
schedule.clone(),
quoted_rate(row),
day_counter(),
schedule,
Shared::clone(&fixture.index),
observation_lag(),
CpiInterpolationType::Flat,
0.0,
day_counter(),
uk(),
BusinessDayConvention::ModifiedFollowing,
Shared::clone(&fixture.settings),
)
.expect("both legs are fully specified");
swap.base_mut()
.set_pricing_engine(shared_mut(DiscountingSwapEngine::new(
fixture.nominal.clone(),
None,
None,
None,
Shared::clone(&fixture.settings),
)) as SharedMut<dyn PricingEngine>);
let npv = swap.npv().expect("the curve prices it");
assert!(
npv.abs() < 1e-6,
"pillar {row} ({}) quoted at {} reprices to {npv}",
maturity(row),
quoted_rate(row)
);
}
drop(fixture.curve);
drop(fixture.handle);
}
}