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::cashflows::IborLeg;
use crate::handle::Handle;
use crate::indexes::IborIndex;
use crate::indexes::ibor::euribor::Euribor;
use crate::indexes::index::Index;
use crate::indexes::interestrateindex::InterestRateIndex;
use crate::instrument::Instrument;
use crate::instruments::{ForwardRateAgreement, MakeVanillaSwap, Swap};
use crate::interestrate::Compounding;
use crate::math::interpolations::loglinear::LogLinear;
use crate::patterns::observable::AsObservable;
use crate::position::Position;
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, FlatForward, FraRateHelper, IborIborBasisSwapRateHelper,
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::schedule::MakeSchedule;
use crate::time::timeunit::TimeUnit;
use crate::types::Integer;
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 a_date_roll_on_a_moving_member_fans_out() {
let (settings, today, index) = env();
let fra = |start: Date, end: Date| {
let quote = Handle::new(shared(SimpleQuote::new(0.02)) as Shared<dyn Quote>);
FraRateHelper::from_dates(quote, start, end, &index, false, Pillar::MaturityDate)
as Shared<dyn RateHelper>
};
let multicurve = MultiCurve::new(1.0e-10);
let a = BootCurve::moving_with_bootstrap(
2,
Target::new(),
vec![fra(
Date::new(15, Month::June, 2020),
Date::new(15, Month::September, 2020),
)],
Actual365Fixed::new(),
LogLinear,
Shared::clone(&settings),
GlobalBootstrap::default(),
)
.expect("moving curve A builds");
let internal_a = RelinkableHandle::<dyn YieldTermStructure>::empty();
multicurve
.add_bootstrapped_curve(&internal_a, a)
.expect("curve A is added");
let b = BootCurve::with_bootstrap(
today,
vec![fra(
Date::new(15, Month::June, 2020),
Date::new(15, Month::December, 2020),
)],
Actual365Fixed::new(),
LogLinear,
GlobalBootstrap::default(),
)
.expect("fixed curve B builds");
let internal_b = RelinkableHandle::<dyn YieldTermStructure>::empty();
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));
settings.set_evaluation_date(today + 21);
assert!(
flag.borrow().fired,
"a date roll on moving member A did not reach member B"
);
}
#[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"
);
}
fn basis_swap_npv(
maturity: Date,
basis: Real,
euribor3m: &Shared<IborIndex>,
euribor6m: &Shared<IborIndex>,
settings: &Shared<Settings<Date>>,
discount: &Handle<dyn YieldTermStructure>,
) -> Real {
let start = euribor3m.fixing_calendar().advance(
settings
.evaluation_date()
.expect("the evaluation date is set"),
euribor3m.fixing_days() as Integer,
TimeUnit::Days,
BusinessDayConvention::Following,
false,
);
let leg = |index: &Shared<IborIndex>, spread: Real| {
let schedule = MakeSchedule::new()
.from(start)
.to(maturity)
.with_tenor(index.tenor())
.with_calendar(index.fixing_calendar())
.with_convention(index.business_day_convention())
.end_of_month(index.end_of_month())
.forwards()
.build();
IborLeg::new(schedule, Shared::clone(index))
.with_spread(spread)
.with_notional(1.0)
.build()
.expect("the basis-swap leg builds")
};
let mut swap = Swap::two_leg(
leg(euribor3m, basis),
leg(euribor6m, 0.0),
Shared::clone(settings),
);
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 basis swap prices")
}
#[test]
fn multicurve_two_piecewise_yield_curves_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 settlement = calendar.advance(
today,
2,
TimeUnit::Days,
BusinessDayConvention::Following,
false,
);
let accuracy = 1.0e-10;
let discount = Handle::new(shared(FlatForward::with_rate(
settlement,
0.02,
Actual360::new(),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>);
let intcurve3m = RelinkableHandle::<dyn YieldTermStructure>::empty();
let intcurve6m = RelinkableHandle::<dyn YieldTermStructure>::empty();
let euribor3m = shared(Euribor::three_months(
intcurve3m.handle(),
Shared::clone(&settings),
));
let euribor6m = shared(Euribor::six_months(
intcurve6m.handle(),
Shared::clone(&settings),
));
let q = Handle::new(shared(SimpleQuote::new(0.03)) as Shared<dyn Quote>);
let b = Handle::new(shared(SimpleQuote::new(0.0020)) as Shared<dyn Quote>);
let basis_helper = |tenor: Period, bootstrap_base_curve: bool| {
IborIborBasisSwapRateHelper::new(
b.clone(),
tenor,
euribor3m.fixing_days(),
euribor3m.fixing_calendar(),
euribor3m.business_day_convention(),
euribor3m.end_of_month(),
&euribor3m,
&euribor6m,
discount.clone(),
bootstrap_base_curve,
) as Shared<dyn RateHelper>
};
let mut helpers3m: Vec<Shared<dyn RateHelper>> = Vec::new();
for i in 1..=9u32 {
helpers3m.push(FraRateHelper::from_months(
q.clone(),
i,
&euribor3m,
true,
Pillar::LastRelevantDate,
) as Shared<dyn RateHelper>);
}
for i in 2..=10i32 {
helpers3m.push(basis_helper(Period::new(i, TimeUnit::Years), true));
}
let mut helpers6m: Vec<Shared<dyn RateHelper>> = Vec::new();
for i in 1..=3i32 {
helpers6m.push(basis_helper(Period::new(i * 6, TimeUnit::Months), false));
}
for i in 2..=10i32 {
helpers6m.push(SwapRateHelper::with_details(
q.clone(),
Period::new(i, TimeUnit::Years),
euribor6m.fixing_calendar(),
Frequency::Annual,
BusinessDayConvention::Following,
Thirty360::with_convention(Convention::BondBasis),
&euribor6m,
Handle::empty(),
Period::new(0, TimeUnit::Days),
Some(discount.clone()),
Pillar::LastRelevantDate,
) as Shared<dyn RateHelper>);
}
let build = |helpers: Vec<Shared<dyn RateHelper>>| {
PiecewiseYieldCurve::<Discount, LogLinear, GlobalBootstrap>::with_bootstrap(
today,
helpers,
Actual360::new(),
LogLinear,
GlobalBootstrap::new(Some(accuracy), None, Vec::new()),
)
.expect("the curve builds")
};
let ptr3m = build(helpers3m);
let ptr6m = build(helpers6m);
let multicurve = MultiCurve::new(accuracy);
let curve3m = multicurve
.add_bootstrapped_curve(&intcurve3m, ptr3m)
.expect("adds the 3m contributor");
let _curve6m = multicurve
.add_bootstrapped_curve(&intcurve6m, ptr6m)
.expect("adds the 6m contributor");
let tolerance = 1.0e-10;
let spot = euribor3m.fixing_calendar().advance(
today,
euribor3m.fixing_days() as Integer,
TimeUnit::Days,
BusinessDayConvention::Following,
false,
);
for i in 1..=9i32 {
let start = euribor3m.fixing_calendar().advance(
spot,
i,
TimeUnit::Months,
euribor3m.business_day_convention(),
euribor3m.end_of_month(),
);
let mut fra = ForwardRateAgreement::new(
Shared::clone(&euribor3m),
start,
Position::Long,
0.03,
1.0,
curve3m.clone(),
)
.expect("the FRA builds");
let rate = fra.forward_rate().expect("the FRA forwards").rate();
assert!(
(rate - 0.03).abs() <= 0.03 * tolerance * 1.0e-2,
"FRA {i}: forward {rate} is not the 3% quote"
);
}
for i in 2..=10i32 {
let maturity = euribor3m.fixing_calendar().advance_by_period(
spot,
Period::new(i, TimeUnit::Years),
euribor3m.business_day_convention(),
false,
);
let npv = basis_swap_npv(
maturity, 0.0020, &euribor3m, &euribor6m, &settings, &discount,
);
assert!(npv.abs() < tolerance, "{i}y basis swap NPV {npv}");
}
for i in 1..=3i32 {
let maturity = euribor3m.fixing_calendar().advance_by_period(
spot,
Period::new(i * 6, TimeUnit::Months),
euribor3m.business_day_convention(),
false,
);
let npv = basis_swap_npv(
maturity, 0.0020, &euribor3m, &euribor6m, &settings, &discount,
);
assert!(npv.abs() < tolerance, "{}m basis swap NPV {npv}", i * 6);
}
for i in 2..=10i32 {
let npv = swap_npv(i, 0.03, &euribor6m, &settings, &discount);
assert!(npv.abs() < tolerance, "{i}y vanilla swap NPV {npv}");
}
}
}