use std::cell::RefCell;
use std::marker::PhantomData;
use std::rc::Weak;
use crate::errors::QlResult;
use crate::math::interpolations::Interpolator;
use crate::patterns::lazyobject::LazyObject;
use crate::patterns::observable::{AsObservable, Observable, Observer};
use crate::require;
use crate::shared::{Shared, SharedMut, shared_mut};
use crate::termstructures::bootstraphelper::RateHelper;
use crate::termstructures::bootstraptraits::{BootstrapTraits, CurveData};
use crate::termstructures::iterativebootstrap::{IterativeBootstrap, PiecewiseCurve};
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::termstructures::{TermStructure, TermStructureBase};
use crate::time::date::Date;
use crate::time::daycounter::DayCounter;
use crate::types::{DiscountFactor, Real, Time};
struct CurveUpdater {
lazy: SharedMut<LazyObject>,
}
impl Observer for CurveUpdater {
fn update(&mut self) {
if let Some(update) = LazyObject::deferred_update(&self.lazy) {
update.notify_observers();
}
}
}
pub struct PiecewiseYieldCurve<T: BootstrapTraits, I: Interpolator> {
base: TermStructureBase,
instruments: Vec<Shared<dyn RateHelper>>,
interpolator: I,
data: RefCell<CurveData<I>>,
lazy: SharedMut<LazyObject>,
observable: Shared<Observable>,
updater: SharedMut<CurveUpdater>,
bootstrap: IterativeBootstrap,
accuracy: Real,
self_weak: Weak<dyn YieldTermStructure>,
_traits: PhantomData<fn() -> T>,
}
impl<T: BootstrapTraits + 'static, I: Interpolator + 'static> PiecewiseYieldCurve<T, I> {
pub fn new(
reference_date: Date,
instruments: Vec<Shared<dyn RateHelper>>,
day_counter: DayCounter,
interpolator: I,
) -> QlResult<Shared<PiecewiseYieldCurve<T, I>>> {
require!(!instruments.is_empty(), "no bootstrap helpers given");
let curve = Shared::new_cyclic(|weak: &Weak<PiecewiseYieldCurve<T, I>>| {
let self_weak: Weak<dyn YieldTermStructure> = weak.clone();
let lazy = shared_mut(LazyObject::new(true));
let observable = lazy.borrow().observable_handle();
let updater = shared_mut(CurveUpdater {
lazy: SharedMut::clone(&lazy),
});
PiecewiseYieldCurve {
base: TermStructureBase::with_reference_date(
reference_date,
None,
Some(day_counter),
),
instruments,
interpolator,
data: RefCell::new(CurveData::new()),
lazy,
observable,
updater,
bootstrap: IterativeBootstrap::new(),
accuracy: 1.0e-12,
self_weak,
_traits: PhantomData,
}
});
let observer = SharedMut::clone(&curve.updater) as SharedMut<dyn Observer>;
for helper in &curve.instruments {
helper.observable().register_observer(&observer);
}
Ok(curve)
}
pub fn calculate(&self) -> QlResult<()> {
if self.lazy.borrow().is_calculated() {
return Ok(());
}
if !self.lazy.borrow_mut().start_calculation() {
return Ok(());
}
let result = self.bootstrap.calculate(self);
self.lazy.borrow_mut().finish_calculation(&result);
result
}
pub fn times(&self) -> QlResult<Vec<Time>> {
self.calculate()?;
Ok(self.data.borrow().times().to_vec())
}
pub fn dates(&self) -> QlResult<Vec<Date>> {
self.calculate()?;
Ok(self.data.borrow().dates().to_vec())
}
pub fn data(&self) -> QlResult<Vec<Real>> {
self.calculate()?;
Ok(self.data.borrow().data().to_vec())
}
pub fn nodes(&self) -> QlResult<Vec<(Date, Real)>> {
self.calculate()?;
Ok(self.data.borrow().nodes())
}
pub fn register_observer(&self, observer: &SharedMut<dyn Observer>) -> bool {
self.observable.register_observer(observer)
}
}
impl<T: BootstrapTraits, I: Interpolator> AsObservable for PiecewiseYieldCurve<T, I> {
fn observable(&self) -> &Observable {
&self.observable
}
}
impl<T: BootstrapTraits + 'static, I: Interpolator + 'static> TermStructure
for PiecewiseYieldCurve<T, I>
{
fn base(&self) -> &TermStructureBase {
&self.base
}
fn max_date(&self) -> Date {
let _ = self.calculate();
self.data
.borrow()
.max_date()
.or_else(|| self.base.reference_date().ok())
.unwrap_or_else(Date::null)
}
}
impl<T: BootstrapTraits + 'static, I: Interpolator + 'static> YieldTermStructure
for PiecewiseYieldCurve<T, I>
{
fn discount(&self, t: Time, extrapolate: bool) -> QlResult<DiscountFactor> {
self.calculate()?;
self.check_range_time(t, extrapolate)?;
self.discount_impl(t)
}
fn discount_impl(&self, t: Time) -> QlResult<DiscountFactor> {
self.data.borrow().discount(t)
}
}
impl<T: BootstrapTraits + 'static, I: Interpolator + 'static> PiecewiseCurve
for PiecewiseYieldCurve<T, I>
{
type Traits = T;
type Interp = I;
fn instruments(&self) -> &[Shared<dyn RateHelper>] {
&self.instruments
}
fn interpolator(&self) -> &I {
&self.interpolator
}
fn curve_data(&self) -> &RefCell<CurveData<I>> {
&self.data
}
fn accuracy(&self) -> Real {
self.accuracy
}
fn reference_date(&self) -> QlResult<Date> {
self.base.reference_date()
}
fn time_from_reference(&self, date: Date) -> QlResult<Time> {
TermStructure::time_from_reference(self, date)
}
fn term_structure_shared(&self) -> QlResult<Shared<dyn YieldTermStructure>> {
match self.self_weak.upgrade() {
Some(curve) => Ok(curve),
None => crate::fail!("curve dropped before bootstrap"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::handle::Handle;
use crate::indexes::ibor::euribor::Euribor;
use crate::indexes::iborindex::IborIndex;
use crate::indexes::index::Index;
use crate::instruments::MakeVanillaSwap;
use crate::math::interpolations::linear::Linear;
use crate::math::interpolations::loglinear::LogLinear;
use crate::quotes::{Quote, SimpleQuote};
use crate::settings::Settings;
use crate::shared::shared;
use crate::termstructures::bootstraptraits::Discount;
use crate::termstructures::yields::{DepositRateHelper, SwapRateHelper};
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::calendars::target::Target;
use crate::time::date::Month;
use crate::time::daycounters::actual360::Actual360;
use crate::time::daycounters::thirty360::{Convention, Thirty360};
use crate::time::frequency::Frequency;
use crate::time::period::Period;
use crate::time::timeunit::TimeUnit;
use crate::types::Rate;
const DEPOSIT_DATA: [(i32, TimeUnit, Rate); 6] = [
(1, TimeUnit::Weeks, 4.559),
(1, TimeUnit::Months, 4.581),
(2, TimeUnit::Months, 4.573),
(3, TimeUnit::Months, 4.557),
(6, TimeUnit::Months, 4.496),
(9, TimeUnit::Months, 4.490),
];
const SWAP_DATA: [(i32, TimeUnit, Rate); 15] = [
(1, TimeUnit::Years, 4.54),
(2, TimeUnit::Years, 4.63),
(3, TimeUnit::Years, 4.75),
(4, TimeUnit::Years, 4.86),
(5, TimeUnit::Years, 4.99),
(6, TimeUnit::Years, 5.11),
(7, TimeUnit::Years, 5.23),
(8, TimeUnit::Years, 5.33),
(9, TimeUnit::Years, 5.41),
(10, TimeUnit::Years, 5.47),
(12, TimeUnit::Years, 5.60),
(15, TimeUnit::Years, 5.75),
(20, TimeUnit::Years, 5.89),
(25, TimeUnit::Years, 5.95),
(30, TimeUnit::Years, 5.96),
];
const TOLERANCE: Real = 1.0e-9;
struct CommonVars {
settings: Shared<Settings<Date>>,
today: Date,
settlement: Date,
instruments: Vec<Shared<dyn RateHelper>>,
}
fn common_vars() -> CommonVars {
let calendar = Target::new();
let today = calendar.adjust(
Date::new(15, Month::June, 2026),
BusinessDayConvention::Following,
);
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(today);
let settlement = calendar.advance(
today,
2,
TimeUnit::Days,
BusinessDayConvention::Following,
false,
);
let mut instruments: Vec<Shared<dyn RateHelper>> = Vec::new();
for (n, units, rate) in DEPOSIT_DATA {
let quote = Handle::new(shared(SimpleQuote::new(rate / 100.0)) as Shared<dyn Quote>);
let index = Euribor::new(Period::new(n, units), Handle::empty(), settings.clone())
.expect("deposit tenor is valid");
instruments.push(DepositRateHelper::new(quote, &index) as Shared<dyn RateHelper>);
}
for (n, units, rate) in SWAP_DATA {
let quote = Handle::new(shared(SimpleQuote::new(rate / 100.0)) as Shared<dyn Quote>);
let euribor6m = Euribor::six_months(Handle::empty(), settings.clone());
instruments.push(SwapRateHelper::new(
quote,
Period::new(n, units),
calendar.clone(),
Frequency::Annual,
BusinessDayConvention::Unadjusted,
Thirty360::with_convention(Convention::BondBasis),
&euribor6m,
) as Shared<dyn RateHelper>);
}
CommonVars {
settings,
today,
settlement,
instruments,
}
}
fn euribor6m_on(
handle: Handle<dyn YieldTermStructure>,
settings: Shared<Settings<Date>>,
) -> Shared<IborIndex> {
shared(Euribor::six_months(handle, settings))
}
fn check_curve_consistency<I: Interpolator + Default + 'static>() {
let vars = common_vars();
let curve = PiecewiseYieldCurve::<Discount, I>::new(
vars.settlement,
vars.instruments.clone(),
Actual360::new(),
I::default(),
)
.unwrap();
let handle: Handle<dyn YieldTermStructure> =
Handle::new(Shared::clone(&curve) as Shared<dyn YieldTermStructure>);
for (n, units, rate) in DEPOSIT_DATA {
let index = Euribor::new(Period::new(n, units), handle.clone(), vars.settings.clone())
.expect("deposit tenor is valid");
let estimated = index.fixing(vars.today, false).unwrap();
let expected = rate / 100.0;
assert!(
(estimated - expected).abs() <= TOLERANCE,
"{n} {units:?} deposit: estimated {estimated} vs expected {expected}"
);
}
let euribor6m = euribor6m_on(handle.clone(), vars.settings.clone());
for (n, units, rate) in SWAP_DATA {
let mut swap = MakeVanillaSwap::new(
Period::new(n, units),
Shared::clone(&euribor6m),
Some(0.0),
Period::new(0, TimeUnit::Days),
vars.settings.clone(),
)
.with_effective_date(vars.settlement)
.with_discounting_term_structure(handle.clone())
.with_fixed_leg_day_count(Thirty360::with_convention(Convention::BondBasis))
.with_fixed_leg_tenor(Period::try_from(Frequency::Annual).unwrap())
.with_fixed_leg_convention(BusinessDayConvention::Unadjusted)
.with_fixed_leg_termination_date_convention(BusinessDayConvention::Unadjusted)
.build()
.unwrap();
let estimated = swap.fixed_vs_floating_mut().fair_rate().unwrap();
let expected = rate / 100.0;
assert!(
(estimated - expected).abs() <= TOLERANCE,
"{n} {units:?} swap: estimated {estimated} vs expected {expected}"
);
}
}
#[test]
fn log_linear_discount_consistency() {
check_curve_consistency::<LogLinear>();
}
#[test]
fn linear_discount_consistency() {
check_curve_consistency::<Linear>();
}
#[test]
fn bootstrap_is_lazy_and_reruns_on_quote_change() {
let calendar = Target::new();
let today = calendar.adjust(
Date::new(15, Month::June, 2026),
BusinessDayConvention::Following,
);
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(today);
let settlement = calendar.advance(
today,
2,
TimeUnit::Days,
BusinessDayConvention::Following,
false,
);
let quote = shared(SimpleQuote::new(0.04557));
let index = Euribor::new(
Period::new(3, TimeUnit::Months),
Handle::empty(),
settings.clone(),
)
.unwrap();
let helper = DepositRateHelper::new(
Handle::new(Shared::clone("e) as Shared<dyn Quote>),
&index,
);
let curve = PiecewiseYieldCurve::<Discount, LogLinear>::new(
settlement,
vec![Shared::clone(&helper) as Shared<dyn RateHelper>],
Actual360::new(),
LogLinear,
)
.unwrap();
assert!(!curve.lazy.borrow().is_calculated());
let df1 = curve.discount_date(helper.maturity_date(), false).unwrap();
assert!(curve.lazy.borrow().is_calculated());
assert!(df1 < 1.0 && df1 > 0.0);
quote.set_value(0.06);
assert!(!curve.lazy.borrow().is_calculated());
let df2 = curve.discount_date(helper.maturity_date(), false).unwrap();
assert!(
df2 < df1,
"a higher deposit rate discounts more: {df2} vs {df1}"
);
}
}