use ndarray::Array1;
use super::InflationCurve;
use crate::traits::FloatExt;
#[derive(Debug, Clone)]
pub struct ZeroCouponInflationSwap<T: FloatExt> {
pub notional: T,
pub fixed_rate: T,
pub maturity: T,
}
impl<T: FloatExt> ZeroCouponInflationSwap<T> {
pub fn npv(&self, curve: &(impl InflationCurve<T> + ?Sized), nominal_df: T) -> T {
let inflation_leg = self.notional * (curve.forward_index_ratio(self.maturity) - T::one());
let fixed_leg = self.notional * ((T::one() + self.fixed_rate).powf(self.maturity) - T::one());
nominal_df * (inflation_leg - fixed_leg)
}
pub fn fair_fixed_rate(&self, curve: &(impl InflationCurve<T> + ?Sized)) -> T {
if self.maturity <= T::epsilon() {
return T::zero();
}
curve
.forward_index_ratio(self.maturity)
.powf(T::one() / self.maturity)
- T::one()
}
}
#[derive(Debug, Clone)]
pub struct YearOnYearInflationSwap<T: FloatExt> {
pub notional: T,
pub fixed_rate: T,
pub payment_times: Array1<T>,
pub accrual_factors: Array1<T>,
pub nominal_discount_factors: Array1<T>,
}
impl<T: FloatExt> YearOnYearInflationSwap<T> {
pub fn new(
notional: T,
fixed_rate: T,
payment_times: Array1<T>,
accrual_factors: Array1<T>,
nominal_discount_factors: Array1<T>,
) -> Self {
assert_eq!(payment_times.len(), accrual_factors.len());
assert_eq!(payment_times.len(), nominal_discount_factors.len());
Self {
notional,
fixed_rate,
payment_times,
accrual_factors,
nominal_discount_factors,
}
}
pub fn inflation_leg_pv(&self, curve: &(impl InflationCurve<T> + ?Sized)) -> T {
let mut pv = T::zero();
let mut prev_t = T::zero();
for i in 0..self.payment_times.len() {
let t_i = self.payment_times[i];
let r_prev = if prev_t <= T::epsilon() {
T::one()
} else {
curve.forward_index_ratio(prev_t)
};
let r_curr = curve.forward_index_ratio(t_i);
let yoy = r_curr / r_prev - T::one();
pv += self.notional * yoy * self.accrual_factors[i] * self.nominal_discount_factors[i];
prev_t = t_i;
}
pv
}
pub fn fixed_leg_pv(&self) -> T {
let mut pv = T::zero();
for i in 0..self.payment_times.len() {
pv += self.notional
* self.fixed_rate
* self.accrual_factors[i]
* self.nominal_discount_factors[i];
}
pv
}
pub fn npv(&self, curve: &(impl InflationCurve<T> + ?Sized)) -> T {
self.inflation_leg_pv(curve) - self.fixed_leg_pv()
}
pub fn fair_fixed_rate(&self, curve: &(impl InflationCurve<T> + ?Sized)) -> T {
let inf_pv = self.inflation_leg_pv(curve);
let mut weight = T::zero();
for i in 0..self.payment_times.len() {
weight += self.notional * self.accrual_factors[i] * self.nominal_discount_factors[i];
}
if weight.abs() < T::epsilon() {
return T::zero();
}
inf_pv / weight
}
}
#[cfg(test)]
mod tests {
use ndarray::array;
use super::*;
use crate::inflation::ZeroCouponInflationCurve;
#[test]
fn zciis_par_makes_npv_zero() {
let curve: ZeroCouponInflationCurve<f64> =
ZeroCouponInflationCurve::new(array![1.0, 5.0, 10.0], array![0.025, 0.024, 0.023]);
let s = ZeroCouponInflationSwap::<f64> {
notional: 1_000_000.0,
fixed_rate: 0.0,
maturity: 5.0,
};
let par = s.fair_fixed_rate(&curve);
let s_par = ZeroCouponInflationSwap::<f64> {
notional: 1_000_000.0,
fixed_rate: par,
maturity: 5.0,
};
let npv = s_par.npv(&curve, (-0.04_f64 * 5.0).exp());
assert!(npv.abs() < 1e-7, "npv at par={npv}");
}
#[test]
fn zciis_value_increases_when_curve_steepens() {
let curve_lo: ZeroCouponInflationCurve<f64> =
ZeroCouponInflationCurve::new(array![5.0], array![0.02]);
let curve_hi: ZeroCouponInflationCurve<f64> =
ZeroCouponInflationCurve::new(array![5.0], array![0.04]);
let s = ZeroCouponInflationSwap::<f64> {
notional: 1_000_000.0,
fixed_rate: 0.025,
maturity: 5.0,
};
let df = (-0.04_f64 * 5.0).exp();
let lo = s.npv(&curve_lo, df);
let hi = s.npv(&curve_hi, df);
assert!(hi > lo, "lo={lo}, hi={hi}");
}
#[test]
fn yyiis_telescopes_to_zc_under_constant_breakeven() {
let breakeven = 0.025_f64;
let curve: ZeroCouponInflationCurve<f64> =
ZeroCouponInflationCurve::new(array![1.0, 5.0], array![breakeven, breakeven]);
let nominal_r = 0.04_f64;
let n = 5;
let payment_times = Array1::from_iter((1..=n).map(|i| i as f64));
let accrual_factors = Array1::from_elem(n, 1.0_f64);
let dfs = Array1::from_iter((1..=n).map(|i| (-nominal_r * i as f64).exp()));
let s = YearOnYearInflationSwap::<f64>::new(
1_000_000.0,
breakeven,
payment_times,
accrual_factors,
dfs,
);
let npv = s.npv(&curve);
assert!(npv.abs() < 1e-7, "npv={npv}");
}
#[test]
fn yyiis_par_rate_zero_npv() {
let curve: ZeroCouponInflationCurve<f64> =
ZeroCouponInflationCurve::new(array![1.0, 3.0, 5.0], array![0.02, 0.025, 0.03]);
let nominal_r = 0.035_f64;
let n = 5;
let payment_times = Array1::from_iter((1..=n).map(|i| i as f64));
let accrual_factors = Array1::from_elem(n, 1.0);
let dfs = Array1::from_iter((1..=n).map(|i| (-nominal_r * i as f64).exp()));
let s = YearOnYearInflationSwap::<f64>::new(
100.0,
0.0,
payment_times.clone(),
accrual_factors.clone(),
dfs.clone(),
);
let par = s.fair_fixed_rate(&curve);
let s_par =
YearOnYearInflationSwap::<f64>::new(100.0, par, payment_times, accrual_factors, dfs);
assert!(s_par.npv(&curve).abs() < 1e-9);
}
}