use crate::error::{MathError, MathResult};
use crate::interpolation::Interpolator;
#[derive(Debug, Clone)]
pub struct MonotoneConvex {
times: Vec<f64>,
zero_rates: Vec<f64>,
discrete_forwards: Vec<f64>,
f_inst: Vec<f64>,
g: Vec<f64>,
allow_extrapolation: bool,
}
impl MonotoneConvex {
pub fn new(times: Vec<f64>, zero_rates: Vec<f64>) -> MathResult<Self> {
if times.len() < 2 {
return Err(MathError::insufficient_data(2, times.len()));
}
if times.len() != zero_rates.len() {
return Err(MathError::invalid_input(format!(
"times and zero_rates must have same length: {} vs {}",
times.len(),
zero_rates.len()
)));
}
if times[0] <= 0.0 {
return Err(MathError::invalid_input(
"first time must be positive (use t=0 extrapolation if needed)",
));
}
for i in 1..times.len() {
if times[i] <= times[i - 1] {
return Err(MathError::invalid_input(
"times must be strictly increasing",
));
}
}
let n = times.len();
let mut discrete_forwards = Vec::with_capacity(n);
discrete_forwards.push(zero_rates[0]);
for i in 1..n {
let t_prev = times[i - 1];
let t_curr = times[i];
let z_prev = zero_rates[i - 1];
let z_curr = zero_rates[i];
let f = (z_curr * t_curr - z_prev * t_prev) / (t_curr - t_prev);
if f < 0.0 {
return Err(MathError::invalid_input(format!(
"negative forward rate {f} between t={t_prev} and t={t_curr}"
)));
}
discrete_forwards.push(f);
}
let mut f_inst = Vec::with_capacity(n);
f_inst.push(discrete_forwards[0]);
for i in 1..n - 1 {
let dt_left = times[i] - times[i - 1];
let dt_right = times[i + 1] - times[i];
let f_left = discrete_forwards[i];
let f_right = discrete_forwards[i + 1];
let f_mid = (dt_right * f_left + dt_left * f_right) / (dt_left + dt_right);
f_inst.push(f_mid);
}
f_inst.push(discrete_forwards[n - 1]);
let g = Self::compute_monotonicity_factors(&discrete_forwards, &f_inst);
Ok(Self {
times,
zero_rates,
discrete_forwards,
f_inst,
g,
allow_extrapolation: false,
})
}
fn compute_monotonicity_factors(discrete_forwards: &[f64], f_inst: &[f64]) -> Vec<f64> {
let n = f_inst.len();
let mut g = vec![0.0; n];
for i in 0..n {
let f_d = discrete_forwards[i.min(discrete_forwards.len() - 1)];
let f_i = f_inst[i];
if f_d > 0.0 {
g[i] = (f_i / f_d).clamp(0.0, 2.0);
} else {
g[i] = 1.0;
}
}
g
}
pub fn forward_rate(&self, t: f64) -> MathResult<f64> {
if t <= 0.0 {
return Ok(self.discrete_forwards[0]);
}
if !self.allow_extrapolation && t > self.times[self.times.len() - 1] {
return Err(MathError::ExtrapolationNotAllowed {
x: t,
min: 0.0,
max: self.times[self.times.len() - 1],
});
}
let i = self.find_interval(t);
let (t_lo, t_hi, f_lo, f_hi) = if i == 0 {
(
0.0,
self.times[0],
self.discrete_forwards[0],
self.f_inst[0],
)
} else if i >= self.times.len() {
let last = self.times.len() - 1;
return Ok(self.f_inst[last]);
} else {
(
self.times[i - 1],
self.times[i],
self.f_inst[i - 1],
self.f_inst[i],
)
};
let x = (t - t_lo) / (t_hi - t_lo);
let f = f_lo + x * (f_hi - f_lo);
Ok(f.max(0.0)) }
fn find_interval(&self, t: f64) -> usize {
match self
.times
.binary_search_by(|probe| probe.partial_cmp(&t).unwrap_or(std::cmp::Ordering::Equal))
{
Ok(i) => i + 1,
Err(i) => i,
}
}
#[must_use]
pub fn with_extrapolation(mut self) -> Self {
self.allow_extrapolation = true;
self
}
#[must_use]
pub fn discrete_forwards(&self) -> &[f64] {
&self.discrete_forwards
}
#[must_use]
pub fn times(&self) -> &[f64] {
&self.times
}
#[must_use]
pub fn zero_rates(&self) -> &[f64] {
&self.zero_rates
}
}
impl Interpolator for MonotoneConvex {
fn interpolate(&self, t: f64) -> MathResult<f64> {
if t <= 0.0 {
return Ok(self.zero_rates[0]);
}
if !self.allow_extrapolation && t > self.times[self.times.len() - 1] {
return Err(MathError::ExtrapolationNotAllowed {
x: t,
min: 0.0,
max: self.times[self.times.len() - 1],
});
}
let i = self.find_interval(t);
if i == 0 {
return Ok(self.zero_rates[0]);
}
if i >= self.times.len() {
return Ok(self.zero_rates[self.zero_rates.len() - 1]);
}
let t_lo = self.times[i - 1];
let t_hi = self.times[i];
let z_lo = self.zero_rates[i - 1];
let z_hi = self.zero_rates[i];
let f_discrete = self.discrete_forwards[i];
let zt_product = z_lo * t_lo + f_discrete * (t - t_lo);
let z = zt_product / t;
let x = (t - t_lo) / (t_hi - t_lo);
let g_factor = self.g[i];
let z_linear = z_lo + x * (z_hi - z_lo);
let z_final = (1.0 - g_factor * 0.1) * z + g_factor * 0.1 * z_linear;
Ok(z_final)
}
fn derivative(&self, t: f64) -> MathResult<f64> {
if t <= 0.0 {
return Ok(0.0);
}
let h = 1e-6;
let t_plus = t + h;
let t_minus = (t - h).max(1e-10);
let z_plus = self.interpolate(t_plus)?;
let z_minus = self.interpolate(t_minus)?;
Ok((z_plus - z_minus) / (t_plus - t_minus))
}
fn allows_extrapolation(&self) -> bool {
self.allow_extrapolation
}
fn min_x(&self) -> f64 {
0.0 }
fn max_x(&self) -> f64 {
self.times[self.times.len() - 1]
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_monotone_convex_through_points() {
let times = vec![0.25, 0.5, 1.0, 2.0, 3.0];
let zero_rates = vec![0.02, 0.025, 0.03, 0.035, 0.04];
let interp = MonotoneConvex::new(times.clone(), zero_rates.clone()).unwrap();
for (t, z) in times.iter().zip(zero_rates.iter()) {
let interpolated = interp.interpolate(*t).unwrap();
assert_relative_eq!(interpolated, *z, epsilon = 0.001);
}
}
#[test]
fn test_monotone_convex_positive_forwards() {
let times = vec![0.5, 1.0, 2.0, 3.0, 5.0, 10.0];
let zero_rates = vec![0.02, 0.025, 0.03, 0.032, 0.035, 0.04];
let interp = MonotoneConvex::new(times, zero_rates).unwrap();
for t in [0.1, 0.5, 1.0, 1.5, 2.5, 4.0, 7.5] {
let fwd = interp.forward_rate(t).unwrap();
assert!(
fwd >= 0.0,
"Forward rate at t={} is {}, should be >= 0",
t,
fwd
);
}
}
#[test]
fn test_monotone_convex_no_oscillation() {
let times = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let zero_rates = vec![0.03, 0.035, 0.035, 0.035, 0.04];
let interp = MonotoneConvex::new(times, zero_rates).unwrap();
let mut prev_z = interp.interpolate(1.0).unwrap();
for t in [1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0] {
let z = interp.interpolate(t).unwrap();
let change = (z - prev_z).abs();
assert!(
change < 0.01,
"Large jump at t={}: {} to {} (change={})",
t,
prev_z,
z,
change
);
prev_z = z;
}
}
#[test]
fn test_monotone_convex_derivative() {
let times = vec![0.5, 1.0, 2.0, 3.0, 5.0];
let zero_rates = vec![0.02, 0.025, 0.03, 0.035, 0.04];
let interp = MonotoneConvex::new(times, zero_rates).unwrap();
let t = 1.5;
let h = 1e-6;
let z_plus = interp.interpolate(t + h).unwrap();
let z_minus = interp.interpolate(t - h).unwrap();
let numerical_deriv = (z_plus - z_minus) / (2.0 * h);
let analytical_deriv = interp.derivative(t).unwrap();
assert_relative_eq!(analytical_deriv, numerical_deriv, epsilon = 1e-4);
}
#[test]
fn test_monotone_convex_rejects_negative_forwards() {
let times = vec![1.0, 2.0, 3.0];
let zero_rates = vec![0.05, 0.02, 0.04];
let result = MonotoneConvex::new(times, zero_rates);
assert!(result.is_err());
}
#[test]
fn test_monotone_convex_flat_curve() {
let times = vec![1.0, 2.0, 3.0, 5.0, 10.0];
let zero_rates = vec![0.03, 0.03, 0.03, 0.03, 0.03];
let interp = MonotoneConvex::new(times, zero_rates).unwrap();
for t in [0.5, 1.5, 2.5, 4.0, 7.0] {
let z = interp.interpolate(t).unwrap();
assert_relative_eq!(z, 0.03, epsilon = 0.001);
}
for t in [0.5, 1.5, 2.5, 4.0, 7.0] {
let f = interp.forward_rate(t).unwrap();
assert_relative_eq!(f, 0.03, epsilon = 0.001);
}
}
#[test]
fn test_monotone_convex_steep_curve() {
let times = vec![0.25, 0.5, 1.0, 2.0, 5.0, 10.0];
let zero_rates = vec![0.01, 0.015, 0.025, 0.04, 0.05, 0.055];
let interp = MonotoneConvex::new(times, zero_rates).unwrap();
for t in [0.1, 0.3, 0.75, 1.5, 3.0, 7.0] {
let f = interp.forward_rate(t).unwrap();
assert!(f > 0.0);
}
}
#[test]
fn test_monotone_convex_inverted_curve() {
let times = vec![0.5, 1.0, 2.0, 5.0, 10.0];
let zero_rates = vec![0.05, 0.048, 0.045, 0.04, 0.038];
let interp = MonotoneConvex::new(times, zero_rates).unwrap();
for t in [0.25, 0.75, 1.5, 3.0, 7.0] {
let f = interp.forward_rate(t).unwrap();
assert!(f >= 0.0, "Forward at t={} is {} (should be >= 0)", t, f);
}
}
#[test]
fn test_monotone_convex_extrapolation() {
let times = vec![1.0, 2.0, 3.0];
let zero_rates = vec![0.03, 0.035, 0.04];
let interp = MonotoneConvex::new(times, zero_rates).unwrap();
assert!(interp.interpolate(5.0).is_err());
let interp_extrap = interp.with_extrapolation();
let z = interp_extrap.interpolate(5.0).unwrap();
assert!(z > 0.0);
}
#[test]
fn test_discrete_forwards() {
let times = vec![1.0, 2.0, 3.0];
let zero_rates = vec![0.02, 0.03, 0.04];
let interp = MonotoneConvex::new(times, zero_rates).unwrap();
let fwds = interp.discrete_forwards();
assert_relative_eq!(fwds[0], 0.02, epsilon = 1e-10);
assert_relative_eq!(fwds[1], 0.04, epsilon = 1e-10);
assert_relative_eq!(fwds[2], 0.06, epsilon = 1e-10);
}
}