use ndarray::Array1;
use super::discount_curve::DiscountCurve;
use crate::traits::FloatExt;
#[derive(Debug, Clone)]
pub struct MultiCurve<T: FloatExt> {
pub discount: DiscountCurve<T>,
pub forecasts: Vec<(String, DiscountCurve<T>)>,
}
impl<T: FloatExt> MultiCurve<T> {
pub fn new(discount: DiscountCurve<T>) -> Self {
Self {
discount,
forecasts: Vec::new(),
}
}
pub fn add_forecast(&mut self, tenor: impl Into<String>, curve: DiscountCurve<T>) {
self.forecasts.push((tenor.into(), curve));
}
pub fn forecast(&self, tenor: &str) -> Option<&DiscountCurve<T>> {
self
.forecasts
.iter()
.find(|(t, _)| t == tenor)
.map(|(_, c)| c)
}
pub fn projected_forward(&self, tenor: &str, t1: T, t2: T) -> Option<T> {
self
.forecast(tenor)
.map(|fc| fc.simple_forward_rate(t1, t2))
}
pub fn basis_spread(&self, tenor: &str, t1: T, t2: T) -> Option<T> {
let fwd_forecast = self.projected_forward(tenor, t1, t2)?;
let fwd_ois = self.discount.simple_forward_rate(t1, t2);
Some(fwd_forecast - fwd_ois)
}
pub fn pv_floating_leg(&self, tenor: &str, schedule: &Array1<T>, notional: T) -> Option<T> {
let fc = self.forecast(tenor)?;
let n = schedule.len();
if n < 2 {
return Some(T::zero());
}
let mut pv = T::zero();
for i in 1..n {
let t0 = schedule[i - 1];
let t1 = schedule[i];
let delta = t1 - t0;
let fwd = fc.simple_forward_rate(t0, t1);
let df = self.discount.discount_factor(t1);
pv += df * delta * fwd * notional;
}
Some(pv)
}
pub fn pv_fixed_leg(&self, schedule: &Array1<T>, fixed_rate: T, notional: T) -> T {
let n = schedule.len();
if n < 2 {
return T::zero();
}
let mut pv = T::zero();
for i in 1..n {
let t0 = schedule[i - 1];
let t1 = schedule[i];
let delta = t1 - t0;
let df = self.discount.discount_factor(t1);
pv += df * delta * fixed_rate * notional;
}
pv
}
pub fn fair_swap_rate(&self, tenor: &str, schedule: &Array1<T>) -> Option<T> {
let fc = self.forecast(tenor)?;
let n = schedule.len();
if n < 2 {
return Some(T::zero());
}
let mut float_leg = T::zero();
let mut annuity = T::zero();
for i in 1..n {
let t0 = schedule[i - 1];
let t1 = schedule[i];
let delta = t1 - t0;
let df = self.discount.discount_factor(t1);
let fwd = fc.simple_forward_rate(t0, t1);
float_leg += df * delta * fwd;
annuity += df * delta;
}
Some(float_leg / annuity)
}
}
#[cfg(test)]
mod tests {
use super::super::types::CurvePoint;
use super::super::types::InterpolationMethod;
use super::*;
fn flat(rate: f64) -> DiscountCurve<f64> {
DiscountCurve::new(
vec![
CurvePoint {
time: 0.5,
discount_factor: (-rate * 0.5).exp(),
},
CurvePoint {
time: 1.0,
discount_factor: (-rate * 1.0).exp(),
},
CurvePoint {
time: 2.0,
discount_factor: (-rate * 2.0).exp(),
},
],
InterpolationMethod::LogLinearOnDiscountFactors,
)
}
#[test]
fn multi_curve_stores_forecasts() {
let mut mc = MultiCurve::new(flat(0.04));
mc.add_forecast("3M", flat(0.045));
assert!(mc.forecast("3M").is_some());
assert!(mc.forecast("6M").is_none());
}
#[test]
fn basis_spread_zero_for_identical_curves() {
let mut mc = MultiCurve::new(flat(0.04));
mc.add_forecast("3M", flat(0.04));
let spread = mc.basis_spread("3M", 0.5, 1.0).unwrap();
assert!(
spread.abs() < 1e-9,
"spread should be zero for identical curves: {spread}"
);
}
}