use crate::cashflow::{CashFlow, Leg};
use crate::cashflows::{SimpleCashFlow, ZeroInflationCashFlow};
use crate::errors::QlResult;
use crate::indexes::inflationindex::{CpiInterpolationType, InflationIndex, ZeroInflationIndex};
use crate::instrument::{Instrument, InstrumentBase};
use crate::instruments::swap::{Swap, SwapType};
use crate::pricingengine::{Arguments, Results};
use crate::require;
use crate::settings::Settings;
use crate::shared::{Shared, shared};
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::calendar::Calendar;
use crate::time::date::Date;
use crate::time::daycounter::DayCounter;
use crate::time::period::Period;
use crate::types::{Rate, Real};
const BASIS_POINT: Real = 1.0e-4;
pub struct ZeroCouponInflationSwap {
swap: Swap,
swap_type: SwapType,
nominal: Real,
start_date: Date,
maturity_date: Date,
fixed_calendar: Calendar,
fixed_convention: BusinessDayConvention,
fixed_rate: Rate,
inflation_index: Shared<ZeroInflationIndex>,
observation_lag: Period,
observation_interpolation: CpiInterpolationType,
inflation_calendar: Calendar,
inflation_convention: BusinessDayConvention,
day_counter: DayCounter,
inflation_cash_flow: Shared<ZeroInflationCashFlow>,
}
impl ZeroCouponInflationSwap {
#[allow(clippy::too_many_arguments)]
pub fn new(
swap_type: SwapType,
nominal: Real,
start_date: Date,
maturity: Date,
fixed_calendar: Calendar,
fixed_convention: BusinessDayConvention,
day_counter: DayCounter,
fixed_rate: Rate,
inflation_index: Shared<ZeroInflationIndex>,
observation_lag: Period,
observation_interpolation: CpiInterpolationType,
inflation_calendar: Option<Calendar>,
inflation_convention: Option<BusinessDayConvention>,
settings: Shared<Settings<Date>>,
) -> QlResult<ZeroCouponInflationSwap> {
let availability_lag = inflation_index.availability_lag();
match observation_interpolation {
CpiInterpolationType::Linear => {
let publication = Period::try_from(inflation_index.frequency())?;
let covered = observation_lag - publication >= availability_lag;
require!(
covered,
"inconsistency between swap observation lag {observation_lag}, \
interpolated index period {publication} and index availability \
{availability_lag}: need (obsLag-index period) >= availLag"
);
}
CpiInterpolationType::Flat => {
let covered = availability_lag <= observation_lag;
require!(
covered,
"index tries to observe inflation fixings that do not yet exist: \
availability lag {availability_lag} versus obs lag = {observation_lag}"
);
}
}
let inflation_calendar = inflation_calendar.unwrap_or_else(|| fixed_calendar.clone());
let inflation_convention = inflation_convention.unwrap_or(fixed_convention);
let inflation_pay_date = inflation_calendar.adjust(maturity, inflation_convention);
let fixed_pay_date = fixed_calendar.adjust(maturity, fixed_convention);
let inflation_cash_flow = shared(ZeroInflationCashFlow::new(
nominal,
Shared::clone(&inflation_index),
observation_interpolation,
start_date,
maturity,
observation_lag,
inflation_pay_date,
true,
));
let time = day_counter.year_fraction(start_date, maturity);
let fixed_amount = nominal * ((1.0 + fixed_rate).powf(time) - 1.0);
let fixed_cash_flow = shared(SimpleCashFlow::new(fixed_amount, fixed_pay_date)?);
let fixed_leg: Leg = vec![fixed_cash_flow as Shared<dyn CashFlow>];
let inflation_leg: Leg = vec![Shared::clone(&inflation_cash_flow) as Shared<dyn CashFlow>];
let payer = match swap_type {
SwapType::Payer => vec![false, true],
SwapType::Receiver => vec![true, false],
};
let swap = Swap::new(vec![fixed_leg, inflation_leg], payer, settings)?;
Ok(ZeroCouponInflationSwap {
swap,
swap_type,
nominal,
start_date,
maturity_date: maturity,
fixed_calendar,
fixed_convention,
fixed_rate,
inflation_index,
observation_lag,
observation_interpolation,
inflation_calendar,
inflation_convention,
day_counter,
inflation_cash_flow,
})
}
pub fn swap(&self) -> &Swap {
&self.swap
}
pub fn swap_mut(&mut self) -> &mut Swap {
&mut self.swap
}
pub fn swap_type(&self) -> SwapType {
self.swap_type
}
pub fn nominal(&self) -> Real {
self.nominal
}
pub fn start_date(&self) -> Date {
self.start_date
}
pub fn maturity_date(&self) -> Date {
self.maturity_date
}
pub fn fixed_calendar(&self) -> &Calendar {
&self.fixed_calendar
}
pub fn fixed_convention(&self) -> BusinessDayConvention {
self.fixed_convention
}
pub fn day_counter(&self) -> &DayCounter {
&self.day_counter
}
pub fn fixed_rate(&self) -> Rate {
self.fixed_rate
}
pub fn inflation_index(&self) -> &Shared<ZeroInflationIndex> {
&self.inflation_index
}
pub fn observation_lag(&self) -> Period {
self.observation_lag
}
pub fn observation_interpolation(&self) -> CpiInterpolationType {
self.observation_interpolation
}
pub fn inflation_calendar(&self) -> &Calendar {
&self.inflation_calendar
}
pub fn inflation_convention(&self) -> BusinessDayConvention {
self.inflation_convention
}
pub fn base_date(&self) -> Date {
self.inflation_cash_flow.base_date()
}
pub fn obs_date(&self) -> Date {
self.inflation_cash_flow.fixing_date()
}
pub fn fixed_leg(&self) -> &Leg {
&self.swap.legs()[0]
}
pub fn inflation_leg(&self) -> &Leg {
&self.swap.legs()[1]
}
pub fn inflation_cash_flow(&self) -> &Shared<ZeroInflationCashFlow> {
&self.inflation_cash_flow
}
pub fn fixed_leg_npv(&mut self) -> QlResult<Real> {
self.swap.leg_npv(0)
}
pub fn inflation_leg_npv(&mut self) -> QlResult<Real> {
self.swap.leg_npv(1)
}
pub fn fair_rate(&self) -> QlResult<Rate> {
let growth = self.inflation_cash_flow.amount()? / self.inflation_cash_flow.notional() + 1.0;
let time = self
.day_counter
.year_fraction(self.start_date, self.maturity_date);
Ok(growth.powf(1.0 / time) - 1.0)
}
pub fn fixed_leg_bps(&mut self) -> QlResult<Real> {
let discount = self.swap.end_discounts(0)?;
let sign = if self.swap.payer(0)? { -1.0 } else { 1.0 };
let time = self
.day_counter
.year_fraction(self.start_date, self.maturity_date);
let shifted = (1.0 + self.fixed_rate + BASIS_POINT).powf(time);
Ok(sign * discount * self.nominal * (shifted - (1.0 + self.fixed_rate).powf(time)))
}
}
impl Instrument for ZeroCouponInflationSwap {
fn base(&self) -> &InstrumentBase {
self.swap.base()
}
fn base_mut(&mut self) -> &mut InstrumentBase {
self.swap.base_mut()
}
fn is_expired(&self) -> QlResult<bool> {
self.swap.is_expired()
}
fn setup_expired(&mut self) {
self.swap.setup_expired();
}
fn setup_arguments(&self, arguments: &mut dyn Arguments) -> QlResult<()> {
self.swap.setup_arguments(arguments)
}
fn fetch_results(&mut self, results: &dyn Results) -> QlResult<()> {
self.swap.fetch_results(results)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::handle::Handle;
use crate::indexes::Index;
use crate::indexes::inflation::UkRpi;
use crate::interestrate::Compounding;
use crate::math::interpolations::linear::Linear;
use crate::pricingengine::PricingEngine;
use crate::pricingengines::DiscountingSwapEngine;
use crate::shared::{SharedMut, shared_mut};
use crate::termstructures::inflation::inflationtermstructure::ZeroInflationTermStructure;
use crate::termstructures::inflation::interpolatedzeroinflationcurve::ZeroInflationCurve;
use crate::termstructures::yields::FlatForward;
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::time::calendars::unitedkingdom::{Market, UnitedKingdom};
use crate::time::date::Month::{July, June, September};
use crate::time::daycounters::actual360::Actual360;
use crate::time::daycounters::actual365fixed::Actual365Fixed;
use crate::time::frequency::Frequency;
use crate::time::timeunit::TimeUnit;
const NOMINAL: Real = 1_000_000.0;
const FIXED_RATE: Rate = 0.025;
const NOMINAL_RATE: Rate = 0.05;
const JUNE_FIXING: Real = 196.0;
const JULY_FIXING: Real = 200.0;
const NODE_RATE: Rate = 0.03;
fn today() -> Date {
Date::new(1, September, 2026)
}
fn maturity() -> Date {
Date::new(1, September, 2031)
}
fn weekend_maturity() -> Date {
Date::new(6, September, 2031)
}
fn curve_base_date() -> Date {
Date::new(1, July, 2026)
}
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 an_index(settings: &Shared<Settings<Date>>) -> Shared<ZeroInflationIndex> {
let curve = shared(
ZeroInflationCurve::new(
today(),
vec![
curve_base_date(),
Date::new(1, June, 2031),
Date::new(1, July, 2036),
],
vec![0.02, NODE_RATE, 0.04],
Frequency::Monthly,
Actual360::new(),
Linear,
None,
)
.expect("a well-formed zero inflation curve"),
);
let index = shared(
UkRpi::new(Shared::clone(settings))
.with_term_structure(Handle::new(curve as Shared<dyn ZeroInflationTermStructure>)),
);
index
.add_fixing(Date::new(1, June, 2026), JUNE_FIXING)
.expect("a published figure");
index
.add_fixing(curve_base_date(), JULY_FIXING)
.expect("a published figure");
index
}
fn a_discount_engine(settings: Shared<Settings<Date>>) -> SharedMut<dyn PricingEngine> {
let curve = shared(FlatForward::with_rate(
today(),
NOMINAL_RATE,
Actual365Fixed::new(),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>;
shared_mut(DiscountingSwapEngine::new(
Handle::new(curve),
None,
None,
None,
settings,
)) as SharedMut<dyn PricingEngine>
}
fn a_swap(swap_type: SwapType, maturity: Date, fixed_rate: Rate) -> ZeroCouponInflationSwap {
let settings = settings_today();
let index = an_index(&settings);
let mut swap = ZeroCouponInflationSwap::new(
swap_type,
NOMINAL,
today(),
maturity,
UnitedKingdom::new(Market::Settlement),
BusinessDayConvention::ModifiedFollowing,
Actual365Fixed::new(),
fixed_rate,
index,
lag(),
CpiInterpolationType::Flat,
None,
None,
Shared::clone(&settings),
)
.expect("a three-month lag covers UK RPI's one-month availability");
swap.base_mut()
.set_pricing_engine(a_discount_engine(settings));
swap
}
#[test]
fn the_two_legs_pay_the_hand_derived_amounts() {
let swap = a_swap(SwapType::Payer, maturity(), FIXED_RATE);
let flow = swap.inflation_cash_flow();
assert!((flow.base_fixing().unwrap() - JUNE_FIXING).abs() < 1e-10);
assert!((flow.index_fixing().unwrap() - 231.7786790231386).abs() < 1e-10);
assert!((flow.amount().unwrap() - 182544.28073029904).abs() < 1e-6);
assert!((swap.fixed_leg()[0].amount().unwrap() - 131484.7563692576).abs() < 1e-6);
}
#[test]
fn a_payer_swap_prices_to_the_hand_derived_npv() {
let mut swap = a_swap(SwapType::Payer, maturity(), FIXED_RATE);
assert!((swap.fixed_leg_npv().unwrap() - 102386.4047267397).abs() < 1e-6);
assert!((swap.inflation_leg_npv().unwrap() + 142146.15536812067).abs() < 1e-6);
assert!((swap.npv().unwrap() + 39759.750641380975).abs() < 1e-6);
}
#[test]
fn the_fair_rate_de_compounds_the_index_ratio() {
let swap = a_swap(SwapType::Payer, maturity(), FIXED_RATE);
let fair = swap.fair_rate().unwrap();
assert!((fair - 0.03408325777213217).abs() < 1e-12);
let mut struck_at_fair = a_swap(SwapType::Payer, maturity(), fair);
assert!(struck_at_fair.npv().unwrap().abs() < 1e-6);
}
#[test]
fn the_fixed_leg_bps_is_analytic_not_the_engines_zero() {
let mut swap = a_swap(SwapType::Payer, maturity(), FIXED_RATE);
assert_eq!(swap.swap_mut().leg_bps(0).unwrap(), 0.0);
assert!((swap.fixed_leg_bps().unwrap() - 430.1148492112715).abs() < 1e-8);
}
#[test]
fn a_receiver_swap_negates_the_payer_npv() {
let mut payer = a_swap(SwapType::Payer, maturity(), FIXED_RATE);
let mut receiver = a_swap(SwapType::Receiver, maturity(), FIXED_RATE);
assert!(!payer.swap().payer(0).unwrap(), "a payer receives fixed");
assert!(payer.swap().payer(1).unwrap(), "a payer pays inflation");
assert!(receiver.swap().payer(0).unwrap());
assert!(!receiver.swap().payer(1).unwrap());
assert!((receiver.npv().unwrap() + payer.npv().unwrap()).abs() < 1e-6);
}
#[test]
fn an_adjusted_payment_date_leaves_the_year_fraction_raw() {
let mut swap = a_swap(SwapType::Payer, weekend_maturity(), FIXED_RATE);
assert_eq!(swap.maturity_date(), weekend_maturity());
assert_eq!(
swap.fixed_leg()[0].date(),
Date::new(8, September, 2031),
"the payment date is adjusted"
);
assert_eq!(
swap.inflation_leg()[0].date(),
Date::new(8, September, 2031)
);
assert!((swap.fixed_leg()[0].amount().unwrap() - 131867.55144569263).abs() < 1e-6);
assert!((swap.inflation_cash_flow().amount().unwrap() - 182544.28073029904).abs() < 1e-6);
assert!((swap.npv().unwrap() + 39423.84855056528).abs() < 1e-6);
assert!((swap.fair_rate().unwrap() - 0.033988620914166656).abs() < 1e-12);
assert!((swap.fixed_leg_bps().unwrap() - 431.02529041491573).abs() < 1e-8);
}
#[test]
fn the_raw_dates_override_the_bases_span_of_the_legs() {
let swap = a_swap(SwapType::Payer, weekend_maturity(), FIXED_RATE);
let adjusted = Date::new(8, September, 2031);
assert_eq!(swap.start_date(), today());
assert_eq!(swap.maturity_date(), weekend_maturity());
assert_eq!(swap.swap().start_date().unwrap(), adjusted);
assert_eq!(swap.swap().maturity_date().unwrap(), adjusted);
}
#[test]
fn the_observation_dates_come_from_the_indexed_flow() {
let swap = a_swap(SwapType::Payer, maturity(), FIXED_RATE);
assert_eq!(swap.base_date(), Date::new(1, June, 2026));
assert_eq!(swap.obs_date(), Date::new(1, June, 2031));
assert_eq!(swap.base_date(), swap.inflation_cash_flow().base_date());
assert_eq!(swap.obs_date(), swap.inflation_cash_flow().fixing_date());
assert_eq!(swap.swap_type(), SwapType::Payer);
assert_eq!(swap.nominal(), NOMINAL);
assert_eq!(swap.fixed_rate(), FIXED_RATE);
assert_eq!(swap.observation_lag(), lag());
assert_eq!(swap.observation_interpolation(), CpiInterpolationType::Flat);
assert_eq!(swap.inflation_index().name(), "UK RPI");
assert!(swap.inflation_cash_flow().growth_only());
}
#[test]
fn the_inflation_calendar_defaults_to_the_fixed_one() {
let swap = a_swap(SwapType::Payer, maturity(), FIXED_RATE);
assert_eq!(
swap.inflation_calendar().name(),
swap.fixed_calendar().name()
);
assert_eq!(swap.inflation_convention(), swap.fixed_convention());
assert_eq!(
swap.fixed_convention(),
BusinessDayConvention::ModifiedFollowing
);
}
#[test]
fn a_lag_the_index_cannot_observe_through_is_rejected() {
let settings = settings_today();
let build = |lag: Period, interpolation: CpiInterpolationType| {
ZeroCouponInflationSwap::new(
SwapType::Payer,
NOMINAL,
today(),
maturity(),
UnitedKingdom::new(Market::Settlement),
BusinessDayConvention::ModifiedFollowing,
Actual365Fixed::new(),
FIXED_RATE,
an_index(&settings),
lag,
interpolation,
None,
None,
Shared::clone(&settings),
)
.map(|_| ())
};
let month = Period::new(1, TimeUnit::Months);
assert!(build(month, CpiInterpolationType::Flat).is_ok());
assert!(
build(Period::new(0, TimeUnit::Months), CpiInterpolationType::Flat)
.unwrap_err()
.message()
.contains("fixings that do not yet exist")
);
assert!(
build(month, CpiInterpolationType::Linear)
.unwrap_err()
.message()
.contains("inconsistency between swap observation lag")
);
assert!(build(lag(), CpiInterpolationType::Linear).is_ok());
}
}