#![allow(clippy::doc_lazy_continuation)]
use ndarray::Array1;
use super::types::CurvePoint;
use crate::traits::FloatExt;
#[derive(Debug, Clone)]
pub struct Svensson<T: FloatExt> {
pub beta0: T,
pub beta1: T,
pub beta2: T,
pub beta3: T,
pub lambda1: T,
pub lambda2: T,
}
impl<T: FloatExt> Svensson<T> {
pub fn new(beta0: T, beta1: T, beta2: T, beta3: T, lambda1: T, lambda2: T) -> Self {
Self {
beta0,
beta1,
beta2,
beta3,
lambda1,
lambda2,
}
}
fn slope_loading(&self, tau: T, lambda: T) -> T {
let x = tau / lambda;
if x < T::from_f64_fast(1e-10) {
return T::one();
}
(T::one() - (-x).exp()) / x
}
fn curvature_loading(&self, tau: T, lambda: T) -> T {
let x = tau / lambda;
if x < T::from_f64_fast(1e-10) {
return T::zero();
}
(T::one() - (-x).exp()) / x - (-x).exp()
}
pub fn zero_rate(&self, tau: T) -> T {
self.beta0
+ self.beta1 * self.slope_loading(tau, self.lambda1)
+ self.beta2 * self.curvature_loading(tau, self.lambda1)
+ self.beta3 * self.curvature_loading(tau, self.lambda2)
}
pub fn forward_rate(&self, tau: T) -> T {
let x1 = tau / self.lambda1;
let x2 = tau / self.lambda2;
let exp_x1 = (-x1).exp();
let exp_x2 = (-x2).exp();
self.beta0 + self.beta1 * exp_x1 + self.beta2 * x1 * exp_x1 + self.beta3 * x2 * exp_x2
}
pub fn discount_factor(&self, tau: T) -> T {
(-self.zero_rate(tau) * tau).exp()
}
pub fn curve_points(&self, maturities: &Array1<T>) -> Vec<CurvePoint<T>> {
maturities
.iter()
.map(|&tau| CurvePoint {
time: tau,
discount_factor: self.discount_factor(tau),
})
.collect()
}
pub fn fit(maturities: &Array1<T>, market_rates: &Array1<T>) -> Self {
let n = maturities.len();
let mut best_sse = T::from_f64_fast(f64::MAX);
let mut best = Self::new(
T::zero(),
T::zero(),
T::zero(),
T::zero(),
T::one(),
T::from_f64_fast(2.0),
);
let lambda_min = T::from_f64_fast(0.1);
let lambda_max = T::from_f64_fast(5.0);
let step = T::from_f64_fast(0.05);
let mut l1 = lambda_min;
while l1 <= lambda_max {
let mut l2 = lambda_min;
while l2 <= lambda_max {
if (l1 - l2).abs() < T::from_f64_fast(0.05) {
l2 += step;
continue;
}
let trial = Self::new(T::zero(), T::zero(), T::zero(), T::zero(), l1, l2);
let mut xtx_data = [0.0_f64; 16];
let mut xty_data = [0.0_f64; 4];
for i in 0..n {
let tau = maturities[i];
let y = market_rates[i].to_f64().unwrap();
let x = [
1.0,
trial.slope_loading(tau, l1).to_f64().unwrap(),
trial.curvature_loading(tau, l1).to_f64().unwrap(),
trial.curvature_loading(tau, l2).to_f64().unwrap(),
];
for r in 0..4 {
xty_data[r] += x[r] * y;
for c in 0..4 {
xtx_data[r * 4 + c] += x[r] * x[c];
}
}
}
if let Some(betas) = super::linalg::solve_linalg::<4>(&xtx_data, &xty_data) {
let candidate = Self::new(
T::from_f64_fast(betas[0]),
T::from_f64_fast(betas[1]),
T::from_f64_fast(betas[2]),
T::from_f64_fast(betas[3]),
l1,
l2,
);
let mut sse = T::zero();
for i in 0..n {
let err = market_rates[i] - candidate.zero_rate(maturities[i]);
sse += err * err;
}
if sse < best_sse {
best_sse = sse;
best = candidate;
}
}
l2 += step;
}
l1 += step;
}
best
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn long_rate_approaches_beta0() {
let s = Svensson::<f64>::new(0.04, -0.02, 0.01, 0.005, 0.5, 2.0);
let r_long = s.zero_rate(50.0);
assert!((r_long - 0.04).abs() < 1e-3, "{r_long} ≠ β0=0.04");
}
#[test]
fn discount_factor_at_zero_is_one() {
let s = Svensson::<f64>::new(0.04, -0.02, 0.01, 0.005, 0.5, 2.0);
let df = s.discount_factor(0.0);
assert!((df - 1.0).abs() < 1e-12);
}
}