use crate::errors::QlResult;
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::types::{DiscountFactor, Rate, Time};
pub trait ZeroYieldStructure: YieldTermStructure {
fn zero_yield_impl(&self, t: Time) -> QlResult<Rate>;
fn discount_from_zero_yield(&self, t: Time) -> QlResult<DiscountFactor> {
if t == 0.0 {
return Ok(1.0);
}
let r = self.zero_yield_impl(t)?;
Ok((-r * t).exp())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fail;
use crate::interestrate::Compounding;
use crate::patterns::observable::{AsObservable, Observable};
use crate::termstructures::{TermStructure, TermStructureBase};
use crate::time::date::{Date, Month};
use crate::time::daycounters::actual360::Actual360;
use crate::time::frequency::Frequency;
use crate::types::Real;
struct LinearZeroCurve {
base: TermStructureBase,
a: Real,
b: Real,
}
impl LinearZeroCurve {
fn new(a: Real, b: Real) -> LinearZeroCurve {
LinearZeroCurve {
base: TermStructureBase::with_reference_date(
Date::new(15, Month::June, 2026),
None,
Some(Actual360::new()),
),
a,
b,
}
}
}
impl AsObservable for LinearZeroCurve {
fn observable(&self) -> &Observable {
self.base.observable()
}
}
impl TermStructure for LinearZeroCurve {
fn base(&self) -> &TermStructureBase {
&self.base
}
fn max_date(&self) -> Date {
Date::max_date()
}
}
impl ZeroYieldStructure for LinearZeroCurve {
fn zero_yield_impl(&self, t: Time) -> QlResult<Rate> {
if t == 0.0 {
fail!("zero yield not defined at t = 0");
}
Ok(self.a + self.b * t)
}
}
impl YieldTermStructure for LinearZeroCurve {
fn discount_impl(&self, t: Time) -> QlResult<DiscountFactor> {
self.discount_from_zero_yield(t)
}
}
#[test]
fn discount_is_derived_from_the_zero_yield() {
let curve = LinearZeroCurve::new(0.03, 0.01);
for t in [0.25_f64, 1.0, 2.5] {
let expected = (-(0.03 + 0.01 * t) * t).exp();
assert!((curve.discount(t, false).unwrap() - expected).abs() < 1.0e-15);
}
}
#[test]
fn discount_at_zero_guards_the_zero_yield_call() {
let curve = LinearZeroCurve::new(0.03, 0.01);
assert!(curve.zero_yield_impl(0.0).is_err());
assert_eq!(curve.discount(0.0, false).unwrap(), 1.0);
}
#[test]
fn zero_yield_errors_propagate_through_discount() {
struct FailingCurve {
inner: LinearZeroCurve,
}
impl AsObservable for FailingCurve {
fn observable(&self) -> &Observable {
self.inner.observable()
}
}
impl TermStructure for FailingCurve {
fn base(&self) -> &TermStructureBase {
self.inner.base()
}
fn max_date(&self) -> Date {
Date::max_date()
}
}
impl ZeroYieldStructure for FailingCurve {
fn zero_yield_impl(&self, _t: Time) -> QlResult<Rate> {
fail!("no zero yield available");
}
}
impl YieldTermStructure for FailingCurve {
fn discount_impl(&self, t: Time) -> QlResult<DiscountFactor> {
self.discount_from_zero_yield(t)
}
}
let curve = FailingCurve {
inner: LinearZeroCurve::new(0.03, 0.01),
};
let err = curve.discount(1.0, false).unwrap_err();
assert!(err.message().contains("no zero yield available"));
assert_eq!(curve.discount(0.0, false).unwrap(), 1.0);
}
#[test]
fn zero_rate_recovers_the_curve_and_forwards_differentiate_it() {
let curve = LinearZeroCurve::new(0.03, 0.01);
let zero = curve
.zero_rate(2.0, Compounding::Continuous, Frequency::Annual, false)
.unwrap();
assert!((zero.rate() - 0.05).abs() < 1.0e-14);
let forward = curve
.forward_rate(2.0, 2.0, Compounding::Continuous, Frequency::Annual, false)
.unwrap();
assert!((forward.rate() - 0.07).abs() < 1.0e-6);
}
}