use std::cell::RefCell;
use std::rc::Weak;
use crate::errors::QlResult;
use crate::handle::{Handle, RelinkableHandle};
use crate::math::optimization::endcriteria::EndCriteria;
use crate::patterns::observable::Observer;
use crate::require;
use crate::shared::{Shared, SharedMut, shared, shared_mut};
use crate::termstructures::globalbootstrap::{MultiCurveBootstrap, MultiCurveBootstrapContributor};
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::types::Real;
struct MultiCurveUpdater {
multicurve: Weak<MultiCurve>,
}
impl Observer for MultiCurveUpdater {
fn update(&mut self) {
if let Some(multicurve) = self.multicurve.upgrade() {
multicurve.update();
}
}
fn defer_reentrant_update(&self) -> bool {
false
}
}
pub struct MultiCurve {
bootstrap: Shared<MultiCurveBootstrap>,
curves: RefCell<Vec<Shared<dyn YieldTermStructure>>>,
updater: SharedMut<MultiCurveUpdater>,
}
impl MultiCurve {
pub fn new(accuracy: Real) -> Shared<MultiCurve> {
Self::assemble(MultiCurveBootstrap::new(accuracy))
}
pub fn with_end_criteria(end_criteria: Option<EndCriteria>) -> Shared<MultiCurve> {
Self::assemble(MultiCurveBootstrap::with_end_criteria(end_criteria))
}
fn assemble(bootstrap: MultiCurveBootstrap) -> Shared<MultiCurve> {
let bootstrap = shared(bootstrap);
Shared::new_cyclic(|weak: &Weak<MultiCurve>| MultiCurve {
bootstrap,
curves: RefCell::new(Vec::new()),
updater: shared_mut(MultiCurveUpdater {
multicurve: weak.clone(),
}),
})
}
pub fn add_bootstrapped_curve<C>(
self: &Shared<Self>,
internal: &RelinkableHandle<dyn YieldTermStructure>,
curve: Shared<C>,
) -> QlResult<Handle<dyn YieldTermStructure>>
where
C: MultiCurveBootstrapContributor + YieldTermStructure + 'static,
{
require!(
internal.handle().is_empty(),
"internal handle must be empty; was the curve added already?"
);
self.bootstrap
.add(&(Shared::clone(&curve) as Shared<dyn MultiCurveBootstrapContributor>));
self.add_curve(internal, curve as Shared<dyn YieldTermStructure>)
}
pub fn add_non_bootstrapped_curve(
self: &Shared<Self>,
internal: &RelinkableHandle<dyn YieldTermStructure>,
curve: Shared<dyn YieldTermStructure>,
) -> QlResult<Handle<dyn YieldTermStructure>> {
require!(
internal.handle().is_empty(),
"internal handle must be empty; was the curve added already?"
);
self.bootstrap.add_observer(&curve.updater());
self.add_curve(internal, curve)
}
fn add_curve(
self: &Shared<Self>,
internal: &RelinkableHandle<dyn YieldTermStructure>,
curve: Shared<dyn YieldTermStructure>,
) -> QlResult<Handle<dyn YieldTermStructure>> {
internal.link_to_weak(Shared::downgrade(&curve));
let observer = SharedMut::clone(&self.updater) as SharedMut<dyn Observer>;
curve.register_upstream(&observer);
self.curves.borrow_mut().push(Shared::clone(&curve));
Ok(Handle::new(curve))
}
fn update(&self) {
let members = self.curves.borrow().clone();
for member in members {
let updater = member.updater();
if let Ok(mut observer) = updater.try_borrow_mut() {
observer.update();
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::handle::Handle;
use crate::indexes::IborIndex;
use crate::indexes::ibor::euribor::Euribor;
use crate::indexes::interestrateindex::InterestRateIndex;
use crate::instrument::Instrument;
use crate::instruments::MakeVanillaSwap;
use crate::interestrate::Compounding;
use crate::math::interpolations::loglinear::LogLinear;
use crate::patterns::observable::AsObservable;
use crate::pricingengine::PricingEngine;
use crate::pricingengines::swap::DiscountingSwapEngine;
use crate::quotes::{Quote, SimpleQuote};
use crate::settings::Settings;
use crate::shared::shared_mut;
use crate::termstructures::bootstraphelper::RateHelper;
use crate::termstructures::bootstraptraits::Discount;
use crate::termstructures::globalbootstrap::GlobalBootstrap;
use crate::termstructures::yields::{
DepositRateHelper, PiecewiseYieldCurve, Pillar, SwapRateHelper, ZeroSpreadedTermStructure,
};
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::calendars::target::Target;
use crate::time::date::{Date, Month};
use crate::time::daycounters::actual360::Actual360;
use crate::time::daycounters::actual365fixed::Actual365Fixed;
use crate::time::daycounters::thirty360::{Convention, Thirty360};
use crate::time::frequency::Frequency;
use crate::time::period::Period;
use crate::time::timeunit::TimeUnit;
type BootCurve = PiecewiseYieldCurve<Discount, LogLinear, GlobalBootstrap>;
#[derive(Default)]
struct Flag {
fired: bool,
}
impl Observer for Flag {
fn update(&mut self) {
self.fired = true;
}
}
fn env() -> (Shared<Settings<Date>>, Date, IborIndex) {
let settings = shared(Settings::<Date>::new());
let today = Date::new(13, Month::December, 2019);
settings.set_evaluation_date(today);
let index = Euribor::six_months(Handle::empty(), Shared::clone(&settings));
(settings, today, index)
}
fn deposit_curve(reference_date: Date, index: &IborIndex, rate: Real) -> Shared<BootCurve> {
let helper = DepositRateHelper::from_rate(rate, index) as Shared<dyn RateHelper>;
PiecewiseYieldCurve::with_bootstrap(
reference_date,
vec![helper],
Actual365Fixed::new(),
LogLinear,
GlobalBootstrap::default(),
)
.expect("the single-deposit strip builds a curve")
}
fn flag_observer(flag: &SharedMut<Flag>) -> SharedMut<dyn Observer> {
SharedMut::clone(flag) as SharedMut<dyn Observer>
}
#[test]
fn adds_bootstrapped_and_non_bootstrapped_curves() {
let (_settings, reference_date, index) = env();
let multicurve = MultiCurve::new(1.0e-10);
let internal_boot = RelinkableHandle::<dyn YieldTermStructure>::empty();
let boot = deposit_curve(reference_date, &index, 0.02);
let boot_dyn = Shared::clone(&boot) as Shared<dyn YieldTermStructure>;
let external_boot = multicurve
.add_bootstrapped_curve(&internal_boot, Shared::clone(&boot))
.expect("a bootstrapped curve is added");
let internal_non = RelinkableHandle::<dyn YieldTermStructure>::empty();
let non_dyn: Shared<dyn YieldTermStructure> = deposit_curve(reference_date, &index, 0.025);
let external_non = multicurve
.add_non_bootstrapped_curve(&internal_non, Shared::clone(&non_dyn))
.expect("a non-bootstrapped curve is added");
assert!(!internal_boot.handle().is_empty());
assert!(!internal_non.handle().is_empty());
assert!(Shared::ptr_eq(
&internal_boot
.handle()
.current_link()
.expect("the internal handle resolves the curve"),
&boot_dyn
));
assert!(Shared::ptr_eq(
&internal_non
.handle()
.current_link()
.expect("the internal handle resolves the curve"),
&non_dyn
));
assert_eq!(
multicurve.bootstrap.contributor_count(),
1,
"only the bootstrapped curve is a contributor"
);
assert_eq!(
multicurve.bootstrap.observer_count(),
1,
"only the non-bootstrapped curve is an observer"
);
assert!(Shared::ptr_eq(
&external_boot
.current_link()
.expect("the external handle owns its curve"),
&boot_dyn
));
assert!(Shared::ptr_eq(
&external_non
.current_link()
.expect("the external handle owns its curve"),
&non_dyn
));
}
#[test]
fn rejects_a_reused_internal_handle() {
let (_settings, reference_date, index) = env();
let multicurve = MultiCurve::new(1.0e-10);
let internal = RelinkableHandle::<dyn YieldTermStructure>::empty();
let first = deposit_curve(reference_date, &index, 0.02);
multicurve
.add_bootstrapped_curve(&internal, first)
.expect("the first add links the handle");
let second = deposit_curve(reference_date, &index, 0.03);
assert!(
multicurve
.add_bootstrapped_curve(&internal, second)
.is_err(),
"a second add on a linked handle must be rejected"
);
}
#[test]
fn dropping_the_multicurve_surfaces_a_dropped_contributor() {
let (_settings, reference_date, index) = env();
let multicurve = MultiCurve::new(1.0e-10);
let internal_a = RelinkableHandle::<dyn YieldTermStructure>::empty();
let a = deposit_curve(reference_date, &index, 0.02);
let external_a = multicurve
.add_bootstrapped_curve(&internal_a, Shared::clone(&a))
.expect("curve A is added");
let internal_b = RelinkableHandle::<dyn YieldTermStructure>::empty();
let b = deposit_curve(reference_date, &index, 0.03);
multicurve
.add_bootstrapped_curve(&internal_b, Shared::clone(&b))
.expect("curve B is added");
drop(b);
drop(multicurve);
assert!(
internal_b.handle().current_link().is_err(),
"the weak internal handle did not keep the dropped contributor alive"
);
let error = a
.calculate()
.expect_err("a dropped contributor must surface as an error");
assert!(
format!("{error}").contains("dropped"),
"the error must name the dropped contributor: {error}"
);
drop(external_a);
}
#[test]
fn update_fans_out_to_member_curves() {
let (_settings, reference_date, index) = env();
let multicurve = MultiCurve::new(1.0e-10);
let helper_a = DepositRateHelper::from_rate(0.02, &index) as Shared<dyn RateHelper>;
let a = PiecewiseYieldCurve::<Discount, LogLinear, GlobalBootstrap>::with_bootstrap(
reference_date,
vec![Shared::clone(&helper_a)],
Actual365Fixed::new(),
LogLinear,
GlobalBootstrap::default(),
)
.expect("curve A builds");
let internal_a = RelinkableHandle::<dyn YieldTermStructure>::empty();
multicurve
.add_bootstrapped_curve(&internal_a, Shared::clone(&a))
.expect("curve A is added");
let internal_b = RelinkableHandle::<dyn YieldTermStructure>::empty();
let b = deposit_curve(reference_date, &index, 0.03);
multicurve
.add_bootstrapped_curve(&internal_b, Shared::clone(&b))
.expect("curve B is added");
let flag = shared_mut(Flag::default());
b.observable().register_observer(&flag_observer(&flag));
helper_a.observable().notify_observers();
assert!(
flag.borrow().fired,
"MultiCurve::update did not fan an upstream change on A out to B"
);
}
#[test]
fn the_dependency_cycle_solves_without_re_entering_the_bootstrap() {
let settings = shared(Settings::<Date>::new());
let today = Date::new(23, Month::October, 2025);
settings.set_evaluation_date(today);
let intcurveois = RelinkableHandle::<dyn YieldTermStructure>::empty();
let intcurve3m = RelinkableHandle::<dyn YieldTermStructure>::empty();
let euribor3m = Euribor::three_months(intcurve3m.handle(), Shared::clone(&settings));
let q = Handle::new(shared(SimpleQuote::new(0.03)) as Shared<dyn Quote>);
let mut helpers3m: Vec<Shared<dyn RateHelper>> = Vec::new();
for i in 1..=5i32 {
helpers3m.push(SwapRateHelper::with_details(
q.clone(),
Period::new(i, TimeUnit::Years),
Target::new(),
Frequency::Annual,
BusinessDayConvention::Following,
Thirty360::with_convention(Convention::BondBasis),
&euribor3m,
Handle::empty(),
Period::new(0, TimeUnit::Days),
Some(intcurveois.handle()),
Pillar::LastRelevantDate,
) as Shared<dyn RateHelper>);
}
let ptr3m = PiecewiseYieldCurve::<Discount, LogLinear, GlobalBootstrap>::with_bootstrap(
today,
helpers3m,
Actual360::new(),
LogLinear,
GlobalBootstrap::new(Some(1.0e-10), None, Vec::new()),
)
.expect("the 3m curve builds");
let multicurve = MultiCurve::new(1.0e-10);
let curve3m = multicurve
.add_bootstrapped_curve(&intcurve3m, ptr3m)
.expect("adds the 3m contributor");
let b = Handle::new(shared(SimpleQuote::new(-0.01)) as Shared<dyn Quote>);
let ptrois = shared(ZeroSpreadedTermStructure::new(intcurve3m.handle(), b));
let curveois = multicurve
.add_non_bootstrapped_curve(
&intcurveois,
Shared::clone(&ptrois) as Shared<dyn YieldTermStructure>,
)
.expect("adds the ois member");
let ois_rate = curveois
.current_link()
.expect("the ois handle resolves")
.zero_rate(1.0, Compounding::Continuous, Frequency::NoFrequency, false)
.expect("the ois zero rate solves without re-entering the bootstrap")
.rate();
let rate3m = curve3m
.current_link()
.expect("the 3m handle resolves")
.zero_rate(1.0, Compounding::Continuous, Frequency::NoFrequency, false)
.expect("the 3m zero rate solves")
.rate();
assert!(
(ois_rate - rate3m - (-0.01)).abs() < 1.0e-10,
"spread {} is not the -0.01 the ZeroSpreadedTermStructure adds",
ois_rate - rate3m
);
}
#[test]
fn the_internal_weak_handle_does_not_forward() {
let (_settings, reference_date, index) = env();
let multicurve = MultiCurve::new(1.0e-10);
let internal_a = RelinkableHandle::<dyn YieldTermStructure>::empty();
let a = deposit_curve(reference_date, &index, 0.02);
multicurve
.add_bootstrapped_curve(&internal_a, Shared::clone(&a))
.expect("curve A is added");
let weak_flag = shared_mut(Flag::default());
internal_a
.handle()
.register_observer(&flag_observer(&weak_flag));
let owning = RelinkableHandle::new(Shared::clone(&a) as Shared<dyn YieldTermStructure>);
let owning_flag = shared_mut(Flag::default());
owning
.handle()
.register_observer(&flag_observer(&owning_flag));
a.observable().notify_observers();
assert!(
!weak_flag.borrow().fired,
"the weak internal handle forwarded a notification it must not"
);
assert!(
owning_flag.borrow().fired,
"an owning handle on the same curve did not forward the notification"
);
}
fn swap_npv(
i: i32,
fixed_rate: Real,
euribor3m: &Shared<IborIndex>,
settings: &Shared<Settings<Date>>,
discount: &Handle<dyn YieldTermStructure>,
) -> Real {
let mut swap = MakeVanillaSwap::new(
Period::new(i, TimeUnit::Years),
Shared::clone(euribor3m),
Some(fixed_rate),
Period::new(0, TimeUnit::Days),
Shared::clone(settings),
)
.with_settlement_days(euribor3m.fixing_days())
.with_fixed_leg_day_count(Thirty360::with_convention(Convention::BondBasis))
.with_fixed_leg_tenor(Period::new(1, TimeUnit::Years))
.with_fixed_leg_convention(BusinessDayConvention::Following)
.with_fixed_leg_termination_date_convention(BusinessDayConvention::Following)
.build()
.expect("the swap builds");
let engine = shared_mut(DiscountingSwapEngine::new(
discount.clone(),
None,
None,
None,
Shared::clone(settings),
));
swap.base_mut()
.set_pricing_engine(engine as SharedMut<dyn PricingEngine>);
swap.npv().expect("the swap prices off the ois curve")
}
#[test]
fn multicurve_piecewise_and_spreaded_curve_self_reprice() {
let calendar = Target::new();
let settings = shared(Settings::<Date>::new());
let today = calendar.adjust(
Date::new(23, Month::October, 2025),
BusinessDayConvention::Following,
);
settings.set_evaluation_date(today);
let intcurveois = RelinkableHandle::<dyn YieldTermStructure>::empty();
let intcurve3m = RelinkableHandle::<dyn YieldTermStructure>::empty();
let euribor3m = shared(Euribor::three_months(
intcurve3m.handle(),
Shared::clone(&settings),
));
let q = shared(SimpleQuote::new(0.03));
let q_handle = Handle::new(Shared::clone(&q) as Shared<dyn Quote>);
let b = shared(SimpleQuote::new(-0.01));
let b_handle = Handle::new(Shared::clone(&b) as Shared<dyn Quote>);
let mut helpers3m: Vec<Shared<dyn RateHelper>> = Vec::new();
for i in 1..=10i32 {
helpers3m.push(SwapRateHelper::with_details(
q_handle.clone(),
Period::new(i, TimeUnit::Years),
calendar.clone(),
Frequency::Annual,
BusinessDayConvention::Following,
Thirty360::with_convention(Convention::BondBasis),
&euribor3m,
Handle::empty(),
Period::new(0, TimeUnit::Days),
Some(intcurveois.handle()),
Pillar::LastRelevantDate,
) as Shared<dyn RateHelper>);
}
let ptr3m = PiecewiseYieldCurve::<Discount, LogLinear, GlobalBootstrap>::with_bootstrap(
today,
helpers3m,
Actual360::new(),
LogLinear,
GlobalBootstrap::new(Some(1.0e-10), None, Vec::new()),
)
.expect("the 3m curve builds");
let multicurve = MultiCurve::new(1.0e-10);
let curve3m = multicurve
.add_bootstrapped_curve(&intcurve3m, ptr3m)
.expect("adds the 3m contributor");
let ptrois = shared(ZeroSpreadedTermStructure::new(
intcurve3m.handle(),
b_handle,
));
let curveois = multicurve
.add_non_bootstrapped_curve(
&intcurveois,
Shared::clone(&ptrois) as Shared<dyn YieldTermStructure>,
)
.expect("adds the ois member");
let zero_rate = |handle: &Handle<dyn YieldTermStructure>| -> Real {
handle
.current_link()
.expect("the handle resolves")
.zero_rate(1.0, Compounding::Continuous, Frequency::NoFrequency, false)
.expect("the zero rate solves")
.rate()
};
assert!(
(zero_rate(&curveois) - zero_rate(&curve3m) - (-0.01)).abs() < 1.0e-10,
"the ois-3m spread is not the -0.01 the ZeroSpreadedTermStructure adds"
);
for i in 1..=10i32 {
let npv = swap_npv(i, 0.03, &euribor3m, &settings, &curveois);
assert!(
npv.abs() < 1.0e-10,
"swap {i} does not reprice to zero: {npv}"
);
}
let flag = shared_mut(Flag::default());
ptrois.observable().register_observer(&flag_observer(&flag));
let pre = zero_rate(&curve3m);
q.set_value(0.035);
let post = zero_rate(&curve3m);
assert!(
(post - pre).abs() > 1.0e-10,
"the q-bump fan-out did not re-solve the 3m curve: {pre} -> {post}"
);
assert!(
flag.borrow().fired,
"the q-bump did not fan out to the spreaded ois member"
);
for i in 1..=10i32 {
let npv = swap_npv(i, 0.035, &euribor3m, &settings, &curveois);
assert!(
npv.abs() < 1.0e-10,
"rebuilt swap {i} does not reprice to zero at the bumped quote: {npv}"
);
}
b.set_value(-0.005);
assert!(
(zero_rate(&curveois) - zero_rate(&curve3m) - (-0.005)).abs() < 1.0e-10,
"the bumped ois-3m spread is not the new -0.005"
);
}
}