mod cubic_spline;
mod flat_forward;
mod linear;
mod log_linear;
mod monotone_convex;
mod parametric;
pub use cubic_spline::CubicSpline;
pub use flat_forward::FlatForward;
pub use linear::LinearInterpolator;
pub use log_linear::LogLinearInterpolator;
pub use monotone_convex::MonotoneConvex;
pub use parametric::{NelsonSiegel, Svensson};
use crate::error::MathResult;
pub trait Interpolator: Send + Sync {
fn interpolate(&self, x: f64) -> MathResult<f64>;
fn derivative(&self, x: f64) -> MathResult<f64>;
fn allows_extrapolation(&self) -> bool {
false
}
fn min_x(&self) -> f64;
fn max_x(&self) -> f64;
fn in_range(&self, x: f64) -> bool {
x >= self.min_x() && x <= self.max_x()
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_linear_basic() {
let xs = vec![0.0, 1.0, 2.0];
let ys = vec![0.0, 1.0, 2.0];
let interp = LinearInterpolator::new(xs, ys).unwrap();
assert_relative_eq!(interp.interpolate(0.5).unwrap(), 0.5, epsilon = 1e-10);
assert_relative_eq!(interp.interpolate(1.5).unwrap(), 1.5, epsilon = 1e-10);
}
#[test]
fn test_all_interpolators_through_points() {
let times = vec![0.5, 1.0, 2.0, 3.0, 5.0];
let rates = vec![0.02, 0.025, 0.03, 0.035, 0.04];
let linear = LinearInterpolator::new(times.clone(), rates.clone()).unwrap();
for (t, r) in times.iter().zip(rates.iter()) {
assert_relative_eq!(linear.interpolate(*t).unwrap(), *r, epsilon = 1e-10);
}
let spline = CubicSpline::new(times.clone(), rates.clone()).unwrap();
for (t, r) in times.iter().zip(rates.iter()) {
assert_relative_eq!(spline.interpolate(*t).unwrap(), *r, epsilon = 1e-10);
}
let dfs: Vec<f64> = times
.iter()
.zip(rates.iter())
.map(|(t, r)| (-r * t).exp())
.collect();
let log_linear = LogLinearInterpolator::new(times.clone(), dfs.clone()).unwrap();
for (t, df) in times.iter().zip(dfs.iter()) {
assert_relative_eq!(log_linear.interpolate(*t).unwrap(), *df, epsilon = 1e-10);
}
let mc = MonotoneConvex::new(times.clone(), rates.clone()).unwrap();
for (t, r) in times.iter().zip(rates.iter()) {
assert_relative_eq!(mc.interpolate(*t).unwrap(), *r, epsilon = 0.002);
}
}
#[test]
fn test_forward_rate_positivity() {
let times = vec![0.5, 1.0, 2.0, 3.0, 5.0, 10.0];
let rates = vec![0.02, 0.025, 0.03, 0.028, 0.035, 0.04];
let mc = MonotoneConvex::new(times, rates).unwrap();
for t in [
0.1, 0.25, 0.5, 0.75, 1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0, 7.0, 10.0,
] {
let fwd = mc.forward_rate(t).unwrap();
assert!(fwd >= 0.0, "Forward at t={} is {}, should be >= 0", t, fwd);
}
}
#[test]
fn test_derivative_consistency() {
let times = vec![0.5, 1.0, 2.0, 3.0, 5.0];
let rates = vec![0.02, 0.025, 0.03, 0.035, 0.04];
let linear = LinearInterpolator::new(times.clone(), rates.clone()).unwrap();
check_derivative(&linear, 1.5, "Linear");
let spline = CubicSpline::new(times.clone(), rates.clone()).unwrap();
check_derivative(&spline, 1.5, "CubicSpline");
let dfs: Vec<f64> = times
.iter()
.zip(rates.iter())
.map(|(t, r)| (-r * t).exp())
.collect();
let log_linear = LogLinearInterpolator::new(times.clone(), dfs).unwrap();
check_derivative(&log_linear, 1.5, "LogLinear");
let mc = MonotoneConvex::new(times.clone(), rates.clone()).unwrap();
check_derivative(&mc, 1.5, "MonotoneConvex");
let ns = NelsonSiegel::new(0.04, -0.02, 0.01, 2.0).unwrap();
check_derivative(&ns, 3.0, "NelsonSiegel");
let sv = Svensson::new(0.04, -0.02, 0.01, -0.005, 2.0, 8.0).unwrap();
check_derivative(&sv, 3.0, "Svensson");
}
fn check_derivative(interp: &dyn Interpolator, t: f64, name: &str) {
let h = 1e-6;
let y_plus = interp.interpolate(t + h).unwrap();
let y_minus = interp.interpolate(t - h).unwrap();
let numerical = (y_plus - y_minus) / (2.0 * h);
let analytical = interp.derivative(t).unwrap();
assert!(
(analytical - numerical).abs() < 1e-4,
"{} derivative at t={}: analytical={}, numerical={}",
name,
t,
analytical,
numerical
);
}
#[test]
fn test_yield_curve_construction() {
let maturities = vec![0.25, 0.5, 1.0, 2.0, 3.0, 5.0, 7.0, 10.0, 20.0, 30.0];
let zero_rates = vec![
0.0200, 0.0210, 0.0225, 0.0250, 0.0275, 0.0310, 0.0340, 0.0370, 0.0400, 0.0410,
];
let linear = LinearInterpolator::new(maturities.clone(), zero_rates.clone()).unwrap();
let spline = CubicSpline::new(maturities.clone(), zero_rates.clone()).unwrap();
let mc = MonotoneConvex::new(maturities.clone(), zero_rates.clone()).unwrap();
let t = 4.0;
let z_linear = linear.interpolate(t).unwrap();
let z_spline = spline.interpolate(t).unwrap();
let z_mc = mc.interpolate(t).unwrap();
assert!(z_linear > 0.0275 && z_linear < 0.0310);
assert!(z_spline > 0.0275 && z_spline < 0.0310);
assert!(z_mc > 0.0275 && z_mc < 0.0310);
let fwd = mc.forward_rate(t).unwrap();
assert!(fwd > 0.0);
}
#[test]
fn test_parametric_curve_fitting() {
let ns = NelsonSiegel::new(0.04, -0.015, 0.008, 2.5).unwrap();
let r_1y = ns.interpolate(1.0).unwrap();
let r_10y = ns.interpolate(10.0).unwrap();
assert!(r_1y < r_10y);
let r_3y = ns.interpolate(3.0).unwrap();
let d_1y = ns.derivative(1.0).unwrap();
let d_10y = ns.derivative(10.0).unwrap();
assert!(r_3y > r_1y && r_3y < r_10y);
assert!(d_1y > 0.0);
assert!(d_1y > d_10y);
}
#[test]
fn test_discount_factor_interpolation() {
let times = vec![0.25, 0.5, 1.0, 2.0, 5.0, 10.0];
let rates = [0.02, 0.022, 0.025, 0.03, 0.035, 0.04];
let dfs: Vec<f64> = times
.iter()
.zip(rates.iter())
.map(|(t, r): (&f64, &f64)| (-r * t).exp())
.collect();
let log_linear = LogLinearInterpolator::new(times.clone(), dfs.clone()).unwrap();
let mut prev_df = 1.0;
for t in [0.25, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 5.0, 7.0, 10.0] {
let df = log_linear.interpolate(t).unwrap();
assert!(
df < prev_df,
"DF at t={} ({}) should be < previous ({})",
t,
df,
prev_df
);
assert!(df > 0.0, "DF should be positive");
prev_df = df;
}
}
}