use crate::errors::QlResult;
use crate::math::interpolations::loglinear::LogLinear;
use crate::math::interpolations::{Interpolation, Interpolator};
use crate::patterns::observable::{AsObservable, Observable};
use crate::termstructures::interpolatedcurve::InterpolatedCurve;
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::termstructures::{TermStructure, TermStructureBase};
use crate::time::calendar::Calendar;
use crate::time::date::Date;
use crate::time::daycounter::DayCounter;
use crate::types::{DiscountFactor, Real, Time};
use crate::{fail, require};
pub struct InterpolatedDiscountCurve<I: Interpolator> {
base: TermStructureBase,
dates: Vec<Date>,
curve: InterpolatedCurve<I>,
}
pub type DiscountCurve = InterpolatedDiscountCurve<LogLinear>;
impl<I: Interpolator + Default> InterpolatedDiscountCurve<I> {
pub fn new(
dates: Vec<Date>,
discounts: Vec<DiscountFactor>,
day_counter: DayCounter,
calendar: Option<Calendar>,
) -> QlResult<InterpolatedDiscountCurve<I>> {
Self::with_interpolator(dates, discounts, day_counter, calendar, I::default())
}
}
impl<I: Interpolator> InterpolatedDiscountCurve<I> {
pub fn with_interpolator(
dates: Vec<Date>,
discounts: Vec<DiscountFactor>,
day_counter: DayCounter,
calendar: Option<Calendar>,
interpolator: I,
) -> QlResult<InterpolatedDiscountCurve<I>> {
require!(
dates.len() >= interpolator.required_points(),
"not enough input dates given"
);
require!(discounts.len() == dates.len(), "dates/data count mismatch");
require!(
discounts[0] == 1.0,
"the first discount must be == 1.0 to flag the corresponding date as reference date"
);
for &discount in discounts.iter().skip(1) {
if discount.is_nan() || discount <= 0.0 {
fail!("negative discount");
}
}
let times = InterpolatedCurve::<I>::times_from_dates(&dates, dates[0], &day_counter)?;
let mut curve = InterpolatedCurve::new(times, discounts, interpolator);
curve.setup_interpolation()?;
Ok(InterpolatedDiscountCurve {
base: TermStructureBase::with_reference_date(dates[0], calendar, Some(day_counter)),
dates,
curve,
})
}
pub fn times(&self) -> &[Time] {
self.curve.times()
}
pub fn dates(&self) -> &[Date] {
&self.dates
}
pub fn data(&self) -> &[Real] {
self.curve.data()
}
pub fn discounts(&self) -> &[DiscountFactor] {
self.curve.data()
}
pub fn nodes(&self) -> Vec<(Date, Real)> {
self.dates
.iter()
.copied()
.zip(self.curve.data().iter().copied())
.collect()
}
}
impl<I: Interpolator> AsObservable for InterpolatedDiscountCurve<I> {
fn observable(&self) -> &Observable {
self.base.observable()
}
}
impl<I: Interpolator> TermStructure for InterpolatedDiscountCurve<I> {
fn base(&self) -> &TermStructureBase {
&self.base
}
fn max_date(&self) -> Date {
match self.curve.max_date() {
Some(date) => date,
None => *self.dates.last().expect("the constructor requires nodes"),
}
}
}
impl<I: Interpolator> YieldTermStructure for InterpolatedDiscountCurve<I> {
fn discount_impl(&self, t: Time) -> QlResult<DiscountFactor> {
let interpolation = self.curve.interpolation()?;
let t_max = *self
.curve
.times()
.last()
.expect("the constructor requires nodes");
if t <= t_max {
return interpolation.value(t);
}
let d_max = *self
.curve
.data()
.last()
.expect("the constructor requires nodes");
let inst_fwd_max = -interpolation.derivative(t_max)? / d_max;
Ok(d_max * (-inst_fwd_max * (t - t_max)).exp())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::interestrate::Compounding;
use crate::math::interpolations::linear::Linear;
use crate::time::date::Month;
use crate::time::daycounters::actual360::Actual360;
use crate::time::frequency::Frequency;
fn reference() -> Date {
Date::new(15, Month::June, 2026)
}
fn expect_err<T>(result: QlResult<T>) -> crate::errors::QlError {
match result {
Ok(_) => panic!("expected an error"),
Err(err) => err,
}
}
fn sample_dates() -> Vec<Date> {
let reference = reference();
vec![reference, reference + 180, reference + 360, reference + 720]
}
fn sample_discounts() -> Vec<DiscountFactor> {
vec![1.0, 0.97, 0.94, 0.88]
}
fn sample_curve() -> DiscountCurve {
DiscountCurve::new(sample_dates(), sample_discounts(), Actual360::new(), None).unwrap()
}
#[test]
fn nodes_are_reproduced() {
let curve = sample_curve();
assert_eq!(curve.reference_date().unwrap(), reference());
assert_eq!(curve.max_date(), reference() + 720);
assert_eq!(curve.times(), &[0.0, 0.5, 1.0, 2.0]);
for (date, discount) in sample_dates().into_iter().zip(sample_discounts()) {
let df = curve.discount_date(date, false).unwrap();
assert!((df - discount).abs() < 1.0e-15);
}
}
#[test]
fn log_linear_interpolates_geometrically_between_nodes() {
let curve = sample_curve();
let df = curve.discount(0.75, false).unwrap();
assert!((df - (0.97_f64 * 0.94).sqrt()).abs() < 1.0e-15);
}
#[test]
fn forwards_are_piecewise_constant_under_log_linear() {
let curve = sample_curve();
let expected = (0.94_f64 / 0.88).ln();
for (t1, t2) in [(1.0, 1.25), (1.25, 1.75), (1.9, 2.0)] {
let forward = curve
.forward_rate(t1, t2, Compounding::Continuous, Frequency::Annual, false)
.unwrap();
assert!((forward.rate() - expected).abs() < 1.0e-12);
}
}
#[test]
fn extrapolation_continues_the_last_forward_flat() {
let curve = sample_curve();
assert!(curve.discount(3.0, false).is_err());
let last_forward = (0.94_f64 / 0.88).ln();
let expected = 0.88 * (-last_forward * 1.0).exp();
let df = curve.discount(3.0, true).unwrap();
assert!((df - expected).abs() < 1.0e-14);
curve.enable_extrapolation();
let df = curve.discount(3.0, false).unwrap();
assert!((df - expected).abs() < 1.0e-14);
let forward = curve
.forward_rate(3.0, 3.0, Compounding::Continuous, Frequency::Annual, false)
.unwrap();
assert!((forward.rate() - last_forward).abs() < 1.0e-9);
}
#[test]
fn zero_rates_round_trip_the_discounts() {
let curve = sample_curve();
let zero = curve
.zero_rate(1.0, Compounding::Continuous, Frequency::Annual, false)
.unwrap();
assert!((zero.rate() - -(0.94_f64.ln())).abs() < 1.0e-14);
}
#[test]
fn generic_interpolator_drives_the_interpolation_space() {
let curve = InterpolatedDiscountCurve::<Linear>::new(
sample_dates(),
sample_discounts(),
Actual360::new(),
None,
)
.unwrap();
let df = curve.discount(0.75, false).unwrap();
assert!((df - 0.955).abs() < 1.0e-15);
}
#[test]
fn inspectors_expose_the_nodes() {
let curve = sample_curve();
assert_eq!(curve.dates(), &sample_dates()[..]);
assert_eq!(curve.data(), &sample_discounts()[..]);
assert_eq!(curve.discounts(), &sample_discounts()[..]);
let nodes = curve.nodes();
assert_eq!(nodes.len(), 4);
assert_eq!(nodes[0], (reference(), 1.0));
assert_eq!(nodes[3], (reference() + 720, 0.88));
}
#[test]
fn constructor_rejects_invalid_input() {
let day_counter = Actual360::new();
let err = expect_err(DiscountCurve::new(
vec![reference()],
vec![1.0],
day_counter.clone(),
None,
));
assert!(err.message().contains("not enough input dates"));
let err = expect_err(DiscountCurve::new(
sample_dates(),
vec![1.0, 0.97],
day_counter.clone(),
None,
));
assert!(err.message().contains("dates/data count mismatch"));
let err = expect_err(DiscountCurve::new(
sample_dates(),
vec![0.99, 0.97, 0.94, 0.88],
day_counter.clone(),
None,
));
assert!(err.message().contains("first discount must be == 1.0"));
for bad in [0.0, -0.5, DiscountFactor::NAN] {
let err = expect_err(DiscountCurve::new(
sample_dates(),
vec![1.0, 0.97, bad, 0.88],
day_counter.clone(),
None,
));
assert!(err.message().contains("negative discount"));
}
let mut dates = sample_dates();
dates.swap(1, 2);
let err = expect_err(DiscountCurve::new(
dates,
sample_discounts(),
day_counter,
None,
));
assert!(err.message().contains("dates not sorted"));
}
}