use std::fmt::Debug;
use ndarray::Array1;
use crate::traits::FloatExt;
pub trait InflationCurve<T: FloatExt>: Debug + Send + Sync {
fn forward_index_ratio(&self, t: T) -> T;
fn breakeven_rate(&self, t: T) -> T {
let ratio = self.forward_index_ratio(t);
if t <= T::epsilon() {
return T::zero();
}
ratio.powf(T::one() / t) - T::one()
}
fn spot_ratio(&self) -> T {
T::one()
}
}
#[derive(Debug, Clone)]
pub struct ZeroCouponInflationCurve<T: FloatExt> {
pub pillars: Array1<T>,
pub breakevens: Array1<T>,
}
impl<T: FloatExt> ZeroCouponInflationCurve<T> {
pub fn new(pillars: Array1<T>, breakevens: Array1<T>) -> Self {
assert_eq!(pillars.len(), breakevens.len());
assert!(!pillars.is_empty(), "need at least one pillar");
for w in pillars.windows(2) {
assert!(w[1] > w[0], "pillars must be strictly increasing");
}
Self {
pillars,
breakevens,
}
}
fn interp_breakeven(&self, t: T) -> T {
let n = self.pillars.len();
if t <= self.pillars[0] {
return self.breakevens[0];
}
if t >= self.pillars[n - 1] {
return self.breakevens[n - 1];
}
for i in 0..n - 1 {
let t0 = self.pillars[i];
let t1 = self.pillars[i + 1];
if t >= t0 && t <= t1 {
let w = (t - t0) / (t1 - t0);
return self.breakevens[i] * (T::one() - w) + self.breakevens[i + 1] * w;
}
}
self.breakevens[n - 1]
}
}
impl<T: FloatExt> InflationCurve<T> for ZeroCouponInflationCurve<T> {
fn forward_index_ratio(&self, t: T) -> T {
if t <= T::epsilon() {
return T::one();
}
let b = self.interp_breakeven(t);
(T::one() + b).powf(t)
}
}
#[derive(Debug, Clone)]
pub struct YoyInflationCurve<T: FloatExt> {
pub end_points: Array1<T>,
pub yoy_breakevens: Array1<T>,
}
impl<T: FloatExt> YoyInflationCurve<T> {
pub fn new(end_points: Array1<T>, yoy_breakevens: Array1<T>) -> Self {
assert_eq!(end_points.len(), yoy_breakevens.len());
assert!(!end_points.is_empty(), "need at least one interval");
for w in end_points.windows(2) {
assert!(w[1] > w[0], "end points must be strictly increasing");
}
assert!(end_points[0] > T::zero(), "first end point must be > 0");
Self {
end_points,
yoy_breakevens,
}
}
}
impl<T: FloatExt> InflationCurve<T> for YoyInflationCurve<T> {
fn forward_index_ratio(&self, t: T) -> T {
let mut ratio = T::one();
let mut prev = T::zero();
for (i, &end) in self.end_points.iter().enumerate() {
if t <= prev {
break;
}
let span = end.min(t) - prev;
if span <= T::zero() {
break;
}
ratio = ratio * (T::one() + self.yoy_breakevens[i]).powf(span);
prev = end;
if t <= end {
return ratio;
}
}
if t > prev {
let last = *self.yoy_breakevens.last().unwrap();
ratio = ratio * (T::one() + last).powf(t - prev);
}
ratio
}
}
#[cfg(test)]
mod tests {
use ndarray::array;
use super::*;
#[test]
fn zc_curve_forward_ratio() {
let c: ZeroCouponInflationCurve<f64> =
ZeroCouponInflationCurve::new(array![1.0, 5.0, 10.0], array![0.025, 0.024, 0.023]);
let ratio_5 = c.forward_index_ratio(5.0);
let expected = (1.0_f64 + 0.024).powf(5.0);
assert!((ratio_5 - expected).abs() < 1e-12);
}
#[test]
fn zc_curve_breakeven_inversion() {
let c: ZeroCouponInflationCurve<f64> =
ZeroCouponInflationCurve::new(array![1.0, 5.0, 10.0], array![0.025, 0.024, 0.023]);
let b = c.breakeven_rate(5.0);
assert!((b - 0.024).abs() < 1e-12);
}
#[test]
fn zc_curve_clamps_below_first_and_above_last() {
let c: ZeroCouponInflationCurve<f64> =
ZeroCouponInflationCurve::new(array![1.0, 5.0], array![0.02, 0.03]);
assert!((c.forward_index_ratio(0.5) - 1.02_f64.powf(0.5)).abs() < 1e-12);
assert!((c.forward_index_ratio(10.0) - 1.03_f64.powf(10.0)).abs() < 1e-12);
}
#[test]
fn zc_curve_t0_is_unity() {
let c: ZeroCouponInflationCurve<f64> =
ZeroCouponInflationCurve::new(array![1.0], array![0.025]);
assert!((c.forward_index_ratio(0.0) - 1.0).abs() < 1e-12);
}
#[test]
fn yoy_curve_compounds_correctly() {
let c: YoyInflationCurve<f64> =
YoyInflationCurve::new(array![1.0, 2.0, 3.0], array![0.02, 0.025, 0.03]);
let ratio_3 = c.forward_index_ratio(3.0);
let expected = 1.02 * 1.025 * 1.03;
assert!((ratio_3 - expected).abs() < 1e-12);
}
#[test]
fn yoy_curve_intermediate_time_partial_compounding() {
let c: YoyInflationCurve<f64> = YoyInflationCurve::new(array![1.0, 2.0], array![0.02, 0.04]);
let ratio_15 = c.forward_index_ratio(1.5);
let expected = 1.02 * 1.04_f64.powf(0.5);
assert!((ratio_15 - expected).abs() < 1e-12);
}
}