use crate::errors::QlResult;
use crate::handle::Handle;
use crate::interestrate::Compounding;
use crate::patterns::observable::{AsObservable, Observable, ResetThenNotify};
use crate::quotes::Quote;
use crate::shared::{Shared, SharedMut, shared};
use crate::termstructures::yields::ZeroYieldStructure;
use crate::termstructures::yields::zerospreadedtermstructure::spawn_extrapolation_sync;
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::time::frequency::Frequency;
use crate::types::{DiscountFactor, Natural, Rate, Time};
pub struct ForwardSpreadedTermStructure {
base: Shared<TermStructureBase>,
original: Handle<dyn YieldTermStructure>,
spread: Handle<dyn Quote>,
_listener: SharedMut<ResetThenNotify>,
}
impl ForwardSpreadedTermStructure {
pub fn new(
original: Handle<dyn YieldTermStructure>,
spread: Handle<dyn Quote>,
) -> ForwardSpreadedTermStructure {
let base = shared(TermStructureBase::new(None));
let listener = spawn_extrapolation_sync(&base, &original, &spread);
ForwardSpreadedTermStructure {
base,
original,
spread,
_listener: listener,
}
}
}
impl AsObservable for ForwardSpreadedTermStructure {
fn observable(&self) -> &Observable {
self.base.observable()
}
}
impl TermStructure for ForwardSpreadedTermStructure {
fn base(&self) -> &TermStructureBase {
&self.base
}
fn max_date(&self) -> Date {
self.original
.current_link()
.map(|curve| curve.max_date())
.unwrap_or_else(|_| Date::null())
}
fn day_counter(&self) -> Option<DayCounter> {
self.original
.current_link()
.ok()
.and_then(|curve| curve.day_counter())
}
fn calendar(&self) -> Option<Calendar> {
self.original
.current_link()
.ok()
.and_then(|curve| curve.calendar())
}
fn settlement_days(&self) -> QlResult<Natural> {
self.original.current_link()?.settlement_days()
}
fn reference_date(&self) -> QlResult<Date> {
self.original.current_link()?.reference_date()
}
fn max_time(&self) -> QlResult<Time> {
self.original.current_link()?.max_time()
}
}
impl ZeroYieldStructure for ForwardSpreadedTermStructure {
fn zero_yield_impl(&self, t: Time) -> QlResult<Rate> {
let original = self.original.current_link()?;
let zero = original.zero_rate(t, Compounding::Continuous, Frequency::NoFrequency, true)?;
Ok(zero.rate() + self.spread.current_link()?.value()?)
}
}
impl YieldTermStructure for ForwardSpreadedTermStructure {
fn discount_impl(&self, t: Time) -> QlResult<DiscountFactor> {
self.discount_from_zero_yield(t)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::handle::RelinkableHandle;
use crate::quotes::SimpleQuote;
use crate::termstructures::yields::FlatForward;
use crate::test_support::{Flag, as_observer};
use crate::time::date::Month;
use crate::time::daycounters::actual360::Actual360;
fn today() -> Date {
Date::new(15, Month::June, 2026)
}
fn flat_curve(rate: Rate) -> Shared<dyn YieldTermStructure> {
shared(FlatForward::with_rate(
today(),
rate,
Actual360::new(),
Compounding::Continuous,
Frequency::Annual,
))
}
#[test]
fn spreaded_forward_is_the_underlying_plus_the_spread() {
let tolerance = 1.0e-10;
let curve = flat_curve(0.06);
let spread = shared(SimpleQuote::new(0.01));
let spreaded = ForwardSpreadedTermStructure::new(
Handle::new(curve.clone()),
Handle::new(spread.clone() as Shared<dyn Quote>),
);
let test_date = curve.reference_date().unwrap() + 1800;
let forward = curve
.forward_rate_between(
test_date,
test_date,
curve.day_counter().unwrap(),
Compounding::Continuous,
Frequency::NoFrequency,
false,
)
.unwrap();
let spreaded_forward = spreaded
.forward_rate_between(
test_date,
test_date,
spreaded.day_counter().unwrap(),
Compounding::Continuous,
Frequency::NoFrequency,
false,
)
.unwrap();
assert!(
(forward.rate() - (spreaded_forward.rate() - spread.value().unwrap())).abs()
< tolerance,
"unable to reproduce forward from spreaded curve"
);
}
#[test]
fn discounts_scale_by_the_exponential_spread() {
let curve = flat_curve(0.06);
let spread = shared(SimpleQuote::new(0.01));
let spreaded = ForwardSpreadedTermStructure::new(
Handle::new(curve.clone()),
Handle::new(spread as Shared<dyn Quote>),
);
for t in [0.5_f64, 2.0, 7.5] {
let df = spreaded.discount(t, false).unwrap();
let expected = curve.discount(t, false).unwrap() * (-0.01 * t).exp();
assert!((df - expected).abs() < 1.0e-15);
}
assert_eq!(spreaded.discount(0.0, false).unwrap(), 1.0);
}
#[test]
fn relink_and_spread_changes_notify_observers() {
let handle: RelinkableHandle<dyn YieldTermStructure> = RelinkableHandle::empty();
let spread = shared(SimpleQuote::new(0.01));
let spreaded = ForwardSpreadedTermStructure::new(
handle.handle(),
Handle::new(spread.clone() as Shared<dyn Quote>),
);
let flag = Flag::new();
spreaded.observable().register_observer(&as_observer(&flag));
handle.link_to(flat_curve(0.06));
assert!(
Flag::is_up(&flag),
"observer was not notified of term structure change"
);
Flag::lower(&flag);
spread.set_value(0.005);
assert!(
Flag::is_up(&flag),
"observer was not notified of spread change"
);
}
}