use crate::cashflow::{CashFlow, Leg};
use crate::cashflows::{CashFlows, YoYInflationLeg};
use crate::errors::QlResult;
use crate::handle::Handle;
use crate::indexes::inflationindex::{CpiInterpolationType, YoYInflationIndex};
use crate::instrument::Instrument;
use crate::instruments::capfloor::CapFloorType;
use crate::instruments::inflationcapfloor::YoYInflationCapFloor;
use crate::pricingengine::PricingEngine;
use crate::settings::Settings;
use crate::shared::{Shared, SharedMut};
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::calendar::Calendar;
use crate::time::date::Date;
use crate::time::dategenerationrule::DateGeneration;
use crate::time::daycounter::DayCounter;
use crate::time::daycounters::thirty360::{Convention, Thirty360};
use crate::time::frequency::Frequency;
use crate::time::period::Period;
use crate::time::schedule::MakeSchedule;
use crate::time::timeunit::TimeUnit;
use crate::types::{Integer, Natural, Rate, Real, Size};
pub struct MakeYoYInflationCapFloor {
cap_floor_type: CapFloorType,
index: Shared<YoYInflationIndex>,
length: Size,
calendar: Calendar,
observation_lag: Period,
interpolation: CpiInterpolationType,
strike: Option<Rate>,
nominal: Real,
roll: BusinessDayConvention,
day_counter: DayCounter,
fixing_days: Natural,
first_caplet_excluded: bool,
as_optionlet: bool,
effective_date: Option<Date>,
forward_start: Period,
nominal_term_structure: Handle<dyn YieldTermStructure>,
engine: Option<SharedMut<dyn PricingEngine>>,
settings: Shared<Settings<Date>>,
}
impl MakeYoYInflationCapFloor {
pub fn new(
cap_floor_type: CapFloorType,
index: Shared<YoYInflationIndex>,
length: Size,
calendar: Calendar,
observation_lag: Period,
interpolation: CpiInterpolationType,
settings: Shared<Settings<Date>>,
) -> MakeYoYInflationCapFloor {
MakeYoYInflationCapFloor {
cap_floor_type,
index,
length,
calendar,
observation_lag,
interpolation,
strike: None,
nominal: 1_000_000.0,
roll: BusinessDayConvention::ModifiedFollowing,
day_counter: Thirty360::with_convention(Convention::BondBasis),
fixing_days: 0,
first_caplet_excluded: false,
as_optionlet: false,
effective_date: None,
forward_start: Period::new(0, TimeUnit::Days),
nominal_term_structure: Handle::<dyn YieldTermStructure>::empty(),
engine: None,
settings,
}
}
pub fn with_nominal(mut self, nominal: Real) -> MakeYoYInflationCapFloor {
self.nominal = nominal;
self
}
pub fn with_effective_date(mut self, effective_date: Date) -> MakeYoYInflationCapFloor {
self.effective_date = Some(effective_date);
self
}
pub fn with_payment_day_counter(mut self, day_counter: DayCounter) -> MakeYoYInflationCapFloor {
self.day_counter = day_counter;
self
}
pub fn with_payment_adjustment(
mut self,
convention: BusinessDayConvention,
) -> MakeYoYInflationCapFloor {
self.roll = convention;
self
}
pub fn with_fixing_days(mut self, fixing_days: Natural) -> MakeYoYInflationCapFloor {
self.fixing_days = fixing_days;
self
}
pub fn with_pricing_engine(
mut self,
engine: SharedMut<dyn PricingEngine>,
) -> MakeYoYInflationCapFloor {
self.engine = Some(engine);
self
}
pub fn as_optionlet(mut self, as_optionlet: bool) -> MakeYoYInflationCapFloor {
self.as_optionlet = as_optionlet;
self
}
pub fn with_forward_start(mut self, forward_start: Period) -> MakeYoYInflationCapFloor {
self.forward_start = forward_start;
self
}
pub fn with_first_caplet_excluded(mut self) -> MakeYoYInflationCapFloor {
self.first_caplet_excluded = true;
self
}
pub fn with_strike(mut self, strike: Rate) -> MakeYoYInflationCapFloor {
self.strike = Some(strike);
self
}
pub fn with_atm_strike(
mut self,
nominal_term_structure: Handle<dyn YieldTermStructure>,
) -> MakeYoYInflationCapFloor {
self.nominal_term_structure = nominal_term_structure;
self
}
pub fn build(self) -> QlResult<YoYInflationCapFloor> {
let start_date = match self.effective_date {
Some(effective_date) => effective_date,
None => {
let reference_date = match self.settings.evaluation_date() {
Some(today) => today,
None => crate::fail!(
"no evaluation date set: MakeYoYInflationCapFloor needs a reference date to derive the start date"
),
};
let spot_date = self.calendar.advance(
reference_date,
self.fixing_days as Integer,
TimeUnit::Days,
BusinessDayConvention::Following,
false,
);
spot_date + self.forward_start
}
};
let end_date = self.calendar.advance(
start_date,
self.length as Integer,
TimeUnit::Years,
BusinessDayConvention::Unadjusted,
false,
);
let schedule = MakeSchedule::new()
.from(start_date)
.to(end_date)
.with_frequency(Frequency::Annual)
.with_calendar(self.calendar.clone())
.with_convention(BusinessDayConvention::Unadjusted)
.with_termination_date_convention(BusinessDayConvention::Unadjusted)
.with_rule(DateGeneration::Forward)
.build();
let mut coupons = YoYInflationLeg::new(
schedule,
self.calendar,
self.index,
self.observation_lag,
self.interpolation,
)
.with_payment_adjustment(self.roll)
.with_payment_day_counter(self.day_counter)
.with_notional(self.nominal)
.coupons()?;
if self.first_caplet_excluded && !coupons.is_empty() {
coupons.remove(0);
}
if self.as_optionlet && coupons.len() > 1 {
coupons.drain(..coupons.len() - 1);
}
let strikes = match (self.strike, self.nominal_term_structure.is_empty()) {
(Some(_), false) => {
crate::fail!("explicit strike and ATM curve both given")
}
(Some(strike), true) => vec![strike],
(None, false) => {
let curve = self.nominal_term_structure.current_link()?;
let reference = curve.reference_date()?;
let leg: Leg = coupons
.iter()
.map(|coupon| Shared::clone(coupon) as Shared<dyn CashFlow>)
.collect();
vec![CashFlows::atm_rate(
&leg,
curve.as_ref(),
&self.settings,
Some(false),
Some(reference),
None,
None,
)?]
}
(None, true) => crate::fail!("no strike and no ATM curve given"),
};
let mut cap_floor = YoYInflationCapFloor::with_strikes(
self.cap_floor_type,
coupons,
strikes,
self.settings,
)?;
if let Some(engine) = self.engine {
cap_floor.base_mut().set_pricing_engine(engine);
}
Ok(cap_floor)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cashflows::{Coupon, YoYInflationCoupon};
use crate::event::Event;
use crate::handle::RelinkableHandle;
use crate::indexes::index::Index;
use crate::indexes::inflation::UkRpi;
use crate::indexes::inflationindex::InflationIndex;
use crate::instruments::inflationcapfloor::YoYInflationCapFloor;
use crate::interestrate::Compounding;
use crate::math::interpolations::linear::Linear;
use crate::quotes::SimpleQuote;
use crate::shared::shared;
use crate::termstructures::inflation::inflationhelpers::{
YearOnYearInflationSwapHelper, YoYInflationHelper,
};
use crate::termstructures::inflation::inflationtermstructure::YoYInflationTermStructure;
use crate::termstructures::inflation::piecewiseyoyinflationcurve::PiecewiseYoYInflationCurve;
use crate::termstructures::yields::{FlatForward, Pillar};
use crate::time::calendars::unitedkingdom::{self, UnitedKingdom};
use crate::time::date::Month;
use crate::time::daycounters::actual365fixed::Actual365Fixed;
use crate::time::daycounters::actualactual::{
ActualActual, Convention as ActualActualConvention,
};
const NOTIONAL: Real = 2_500_000.0;
const LENGTH: Size = 5;
const QUOTED_FIVE_YEAR_RATE: Real = 0.04;
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)
}
struct Fixture {
settings: Shared<Settings<Date>>,
index: Shared<YoYInflationIndex>,
nominal: Handle<dyn YieldTermStructure>,
evaluation_date: Date,
_curve: Shared<PiecewiseYoYInflationCurve<Linear>>,
_handle: RelinkableHandle<dyn YoYInflationTermStructure>,
}
fn a_sloped_market() -> Fixture {
let settings = shared(Settings::<Date>::new());
let evaluation_date = Date::new(13, Month::August, 2007);
settings.set_evaluation_date(evaluation_date);
let rpi_schedule = MakeSchedule::new()
.from(Date::new(1, Month::January, 2005))
.to(Date::new(1, Month::August, 2007))
.with_tenor(Period::new(1, TimeUnit::Months))
.with_calendar(uk())
.with_convention(BusinessDayConvention::ModifiedFollowing)
.with_rule(DateGeneration::Forward)
.build();
let rpi = shared(UkRpi::new(Shared::clone(&settings)));
for (n, date) in rpi_schedule.dates().iter().enumerate() {
rpi.add_fixing(*date, 190.0 + n as Real)
.expect("a published figure");
}
let handle = RelinkableHandle::<dyn YoYInflationTermStructure>::empty();
let index = shared(
YoYInflationIndex::from_underlying(Shared::clone(&rpi))
.with_term_structure(handle.handle()),
);
let nominal = Handle::new(shared(FlatForward::with_rate(
evaluation_date,
0.05,
ActualActual::with_convention(ActualActualConvention::ISDA),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>);
let helpers: Vec<Shared<dyn YoYInflationHelper>> = (1..=5)
.map(|year| {
let rate = 0.02 + 0.005 * (year - 1) as Real;
YearOnYearInflationSwapHelper::new(
Handle::new(shared(SimpleQuote::new(Some(rate)))),
observation_lag(),
uk().advance(
evaluation_date,
year,
TimeUnit::Years,
BusinessDayConvention::Unadjusted,
false,
),
uk(),
BusinessDayConvention::ModifiedFollowing,
day_counter(),
&index,
CpiInterpolationType::Flat,
nominal.clone(),
Pillar::LastRelevantDate,
Shared::clone(&settings),
)
.expect("a well-formed helper") as Shared<dyn YoYInflationHelper>
})
.collect();
let curve = PiecewiseYoYInflationCurve::<Linear>::new(
evaluation_date,
rpi.last_fixing_date().expect("RPI has history"),
0.02,
index.frequency(),
day_counter(),
helpers,
None,
)
.expect("five helpers");
handle.link_to(Shared::clone(&curve) as Shared<dyn YoYInflationTermStructure>);
Fixture {
settings,
index,
nominal,
evaluation_date,
_curve: curve,
_handle: handle,
}
}
fn a_builder(fixture: &Fixture) -> MakeYoYInflationCapFloor {
MakeYoYInflationCapFloor::new(
CapFloorType::Cap,
Shared::clone(&fixture.index),
LENGTH,
uk(),
observation_lag(),
CpiInterpolationType::Flat,
Shared::clone(&fixture.settings),
)
}
fn a_hand_built_leg(fixture: &Fixture) -> Vec<Shared<YoYInflationCoupon>> {
let start = fixture.evaluation_date;
let end = uk().advance(
start,
LENGTH as Integer,
TimeUnit::Years,
BusinessDayConvention::Unadjusted,
false,
);
let schedule = MakeSchedule::new()
.from(start)
.to(end)
.with_frequency(Frequency::Annual)
.with_calendar(uk())
.with_convention(BusinessDayConvention::Unadjusted)
.with_termination_date_convention(BusinessDayConvention::Unadjusted)
.with_rule(DateGeneration::Forward)
.build();
YoYInflationLeg::new(
schedule,
uk(),
Shared::clone(&fixture.index),
observation_lag(),
CpiInterpolationType::Flat,
)
.with_payment_adjustment(BusinessDayConvention::ModifiedFollowing)
.with_payment_day_counter(day_counter())
.with_notional(NOTIONAL)
.coupons()
.expect("a well-formed leg")
}
fn pay_dates(cap_floor: &YoYInflationCapFloor) -> Vec<Date> {
cap_floor.yoy_leg().iter().map(|c| c.date()).collect()
}
#[test]
fn the_factory_reproduces_a_hand_built_cap() {
let fixture = a_sloped_market();
let built = a_builder(&fixture)
.with_nominal(NOTIONAL)
.with_strike(0.03)
.build()
.expect("an explicit strike is enough to build");
let by_hand = YoYInflationCapFloor::cap(
a_hand_built_leg(&fixture),
vec![0.03],
Shared::clone(&fixture.settings),
)
.expect("a well-formed cap");
assert_eq!(built.cap_rates(), by_hand.cap_rates());
assert_eq!(built.yoy_leg().len(), by_hand.yoy_leg().len());
for (from_factory, by_hand) in built.yoy_leg().iter().zip(by_hand.yoy_leg()) {
assert_eq!(from_factory.nominal(), by_hand.nominal());
assert_eq!(from_factory.accrual_period(), by_hand.accrual_period());
assert_eq!(from_factory.date(), by_hand.date());
assert_eq!(from_factory.fixing_date(), by_hand.fixing_date());
}
}
#[test]
fn atm_rate_is_the_leg_repricing_rate() {
let fixture = a_sloped_market();
let cap = a_builder(&fixture)
.with_strike(0.03)
.build()
.expect("a well-formed cap");
let curve = fixture.nominal.current_link().expect("a linked curve");
let leg: Leg = cap
.yoy_leg()
.iter()
.map(|coupon| Shared::clone(coupon) as Shared<dyn CashFlow>)
.collect();
let by_hand = CashFlows::atm_rate(
&leg,
curve.as_ref(),
&fixture.settings,
Some(false),
Some(curve.reference_date().expect("a reference date")),
None,
None,
)
.expect("a leg with basis-point sensitivity");
assert_eq!(cap.atm_rate(curve.as_ref()).expect("an atm rate"), by_hand);
}
#[test]
fn an_unset_strike_is_filled_at_the_money() {
let fixture = a_sloped_market();
let cap = a_builder(&fixture)
.with_atm_strike(fixture.nominal.clone())
.build()
.expect("an atm curve is enough to build");
let curve = fixture.nominal.current_link().expect("a linked curve");
assert!(
(cap.cap_rates()[0] - cap.atm_rate(curve.as_ref()).expect("an atm rate")).abs() < 1e-12
);
assert!(
(cap.cap_rates()[0] - QUOTED_FIVE_YEAR_RATE).abs() < 1e-12,
"the atm strike {} missed the five-year quote",
cap.cap_rates()[0]
);
}
#[test]
fn an_optionlet_strikes_at_the_money_on_the_coupon_it_keeps() {
let fixture = a_sloped_market();
let whole_leg = a_builder(&fixture)
.with_atm_strike(fixture.nominal.clone())
.build()
.expect("a well-formed cap");
let optionlet = a_builder(&fixture)
.as_optionlet(true)
.with_atm_strike(fixture.nominal.clone())
.build()
.expect("a well-formed optionlet");
let curve = fixture.nominal.current_link().expect("a linked curve");
assert_eq!(whole_leg.yoy_leg().len(), LENGTH);
assert_eq!(optionlet.yoy_leg().len(), 1);
assert_eq!(
optionlet.yoy_leg()[0].date(),
whole_leg.yoy_leg()[LENGTH - 1].date()
);
assert!(
(optionlet.cap_rates()[0] - optionlet.atm_rate(curve.as_ref()).expect("an atm rate"))
.abs()
< 1e-12
);
assert!(
(optionlet.cap_rates()[0] - whole_leg.cap_rates()[0]).abs() > 1e-2,
"the tail rate {} is indistinguishable from the whole leg's {}",
optionlet.cap_rates()[0],
whole_leg.cap_rates()[0]
);
}
#[test]
fn the_trimming_flags_reach_the_leg() {
let fixture = a_sloped_market();
let whole = a_builder(&fixture).with_strike(0.03).build().unwrap();
let trimmed = a_builder(&fixture)
.with_first_caplet_excluded()
.with_strike(0.03)
.build()
.unwrap();
assert_eq!(trimmed.yoy_leg().len(), whole.yoy_leg().len() - 1);
assert_eq!(pay_dates(&trimmed), pay_dates(&whole)[1..]);
}
#[test]
fn every_setter_moves_the_built_leg() {
let fixture = a_sloped_market();
let default = a_builder(&fixture).with_strike(0.03).build().unwrap();
let default_dates = pay_dates(&default);
let shifted = a_builder(&fixture)
.with_effective_date(fixture.evaluation_date + Period::new(1, TimeUnit::Months))
.with_strike(0.03)
.build()
.unwrap();
assert_ne!(pay_dates(&shifted), default_dates);
let unadjusted = a_builder(&fixture)
.with_payment_adjustment(BusinessDayConvention::Unadjusted)
.with_strike(0.03)
.build()
.unwrap();
assert_ne!(pay_dates(&unadjusted), default_dates);
let actual365 = a_builder(&fixture)
.with_payment_day_counter(Actual365Fixed::new())
.with_strike(0.03)
.build()
.unwrap();
assert_ne!(
actual365.yoy_leg()[0].accrual_period(),
default.yoy_leg()[0].accrual_period()
);
let delayed = a_builder(&fixture)
.with_fixing_days(5)
.with_strike(0.03)
.build()
.unwrap();
assert_ne!(pay_dates(&delayed), default_dates);
let forward = a_builder(&fixture)
.with_forward_start(Period::new(1, TimeUnit::Years))
.with_strike(0.03)
.build()
.unwrap();
assert_ne!(pay_dates(&forward), default_dates);
}
#[test]
fn a_strike_and_an_atm_curve_are_mutually_exclusive() {
let fixture = a_sloped_market();
let strike_first = a_builder(&fixture)
.with_strike(0.03)
.with_atm_strike(fixture.nominal.clone())
.build()
.err()
.expect("both given");
assert!(
strike_first.message().contains("both given"),
"err was: {strike_first}"
);
let curve_first = a_builder(&fixture)
.with_atm_strike(fixture.nominal.clone())
.with_strike(0.03)
.build()
.err()
.expect("both given");
assert!(
curve_first.message().contains("both given"),
"err was: {curve_first}"
);
let neither = a_builder(&fixture).build().err().expect("neither given");
assert!(
neither.message().contains("no strike"),
"err was: {neither}"
);
}
}