pub mod bootstraphelper;
pub mod bootstraptraits;
pub mod interpolatedcurve;
pub mod iterativebootstrap;
pub mod volatility;
pub mod yields;
pub mod yieldtermstructure;
pub use bootstraphelper::{
BootstrapHelperBase, RateHelper, RelativeDateRateHelper, compare_by_pillar_date,
sort_by_pillar_date,
};
use std::cell::Cell;
use crate::errors::QlResult;
use crate::math::comparison::close_enough;
use crate::patterns::observable::{AsObservable, Observable, Observer, ResetThenNotify};
use crate::settings::Settings;
use crate::shared::{Shared, SharedMut, shared};
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::calendar::Calendar;
use crate::time::date::Date;
use crate::time::daycounter::DayCounter;
use crate::time::timeunit::TimeUnit;
use crate::types::{Integer, Natural, Time};
use crate::{fail, require};
struct ReferenceState {
date: Cell<Option<Date>>,
moving: bool,
}
pub struct TermStructureBase {
calendar: Option<Calendar>,
settlement_days: Option<Natural>,
day_counter: Option<DayCounter>,
settings: Option<Shared<Settings<Date>>>,
extrapolation: Cell<bool>,
reference: Shared<ReferenceState>,
observable: Shared<Observable>,
updater: SharedMut<ResetThenNotify>,
}
impl TermStructureBase {
fn assemble(
calendar: Option<Calendar>,
settlement_days: Option<Natural>,
day_counter: Option<DayCounter>,
settings: Option<Shared<Settings<Date>>>,
moving: bool,
reference_date: Option<Date>,
) -> TermStructureBase {
let reference = shared(ReferenceState {
date: Cell::new(reference_date),
moving,
});
let observable = shared(Observable::new());
let updater = ResetThenNotify::broadcasting(Shared::clone(&observable), {
let reference = Shared::clone(&reference);
move || {
if reference.moving {
reference.date.set(None);
}
}
});
TermStructureBase {
calendar,
settlement_days,
day_counter,
settings,
extrapolation: Cell::new(false),
reference,
observable,
updater,
}
}
pub fn new(day_counter: Option<DayCounter>) -> TermStructureBase {
Self::assemble(None, None, day_counter, None, false, None)
}
pub fn with_reference_date(
reference_date: Date,
calendar: Option<Calendar>,
day_counter: Option<DayCounter>,
) -> TermStructureBase {
Self::assemble(
calendar,
None,
day_counter,
None,
false,
Some(reference_date),
)
}
pub fn moving(
settlement_days: Natural,
calendar: Calendar,
day_counter: Option<DayCounter>,
settings: Shared<Settings<Date>>,
) -> TermStructureBase {
let base = Self::assemble(
Some(calendar),
Some(settlement_days),
day_counter,
Some(Shared::clone(&settings)),
true,
None,
);
settings.register_eval_date_observer(&(base.updater.clone() as SharedMut<dyn Observer>));
base
}
pub fn reference_date(&self) -> QlResult<Date> {
if self.reference.moving && self.reference.date.get().is_none() {
let settings = self
.settings
.as_ref()
.expect("a moving term structure holds settings");
let Some(today) = settings.evaluation_date() else {
fail!("no evaluation date set: a moving term structure needs one");
};
require!(
today != Date::null(),
"null evaluation date set for a moving term structure"
);
let days = self
.settlement_days
.expect("a moving term structure holds settlement days");
let Ok(n) = Integer::try_from(days) else {
fail!("settlement days ({days}) overflow Integer");
};
let horizon = (i64::from(days) + 1) * 10;
require!(
i64::from(today.serial_number()) + horizon
<= i64::from(Date::max_date().serial_number()),
"evaluation date ({today}) is too close to the maximum date to advance {days} settlement days"
);
let calendar = self
.calendar
.as_ref()
.expect("a moving term structure holds a calendar");
let advanced = calendar.advance(
today,
n,
TimeUnit::Days,
BusinessDayConvention::Following,
false,
);
self.reference.date.set(Some(advanced));
}
match self.reference.date.get() {
Some(date) if date != Date::null() => Ok(date),
_ => fail!("no reference date provided: construct with one or override reference_date"),
}
}
pub fn day_counter(&self) -> Option<DayCounter> {
self.day_counter.clone()
}
pub fn calendar(&self) -> Option<Calendar> {
self.calendar.clone()
}
pub fn settlement_days(&self) -> QlResult<Natural> {
match self.settlement_days {
Some(days) => Ok(days),
None => fail!("settlement days not provided for this instance"),
}
}
pub fn observable(&self) -> &Observable {
&self.observable
}
pub fn updater(&self) -> SharedMut<dyn Observer> {
self.updater.clone()
}
pub fn allows_extrapolation(&self) -> bool {
self.extrapolation.get()
}
pub fn enable_extrapolation(&self) {
self.extrapolation.set(true);
}
pub fn disable_extrapolation(&self) {
self.extrapolation.set(false);
}
}
pub trait TermStructure: AsObservable {
fn base(&self) -> &TermStructureBase;
fn max_date(&self) -> Date;
fn day_counter(&self) -> Option<DayCounter> {
self.base().day_counter()
}
fn require_day_counter(&self) -> QlResult<DayCounter> {
let Some(day_counter) = self.day_counter() else {
fail!("no day counter provided for this term structure");
};
Ok(day_counter)
}
fn calendar(&self) -> Option<Calendar> {
self.base().calendar()
}
fn settlement_days(&self) -> QlResult<Natural> {
self.base().settlement_days()
}
fn reference_date(&self) -> QlResult<Date> {
self.base().reference_date()
}
fn time_from_reference(&self, date: Date) -> QlResult<Time> {
let day_counter = self.require_day_counter()?;
Ok(day_counter.year_fraction(self.reference_date()?, date))
}
fn max_time(&self) -> QlResult<Time> {
self.time_from_reference(self.max_date())
}
fn allows_extrapolation(&self) -> bool {
self.base().allows_extrapolation()
}
fn enable_extrapolation(&self) {
self.base().enable_extrapolation()
}
fn disable_extrapolation(&self) {
self.base().disable_extrapolation()
}
fn check_range_date(&self, date: Date, extrapolate: bool) -> QlResult<()> {
let reference = self.reference_date()?;
require!(
date >= reference,
"date ({date}) before reference date ({reference})"
);
require!(
extrapolate || self.allows_extrapolation() || date <= self.max_date(),
"date ({date}) is past max curve date ({max})",
max = self.max_date()
);
Ok(())
}
fn check_range_time(&self, t: Time, extrapolate: bool) -> QlResult<()> {
if !t.is_finite() || t < 0.0 {
fail!("negative time ({t}) given");
}
if extrapolate || self.allows_extrapolation() {
return Ok(());
}
let max_time = self.max_time()?;
require!(
t <= max_time || close_enough(t, max_time),
"time ({t}) is past max curve time ({max_time})"
);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::{Flag, as_observer};
use crate::time::calendars::target::Target;
use crate::time::date::Month;
use crate::time::daycounters::actual360::Actual360;
struct TestCurve {
base: TermStructureBase,
max: Date,
}
impl TestCurve {
fn fixed(reference_date: Date) -> TestCurve {
TestCurve {
base: TermStructureBase::with_reference_date(
reference_date,
None,
Some(Actual360::new()),
),
max: reference_date + 360,
}
}
}
impl AsObservable for TestCurve {
fn observable(&self) -> &Observable {
self.base.observable()
}
}
impl TermStructure for TestCurve {
fn base(&self) -> &TermStructureBase {
&self.base
}
fn max_date(&self) -> Date {
self.max
}
}
#[test]
fn fixed_reference_date_is_returned_verbatim() {
let reference = Date::new(15, Month::June, 2026);
let curve = TestCurve::fixed(reference);
assert_eq!(curve.reference_date().unwrap(), reference);
assert!(curve.settlement_days().is_err());
assert!(curve.calendar().is_none());
}
#[test]
fn time_from_reference_uses_the_day_counter() {
let reference = Date::new(15, Month::June, 2026);
let curve = TestCurve::fixed(reference);
let half_year = curve.time_from_reference(reference + 180).unwrap();
assert_eq!(half_year, 0.5);
assert_eq!(curve.max_time().unwrap(), 1.0);
}
#[test]
fn moving_reference_date_advances_off_the_evaluation_date() {
let settings = shared(Settings::new());
settings.set_evaluation_date(Date::new(15, Month::January, 2026));
let curve = TestCurve {
base: TermStructureBase::moving(
2,
Target::new(),
Some(Actual360::new()),
settings.clone(),
),
max: Date::new(15, Month::January, 2027),
};
assert_eq!(
curve.reference_date().unwrap(),
Date::new(19, Month::January, 2026)
);
assert_eq!(curve.settlement_days().unwrap(), 2);
}
#[test]
fn evaluation_date_change_recomputes_reference_and_notifies() {
let settings = shared(Settings::new());
settings.set_evaluation_date(Date::new(15, Month::January, 2026));
let curve = TestCurve {
base: TermStructureBase::moving(
0,
Target::new(),
Some(Actual360::new()),
settings.clone(),
),
max: Date::new(15, Month::January, 2027),
};
assert_eq!(
curve.reference_date().unwrap(),
Date::new(15, Month::January, 2026)
);
let flag = Flag::new();
curve.observable().register_observer(&as_observer(&flag));
settings.set_evaluation_date(Date::new(16, Month::January, 2026));
assert!(Flag::is_up(&flag));
assert_eq!(
curve.reference_date().unwrap(),
Date::new(16, Month::January, 2026)
);
}
#[test]
fn moving_without_evaluation_date_is_an_error() {
let settings = shared(Settings::new());
let base = TermStructureBase::moving(2, Target::new(), Some(Actual360::new()), settings);
let err = base.reference_date().unwrap_err();
assert!(err.message().contains("no evaluation date set"));
}
#[test]
fn null_or_near_max_evaluation_dates_are_errors_not_panics() {
let settings = shared(Settings::new());
let base =
TermStructureBase::moving(2, Target::new(), Some(Actual360::new()), settings.clone());
settings.set_evaluation_date(Date::null());
let err = base.reference_date().unwrap_err();
assert!(err.message().contains("null evaluation date"));
settings.set_evaluation_date(Date::max_date());
let err = base.reference_date().unwrap_err();
assert!(err.message().contains("too close to the maximum date"));
}
#[test]
fn extrapolation_short_circuits_before_max_time_is_computed() {
struct NoDayCounterCurve {
base: TermStructureBase,
}
impl AsObservable for NoDayCounterCurve {
fn observable(&self) -> &Observable {
self.base.observable()
}
}
impl TermStructure for NoDayCounterCurve {
fn base(&self) -> &TermStructureBase {
&self.base
}
fn max_date(&self) -> Date {
Date::max_date()
}
}
let curve = NoDayCounterCurve {
base: TermStructureBase::new(None),
};
assert!(curve.check_range_time(0.5, true).is_ok());
curve.enable_extrapolation();
assert!(curve.check_range_time(0.5, false).is_ok());
curve.disable_extrapolation();
assert!(curve.check_range_time(0.5, false).is_err());
}
#[test]
fn range_check_rejects_non_finite_time_even_with_extrapolation() {
let curve = TestCurve::fixed(Date::new(15, Month::June, 2026));
curve.enable_extrapolation();
assert!(curve.check_range_time(Time::INFINITY, false).is_err());
assert!(curve.check_range_time(Time::NEG_INFINITY, true).is_err());
assert!(curve.check_range_time(Time::NAN, true).is_err());
}
#[test]
fn detached_base_has_no_reference_date() {
let base = TermStructureBase::new(Some(Actual360::new()));
assert!(base.reference_date().is_err());
assert!(base.settlement_days().is_err());
}
#[test]
fn updater_forwards_notifications_without_touching_a_fixed_reference() {
let reference = Date::new(15, Month::June, 2026);
let curve = TestCurve::fixed(reference);
let flag = Flag::new();
curve.observable().register_observer(&as_observer(&flag));
let source = Observable::new();
source.register_observer(&curve.base().updater());
source.notify_observers();
assert!(Flag::is_up(&flag));
assert_eq!(curve.reference_date().unwrap(), reference);
}
#[test]
fn check_range_date_enforces_reference_and_max() {
let reference = Date::new(15, Month::June, 2026);
let curve = TestCurve::fixed(reference);
assert!(curve.check_range_date(reference, false).is_ok());
assert!(curve.check_range_date(curve.max_date(), false).is_ok());
let before = curve.check_range_date(reference - 1, false).unwrap_err();
assert!(before.message().contains("before reference date"));
let past = curve
.check_range_date(curve.max_date() + 1, false)
.unwrap_err();
assert!(past.message().contains("past max curve date"));
assert!(curve.check_range_date(curve.max_date() + 1, true).is_ok());
curve.enable_extrapolation();
assert!(curve.check_range_date(curve.max_date() + 1, false).is_ok());
curve.disable_extrapolation();
assert!(curve.check_range_date(curve.max_date() + 1, false).is_err());
}
#[test]
fn check_range_time_enforces_sign_and_max() {
let curve = TestCurve::fixed(Date::new(15, Month::June, 2026));
assert!(curve.check_range_time(0.0, false).is_ok());
assert!(curve.check_range_time(1.0, false).is_ok());
let negative = curve.check_range_time(-0.1, false).unwrap_err();
assert!(negative.message().contains("negative time"));
assert!(curve.check_range_time(Time::NAN, false).is_err());
let past = curve.check_range_time(1.5, false).unwrap_err();
assert!(past.message().contains("past max curve time"));
assert!(curve.check_range_time(1.5, true).is_ok());
curve.enable_extrapolation();
assert!(curve.check_range_time(1.5, false).is_ok());
}
}