use std::any::Any;
use crate::cashflow::{CashFlow, Leg};
use crate::cashflows::{FixedRateLeg, YoYInflationCoupon, YoYInflationLeg};
use crate::errors::QlResult;
use crate::fail;
use crate::indexes::inflationindex::{CpiInterpolationType, YoYInflationIndex};
use crate::instrument::{Instrument, InstrumentBase};
use crate::instruments::swap::{Swap, SwapResults, SwapType};
use crate::interestrate::Compounding;
use crate::pricingengine::{Arguments, Results};
use crate::settings::Settings;
use crate::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::frequency::Frequency;
use crate::time::period::Period;
use crate::time::schedule::Schedule;
use crate::types::{Rate, Real, Spread};
use crate::utilities::null::Null;
const BASIS_POINT: Real = 1.0e-4;
pub struct YearOnYearInflationSwap {
swap: Swap,
swap_type: SwapType,
nominal: Real,
fixed_schedule: Schedule,
fixed_rate: Rate,
fixed_day_count: DayCounter,
yoy_schedule: Schedule,
yoy_index: Shared<YoYInflationIndex>,
yoy_coupons: Vec<Shared<YoYInflationCoupon>>,
observation_lag: Period,
interpolation: CpiInterpolationType,
spread: Spread,
yoy_day_count: DayCounter,
payment_calendar: Calendar,
payment_convention: BusinessDayConvention,
fair_rate: Option<Rate>,
fair_spread: Option<Spread>,
}
impl YearOnYearInflationSwap {
#[allow(clippy::too_many_arguments)]
pub fn new(
swap_type: SwapType,
nominal: Real,
fixed_schedule: Schedule,
fixed_rate: Rate,
fixed_day_count: DayCounter,
yoy_schedule: Schedule,
yoy_index: Shared<YoYInflationIndex>,
observation_lag: Period,
interpolation: CpiInterpolationType,
spread: Spread,
yoy_day_count: DayCounter,
payment_calendar: Calendar,
payment_convention: BusinessDayConvention,
settings: Shared<Settings<Date>>,
) -> QlResult<YearOnYearInflationSwap> {
let fixed_leg = FixedRateLeg::new(fixed_schedule.clone())
.with_notional(nominal)
.with_coupon_rate(
fixed_rate,
fixed_day_count.clone(),
Compounding::Simple,
Frequency::Annual,
)?
.with_payment_adjustment(payment_convention)
.build()?;
let yoy_coupons = YoYInflationLeg::new(
yoy_schedule.clone(),
payment_calendar.clone(),
Shared::clone(&yoy_index),
observation_lag,
interpolation,
)
.with_notional(nominal)
.with_payment_day_counter(yoy_day_count.clone())
.with_payment_adjustment(payment_convention)
.with_spread(spread)
.coupons()?;
let yoy_leg: Leg = yoy_coupons
.iter()
.map(|coupon| Shared::clone(coupon) as Shared<dyn CashFlow>)
.collect();
let payer = match swap_type {
SwapType::Payer => vec![true, false],
SwapType::Receiver => vec![false, true],
};
let swap = Swap::new(vec![fixed_leg, yoy_leg], payer, settings)?;
Ok(YearOnYearInflationSwap {
swap,
swap_type,
nominal,
fixed_schedule,
fixed_rate,
fixed_day_count,
yoy_schedule,
yoy_index,
yoy_coupons,
observation_lag,
interpolation,
spread,
yoy_day_count,
payment_calendar,
payment_convention,
fair_rate: None,
fair_spread: None,
})
}
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 fixed_schedule(&self) -> &Schedule {
&self.fixed_schedule
}
pub fn fixed_rate(&self) -> Rate {
self.fixed_rate
}
pub fn fixed_day_count(&self) -> &DayCounter {
&self.fixed_day_count
}
pub fn yoy_schedule(&self) -> &Schedule {
&self.yoy_schedule
}
pub fn yoy_inflation_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 spread(&self) -> Spread {
self.spread
}
pub fn yoy_day_count(&self) -> &DayCounter {
&self.yoy_day_count
}
pub fn payment_calendar(&self) -> &Calendar {
&self.payment_calendar
}
pub fn payment_convention(&self) -> BusinessDayConvention {
self.payment_convention
}
pub fn fixed_leg(&self) -> &Leg {
&self.swap.legs()[0]
}
pub fn yoy_leg(&self) -> &Leg {
&self.swap.legs()[1]
}
pub fn yoy_coupons(&self) -> &[Shared<YoYInflationCoupon>] {
&self.yoy_coupons
}
pub fn fixed_leg_npv(&mut self) -> QlResult<Real> {
self.calculate()?;
self.swap.leg_npv(0)
}
pub fn yoy_leg_npv(&mut self) -> QlResult<Real> {
self.calculate()?;
self.swap.leg_npv(1)
}
pub fn fair_rate(&mut self) -> QlResult<Rate> {
self.calculate()?;
let Some(value) = self.fair_rate else {
fail!("result not available");
};
Ok(value)
}
pub fn fair_spread(&mut self) -> QlResult<Spread> {
self.calculate()?;
let Some(value) = self.fair_spread else {
fail!("result not available");
};
Ok(value)
}
}
impl Instrument for YearOnYearInflationSwap {
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();
self.fair_rate = None;
self.fair_spread = None;
}
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)?;
let Some(results) = (results as &dyn Any).downcast_ref::<SwapResults>() else {
fail!("wrong result type");
};
let npv = results.instrument.value;
self.fair_rate = recover(npv, results.leg_bps.first(), self.fixed_rate);
self.fair_spread = recover(npv, results.leg_bps.get(1), self.spread);
Ok(())
}
}
fn recover(npv: Option<Real>, leg_bps: Option<&Real>, quoted: Real) -> Option<Real> {
let (Some(npv), Some(&bps)) = (npv, leg_bps) else {
return None;
};
(!bps.is_null()).then(|| quoted - npv / (bps / BASIS_POINT))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::currency::Currency;
use crate::handle::Handle;
use crate::indexes::Region;
use crate::indexes::index::Index;
use crate::math::interpolations::linear::Linear;
use crate::pricingengine::PricingEngine;
use crate::pricingengines::DiscountingSwapEngine;
use crate::shared::{SharedMut, shared, shared_mut};
use crate::termstructures::inflation::inflationtermstructure::YoYInflationTermStructure;
use crate::termstructures::inflation::interpolatedyoyinflationcurve::YoYInflationCurve;
use crate::termstructures::yields::FlatForward;
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::time::calendars::unitedkingdom::{self, UnitedKingdom};
use crate::time::date::Month::{August, July};
use crate::time::daycounters::actual360::Actual360;
use crate::time::daycounters::thirty360::{Convention, Thirty360};
use crate::time::schedule::MakeSchedule;
use crate::time::timeunit::TimeUnit;
const NOMINAL: Real = 1_000_000.0;
const CURVE_RATE: Rate = 0.03;
const OFF_MARKET_RATE: Rate = 0.04;
const NOMINAL_RATE: Rate = 0.05;
fn today() -> Date {
Date::new(13, August, 2007)
}
fn maturity() -> Date {
Date::new(13, August, 2012)
}
fn uk() -> Calendar {
UnitedKingdom::new(unitedkingdom::Market::Settlement)
}
fn day_counter() -> DayCounter {
Thirty360::with_convention(Convention::BondBasis)
}
fn lag() -> Period {
Period::new(2, 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<YoYInflationIndex> {
let curve = shared(
YoYInflationCurve::new(
today(),
vec![Date::new(1, July, 2007), Date::new(1, July, 2015)],
vec![CURVE_RATE, CURVE_RATE],
Frequency::Monthly,
Actual360::new(),
Linear,
None,
)
.expect("a well-formed year-on-year curve"),
);
shared(
YoYInflationIndex::new(
"YY_RPI".into(),
Region::uk(),
false,
Frequency::Monthly,
Period::new(1, TimeUnit::Months),
Currency::gbp(),
Shared::clone(settings),
)
.with_term_structure(Handle::new(curve as Shared<dyn YoYInflationTermStructure>)),
)
}
fn a_discount_engine(settings: Shared<Settings<Date>>) -> SharedMut<dyn PricingEngine> {
let curve = shared(FlatForward::with_rate(
today(),
NOMINAL_RATE,
Actual360::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, fixed_rate: Rate) -> YearOnYearInflationSwap {
let settings = settings_today();
let schedule = MakeSchedule::new()
.from(today())
.to(maturity())
.with_tenor(Period::new(1, TimeUnit::Years))
.with_convention(BusinessDayConvention::Unadjusted)
.with_calendar(uk())
.backwards()
.build();
let mut swap = YearOnYearInflationSwap::new(
swap_type,
NOMINAL,
schedule.clone(),
fixed_rate,
day_counter(),
schedule,
an_index(&settings),
lag(),
CpiInterpolationType::Flat,
0.0,
day_counter(),
uk(),
BusinessDayConvention::ModifiedFollowing,
Shared::clone(&settings),
)
.expect("both legs are fully specified");
swap.base_mut()
.set_pricing_engine(a_discount_engine(settings));
swap
}
#[test]
fn the_two_legs_run_over_the_same_five_periods() {
let swap = a_swap(SwapType::Payer, CURVE_RATE);
assert_eq!(swap.swap().number_of_legs(), 2);
assert_eq!(swap.fixed_leg().len(), 5);
assert_eq!(swap.yoy_leg().len(), 5);
assert_eq!(swap.yoy_coupons().len(), 5);
for (fixed, yoy) in swap.fixed_leg().iter().zip(swap.yoy_leg()) {
assert_eq!(fixed.date(), yoy.date());
}
for coupon in swap.yoy_coupons() {
assert!((coupon.index_fixing().unwrap() - CURVE_RATE).abs() < 1e-14);
}
}
#[test]
fn an_at_market_swap_is_worth_nothing_and_is_fair_at_the_curve_rate() {
let mut swap = a_swap(SwapType::Payer, CURVE_RATE);
assert!(swap.npv().unwrap().abs() < 1e-8);
assert!((swap.fair_rate().unwrap() - CURVE_RATE).abs() < 1e-8);
assert!((swap.fair_spread().unwrap() - 0.0).abs() < 1e-8);
}
#[test]
fn an_off_market_payer_pays_the_fixed_leg() {
let mut swap = a_swap(SwapType::Payer, OFF_MARKET_RATE);
let fixed = swap.fixed_leg_npv().unwrap();
let yoy = swap.yoy_leg_npv().unwrap();
assert!(fixed < 0.0, "a payer pays the fixed leg: {fixed}");
assert!(yoy > 0.0, "a payer receives the year-on-year leg: {yoy}");
assert!(swap.npv().unwrap() < 0.0);
assert!(swap.swap().payer(0).unwrap());
assert!(!swap.swap().payer(1).unwrap());
assert!((swap.fair_rate().unwrap() - CURVE_RATE).abs() < 1e-8);
}
#[test]
fn a_receiver_swap_negates_the_payer_npv_at_the_same_fair_rate() {
let mut payer = a_swap(SwapType::Payer, OFF_MARKET_RATE);
let mut receiver = a_swap(SwapType::Receiver, OFF_MARKET_RATE);
assert!((receiver.npv().unwrap() + payer.npv().unwrap()).abs() < 1e-8);
assert!(receiver.npv().unwrap() > 0.0);
assert!((receiver.fair_rate().unwrap() - payer.fair_rate().unwrap()).abs() < 1e-12);
}
#[test]
fn the_inspectors_report_the_contract_terms() {
let swap = a_swap(SwapType::Payer, OFF_MARKET_RATE);
assert_eq!(swap.swap_type(), SwapType::Payer);
assert_eq!(swap.nominal(), NOMINAL);
assert_eq!(swap.fixed_rate(), OFF_MARKET_RATE);
assert_eq!(swap.observation_lag(), lag());
assert_eq!(swap.interpolation(), CpiInterpolationType::Flat);
assert_eq!(swap.spread(), 0.0);
assert_eq!(swap.payment_calendar().name(), uk().name());
assert_eq!(
swap.payment_convention(),
BusinessDayConvention::ModifiedFollowing
);
assert_eq!(swap.fixed_schedule().dates(), swap.yoy_schedule().dates());
assert_eq!(swap.fixed_day_count().name(), day_counter().name());
assert_eq!(swap.yoy_day_count().name(), day_counter().name());
assert_eq!(swap.yoy_inflation_index().name(), "UK YY_RPI");
}
#[test]
fn the_fair_values_need_a_priced_swap() {
let settings = settings_today();
let schedule = MakeSchedule::new()
.from(today())
.to(maturity())
.with_tenor(Period::new(1, TimeUnit::Years))
.with_convention(BusinessDayConvention::Unadjusted)
.with_calendar(uk())
.backwards()
.build();
let mut swap = YearOnYearInflationSwap::new(
SwapType::Payer,
NOMINAL,
schedule.clone(),
CURVE_RATE,
day_counter(),
schedule,
an_index(&settings),
lag(),
CpiInterpolationType::Flat,
0.0,
day_counter(),
uk(),
BusinessDayConvention::ModifiedFollowing,
settings,
)
.expect("both legs are fully specified");
assert!(swap.fair_rate().is_err());
assert!(swap.fair_spread().is_err());
}
}