use crate::curves::DiscountCurve;
use crate::errors::CurveError;
use crate::types::Daycount;
#[derive(Debug)]
pub struct ForwardCurve<'a> {
curve: &'a DiscountCurve,
}
impl<'a> ForwardCurve<'a> {
#[must_use]
#[inline]
pub fn from(curve: &'a DiscountCurve) -> Self {
Self { curve }
}
pub fn instantaneous(&self, t: f64) -> Result<f64, CurveError> {
self.curve.instantaneous_forward(t)
}
pub fn forward(&self, t1: f64, t2: f64, daycount: Daycount) -> Result<f64, CurveError> {
self.curve.forward_rate(t1, t2, daycount)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::interpolation::Interpolation;
use crate::types::Date;
fn d(y: i32, m: u32, day: u32) -> Date {
Date::from_ymd(y, m, day).unwrap()
}
fn reference_date() -> Date {
d(2024, 1, 2)
}
fn flat_curve(r_c: f64) -> DiscountCurve {
let mut times = Vec::new();
let mut discs = Vec::new();
for i in 0..=120 {
let date = Date::from_serial(reference_date().serial() + i * 91);
let t = Daycount::Act365F
.year_fraction(reference_date(), date)
.unwrap();
times.push(t);
discs.push((-r_c * t).exp());
}
DiscountCurve::from_times_and_discounts(
reference_date(),
Daycount::Act365F,
×,
&discs,
Interpolation::LogLinear,
)
.unwrap()
}
#[test]
fn from_constructs_view() {
let curve = flat_curve(0.04);
let _f = ForwardCurve::from(&curve);
}
#[test]
fn instantaneous_flat_curve_loglinear() {
let r_c = 0.04;
let curve = flat_curve(r_c);
let f = ForwardCurve::from(&curve);
for t in [0.5_f64, 1.0, 5.0, 10.0] {
let v = f.instantaneous(t).unwrap();
assert!((v - r_c).abs() < 1e-10, "t={t}: {v}");
}
}
#[test]
fn forward_flat_curve_closed_form() {
let r_c = 0.04_f64;
let curve = flat_curve(r_c);
let f = ForwardCurve::from(&curve);
let cases = [(1.0_f64, 2.0_f64), (0.5, 3.0), (2.0, 5.0)];
for (t1, t2) in cases {
let l = f.forward(t1, t2, Daycount::Act365F).unwrap();
let expected = (r_c * (t2 - t1)).exp_m1() / (t2 - t1);
assert!((l - expected).abs() < 1e-12, "fwd[{t1},{t2}]: {l}");
}
}
#[test]
fn instantaneous_via_forward_finite_difference_limit() {
let r_c = 0.04_f64;
let curve = flat_curve(r_c);
let f = ForwardCurve::from(&curve);
let t = 1.5;
let h = 1e-4_f64;
let l = f.forward(t, t + h, Daycount::Act365F).unwrap();
let inst = f.instantaneous(t).unwrap();
assert!(
(l - inst).abs() < 1e-6,
"fwd FD = {l}, instantaneous = {inst}"
);
}
#[test]
fn forward_rejects_t2_le_t1() {
let curve = flat_curve(0.04);
let f = ForwardCurve::from(&curve);
assert!(matches!(
f.forward(2.0, 1.0, Daycount::Act365F).unwrap_err(),
CurveError::InvalidTime { .. }
));
}
#[test]
fn instantaneous_rejects_negative_time() {
let curve = flat_curve(0.04);
let f = ForwardCurve::from(&curve);
assert!(matches!(
f.instantaneous(-0.5).unwrap_err(),
CurveError::InvalidTime { .. }
));
}
#[test]
fn instantaneous_at_anchor() {
let r_c = 0.04_f64;
let curve = flat_curve(r_c);
let f = ForwardCurve::from(&curve);
let v = f.instantaneous(0.0).unwrap();
assert!(
(v - r_c).abs() < 1e-10,
"instantaneous at anchor: {v}, expected {r_c}"
);
}
}