use std::cell::RefCell;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::rc::Weak;
use crate::cashflow::CashFlow;
use crate::cashflows::IborLeg;
use crate::errors::{QlError, QlResult};
use crate::event::Event;
use crate::handle::{Handle, RelinkableHandle};
use crate::indexes::iborindex::IborIndex;
use crate::indexes::index::Index;
use crate::indexes::interestrateindex::InterestRateIndex;
use crate::instrument::Instrument;
use crate::instruments::Swap;
use crate::patterns::observable::{AsObservable, Observable};
use crate::pricingengine::PricingEngine;
use crate::pricingengines::swap::DiscountingSwapEngine;
use crate::quotes::Quote;
use crate::settings::Settings;
use crate::shared::{Shared, SharedMut, shared, shared_mut};
use crate::termstructures::bootstraphelper::{
BootstrapHelperBase, RateHelper, RelativeDateRateHelper,
};
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::calendar::Calendar;
use crate::time::date::Date;
use crate::time::period::Period;
use crate::time::schedule::MakeSchedule;
use crate::time::timeunit::TimeUnit;
use crate::types::{Integer, Natural, Real};
pub struct IborIborBasisSwapRateHelper {
base: BootstrapHelperBase,
swap: RefCell<Option<Swap>>,
date_error: RefCell<Option<QlError>>,
tenor: Period,
settlement_days: Natural,
calendar: Calendar,
convention: BusinessDayConvention,
end_of_month: bool,
base_index: Shared<IborIndex>,
other_index: Shared<IborIndex>,
discount_handle: Handle<dyn YieldTermStructure>,
settings: Shared<Settings<Date>>,
term_structure_handle: RelinkableHandle<dyn YieldTermStructure>,
}
impl IborIborBasisSwapRateHelper {
#[allow(clippy::too_many_arguments)]
pub fn new(
basis: Handle<dyn Quote>,
tenor: Period,
settlement_days: Natural,
calendar: Calendar,
convention: BusinessDayConvention,
end_of_month: bool,
base_index: &Shared<IborIndex>,
other_index: &Shared<IborIndex>,
discount_handle: Handle<dyn YieldTermStructure>,
bootstrap_base_curve: bool,
) -> Shared<IborIborBasisSwapRateHelper> {
Self::build(
basis,
tenor,
settlement_days,
calendar,
convention,
end_of_month,
base_index,
other_index,
discount_handle,
bootstrap_base_curve,
)
.expect("valid basis-swap helper inputs")
}
#[allow(clippy::too_many_arguments)]
pub fn try_new(
basis: Handle<dyn Quote>,
tenor: Period,
settlement_days: Natural,
calendar: Calendar,
convention: BusinessDayConvention,
end_of_month: bool,
base_index: &Shared<IborIndex>,
other_index: &Shared<IborIndex>,
discount_handle: Handle<dyn YieldTermStructure>,
bootstrap_base_curve: bool,
) -> QlResult<Shared<IborIborBasisSwapRateHelper>> {
crate::require!(!basis.is_empty(), "basis quote handle is empty");
crate::require!(
!discount_handle.is_empty(),
"discount curve handle is empty"
);
crate::require!(
Shared::ptr_eq(base_index.base().settings(), other_index.base().settings()),
"basis indices must share settings"
);
let date_span = Date::max_date() - Date::min_date();
crate::require!(
settlement_days <= date_span as Natural,
"settlement days exceed the supported date range"
);
for period in [tenor, base_index.tenor(), other_index.tenor()] {
let minimum_days = match period.units() {
TimeUnit::Days => 1,
TimeUnit::Weeks => 7,
TimeUnit::Months => 28,
TimeUnit::Years => 365,
_ => crate::fail!("basis swap periods must use days, weeks, months or years"),
};
crate::require!(
period.length() > 0
&& i64::from(period.length()) * minimum_days <= i64::from(date_span),
"basis swap periods must be positive and within the supported date range"
);
}
for index in [base_index, other_index] {
crate::require!(
index.fixing_days() <= date_span as Natural,
"index fixing days exceed the supported date range"
);
}
Self::build(
basis,
tenor,
settlement_days,
calendar,
convention,
end_of_month,
base_index,
other_index,
discount_handle,
bootstrap_base_curve,
)
}
#[allow(clippy::too_many_arguments)]
fn build(
basis: Handle<dyn Quote>,
tenor: Period,
settlement_days: Natural,
calendar: Calendar,
convention: BusinessDayConvention,
end_of_month: bool,
base_index: &Shared<IborIndex>,
other_index: &Shared<IborIndex>,
discount_handle: Handle<dyn YieldTermStructure>,
bootstrap_base_curve: bool,
) -> QlResult<Shared<IborIborBasisSwapRateHelper>> {
let settings = base_index.base().settings().clone();
let helper = Shared::new_cyclic(|weak: &Weak<IborIborBasisSwapRateHelper>| {
let weak = weak.clone();
let on_eval_change = Box::new(move || {
if let Some(helper) = weak.upgrade() {
helper.initialize_dates();
}
});
let term_structure_handle = RelinkableHandle::<dyn YieldTermStructure>::empty();
let (base_index, other_index) = if bootstrap_base_curve {
(
shared(base_index.clone_with(term_structure_handle.handle())),
Shared::clone(other_index),
)
} else {
(
Shared::clone(base_index),
shared(other_index.clone_with(term_structure_handle.handle())),
)
};
let base = BootstrapHelperBase::new_relative(
basis,
Shared::clone(&settings),
true,
on_eval_change,
);
let (cloned, kept) = if bootstrap_base_curve {
(&base_index, &other_index)
} else {
(&other_index, &base_index)
};
cloned
.settings()
.register_fixing_observer(&cloned.name(), &base.observer());
kept.observable().register_observer(&base.observer());
discount_handle.register_observer(&base.observer());
IborIborBasisSwapRateHelper {
base,
swap: RefCell::new(None),
date_error: RefCell::new(None),
tenor,
settlement_days,
calendar,
convention,
end_of_month,
base_index,
other_index,
discount_handle,
settings,
term_structure_handle,
}
});
helper.try_initialize_dates()?;
Ok(helper)
}
fn leg(
&self,
index: &Shared<IborIndex>,
earliest: Date,
termination: Date,
) -> QlResult<BasisLeg> {
let schedule = MakeSchedule::new()
.from(earliest)
.to(termination)
.with_tenor(index.tenor())
.with_calendar(self.calendar.clone())
.with_convention(self.convention)
.end_of_month(self.end_of_month)
.backwards()
.build();
let end = schedule.end_date();
let coupons = IborLeg::new(schedule, Shared::clone(index))
.with_notional(100.0)
.coupons()?;
let last = coupons
.last()
.ok_or_else(|| QlError::new("basis swap leg is empty", file!(), line!()))?;
let last_payment = last.date();
let last_fixing_end = last.fixing_end_date()?;
let leg = coupons
.into_iter()
.map(|coupon| coupon as Shared<dyn CashFlow>)
.collect();
Ok(BasisLeg {
cashflows: leg,
end,
last_payment,
last_fixing_end,
})
}
}
struct BasisLeg {
cashflows: Vec<Shared<dyn CashFlow>>,
end: Date,
last_payment: Date,
last_fixing_end: Date,
}
impl AsObservable for IborIborBasisSwapRateHelper {
fn observable(&self) -> &Observable {
self.base.observable()
}
}
impl RateHelper for IborIborBasisSwapRateHelper {
fn base(&self) -> &BootstrapHelperBase {
&self.base
}
fn implied_quote(&self) -> QlResult<Real> {
if let Some(error) = self.date_error.borrow().as_ref() {
return Err(error.clone());
}
let curve = self.base.term_structure()?;
curve.discount(0.0, true)?;
let mut guard = self.swap.borrow_mut();
let swap = guard
.as_mut()
.expect("initialize_dates populates the swap at construction");
swap.recalculate()?;
const BASIS_POINT: Real = 1.0e-4;
Ok(-(swap.npv()? / swap.leg_bps(0)?) * BASIS_POINT)
}
fn set_term_structure(&self, term_structure: &Shared<dyn YieldTermStructure>) {
self.term_structure_handle
.link_to_weak(Shared::downgrade(term_structure));
self.base.set_term_structure(term_structure);
}
}
impl IborIborBasisSwapRateHelper {
pub fn try_initialize_dates(&self) -> QlResult<()> {
let result =
catch_unwind(AssertUnwindSafe(|| self.rebuild_dates())).unwrap_or_else(|payload| {
let message = payload
.downcast_ref::<String>()
.map(String::as_str)
.or_else(|| payload.downcast_ref::<&str>().copied())
.unwrap_or("invalid basis swap schedule");
Err(QlError::new(message, file!(), line!()))
});
match result {
Ok((swap, earliest, maturity, latest_relevant)) => {
*self.swap.borrow_mut() = Some(swap);
self.base.set_earliest_date(earliest);
self.base.set_maturity_date(maturity);
self.base.set_latest_relevant_date(latest_relevant);
self.base.set_pillar_date(latest_relevant);
self.base.set_latest_date(latest_relevant);
*self.date_error.borrow_mut() = None;
Ok(())
}
Err(error) => {
*self.date_error.borrow_mut() = Some(error.clone());
Err(error)
}
}
}
fn rebuild_dates(&self) -> QlResult<(Swap, Date, Date, Date)> {
let today = self
.base
.evaluation_date()
.ok_or_else(|| QlError::new("evaluation date is not set", file!(), line!()))?;
crate::require!(
today >= Date::min_date() && today <= Date::max_date(),
"evaluation date is outside the supported range"
);
let earliest = self.calendar.advance(
today,
self.settlement_days as Integer,
TimeUnit::Days,
BusinessDayConvention::Following,
false,
);
let termination = earliest + self.tenor;
crate::require!(
termination > earliest,
"basis swap maturity must follow its spot date"
);
let base = self.leg(&self.base_index, earliest, termination)?;
let other = self.leg(&self.other_index, earliest, termination)?;
let maturity = base.end.max(other.end);
let latest_relevant = maturity
.max(base.last_payment.max(other.last_payment))
.max(base.last_fixing_end.max(other.last_fixing_end));
let mut swap = Swap::two_leg(
base.cashflows,
other.cashflows,
Shared::clone(&self.settings),
);
let engine = shared_mut(DiscountingSwapEngine::new(
self.discount_handle.clone(),
None,
None,
None,
Shared::clone(&self.settings),
));
swap.base_mut()
.set_pricing_engine(engine as SharedMut<dyn PricingEngine>);
Ok((swap, earliest, maturity, latest_relevant))
}
}
impl RelativeDateRateHelper for IborIborBasisSwapRateHelper {
fn initialize_dates(&self) {
let _ = self.try_initialize_dates();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::indexes::ibor::euribor::Euribor;
use crate::interestrate::Compounding;
use crate::math::interpolations::loglinear::LogLinear;
use crate::quotes::SimpleQuote;
use crate::termstructures::bootstraptraits::Discount;
use crate::termstructures::yields::{FlatForward, PiecewiseYieldCurve};
use crate::test_support::{Flag, as_observer, implied_quote_on_dirty_curve};
use crate::time::calendars::target::Target;
use crate::time::date::Month;
use crate::time::dategenerationrule::DateGeneration;
use crate::time::daycounters::actual360::Actual360;
use crate::time::frequency::Frequency;
fn flat(reference: Date, rate: Real) -> Shared<dyn YieldTermStructure> {
shared(FlatForward::with_rate(
reference,
rate,
Actual360::new(),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>
}
struct Market {
settings: Shared<Settings<Date>>,
euribor3m: Shared<IborIndex>,
euribor6m: Shared<IborIndex>,
curve3m: Shared<dyn YieldTermStructure>,
curve6m: Shared<dyn YieldTermStructure>,
discount: Handle<dyn YieldTermStructure>,
}
fn market() -> Market {
let settings = shared(Settings::<Date>::new());
let today = Date::new(23, Month::October, 2025);
settings.set_evaluation_date(today);
let curve3m = flat(today, 0.03);
let curve6m = flat(today, 0.035);
let discount = Handle::new(flat(today, 0.02));
let euribor3m = shared(Euribor::three_months(
Handle::new(Shared::clone(&curve3m)),
Shared::clone(&settings),
));
let euribor6m = shared(Euribor::six_months(
Handle::new(Shared::clone(&curve6m)),
Shared::clone(&settings),
));
Market {
settings,
euribor3m,
euribor6m,
curve3m,
curve6m,
discount,
}
}
fn helper(m: &Market, bootstrap_base_curve: bool) -> Shared<IborIborBasisSwapRateHelper> {
IborIborBasisSwapRateHelper::new(
Handle::new(shared(SimpleQuote::new(0.002)) as Shared<dyn Quote>),
Period::new(5, TimeUnit::Years),
m.euribor3m.fixing_days(),
m.euribor3m.fixing_calendar(),
m.euribor3m.business_day_convention(),
m.euribor3m.end_of_month(),
&m.euribor3m,
&m.euribor6m,
m.discount.clone(),
bootstrap_base_curve,
)
}
#[test]
fn fallible_constructor_rejects_malformed_inputs() {
let m = market();
let create = |tenor, settlement_days, quote, discount, other: &Shared<IborIndex>| {
IborIborBasisSwapRateHelper::try_new(
quote,
tenor,
settlement_days,
Target::new(),
BusinessDayConvention::ModifiedFollowing,
false,
&m.euribor3m,
other,
discount,
true,
)
};
let quote = Handle::new(shared(SimpleQuote::new(0.002)) as Shared<dyn Quote>);
let valid = Period::new(5, TimeUnit::Years);
for tenor in [
Period::new(0, TimeUnit::Years),
Period::new(-1, TimeUnit::Months),
Period::new(1, TimeUnit::Hours),
Period::new(i32::MAX, TimeUnit::Months),
] {
assert!(create(tenor, 2, quote.clone(), m.discount.clone(), &m.euribor6m).is_err());
}
assert!(
create(
valid,
u32::MAX,
quote.clone(),
m.discount.clone(),
&m.euribor6m
)
.is_err()
);
assert!(create(valid, 2, Handle::empty(), m.discount.clone(), &m.euribor6m).is_err());
assert!(create(valid, 2, quote.clone(), Handle::empty(), &m.euribor6m).is_err());
let other = market();
assert!(
create(
valid,
2,
quote.clone(),
m.discount.clone(),
&other.euribor6m
)
.is_err()
);
let h = create(valid, 2, quote.clone(), m.discount.clone(), &m.euribor6m).unwrap();
drop(h);
m.settings.set_evaluation_date(Date::max_date());
assert!(create(valid, 2, quote, m.discount.clone(), &m.euribor6m).is_err());
}
#[test]
fn date_range_failure_preserves_swap_and_recovers() {
let m = market();
let h = helper(&m, true);
h.set_term_structure(&m.curve3m);
let before = h.implied_quote().unwrap();
let spot = h.earliest_date();
let maturity = h.maturity_date();
let original_date = m.settings.evaluation_date().unwrap();
for invalid_date in [Date::max_date(), Date::null()] {
m.settings.set_evaluation_date(invalid_date);
assert!(h.implied_quote().is_err());
assert!(h.try_initialize_dates().is_err());
assert_eq!(h.earliest_date(), spot);
assert_eq!(h.maturity_date(), maturity);
m.settings.set_evaluation_date(original_date);
assert_eq!(h.implied_quote().unwrap(), before);
}
}
#[test]
fn legacy_constructor_keeps_deferred_handle_linking() {
let m = market();
let quote = RelinkableHandle::<dyn Quote>::empty();
let discount = RelinkableHandle::<dyn YieldTermStructure>::empty();
let h = IborIborBasisSwapRateHelper::new(
quote.handle(),
Period::new(5, TimeUnit::Years),
2,
Target::new(),
BusinessDayConvention::ModifiedFollowing,
false,
&m.euribor3m,
&m.euribor6m,
discount.handle(),
true,
);
quote.link_to(shared(SimpleQuote::new(0.002)) as Shared<dyn Quote>);
discount.link_to(m.discount.current_link().unwrap());
h.set_term_structure(&m.curve3m);
assert!(h.implied_quote().unwrap().is_finite());
}
fn independent_swap_npv(m: &Market, h: &IborIborBasisSwapRateHelper, basis: Real) -> Real {
let tenor = Period::new(5, TimeUnit::Years);
independent_swap_npv_on(m, h, basis, tenor, DateGeneration::Backward)
}
fn independent_swap_npv_on(
m: &Market,
h: &IborIborBasisSwapRateHelper,
basis: Real,
tenor: Period,
rule: DateGeneration,
) -> Real {
let leg = |index: &Shared<IborIndex>, spread: Real| {
let schedule = MakeSchedule::new()
.from(h.earliest_date())
.to(h.earliest_date() + tenor)
.with_tenor(index.tenor())
.with_calendar(m.euribor3m.fixing_calendar())
.with_convention(m.euribor3m.business_day_convention())
.end_of_month(m.euribor3m.end_of_month())
.with_rule(rule)
.build();
IborLeg::new(schedule, Shared::clone(index))
.with_spread(spread)
.with_notional(1.0)
.build()
.expect("the leg builds")
};
let mut swap = Swap::two_leg(
leg(&m.euribor3m, basis),
leg(&m.euribor6m, 0.0),
Shared::clone(&m.settings),
);
let engine = shared_mut(DiscountingSwapEngine::new(
m.discount.clone(),
None,
None,
None,
Shared::clone(&m.settings),
));
swap.base_mut()
.set_pricing_engine(engine as SharedMut<dyn PricingEngine>);
swap.npv().expect("the swap prices")
}
#[test]
fn implied_quote_zeroes_the_independent_swap() {
let m = market();
for (bootstrap_base_curve, curve) in [(true, &m.curve3m), (false, &m.curve6m)] {
let h = helper(&m, bootstrap_base_curve);
assert!(h.implied_quote().is_err(), "no curve set yet");
h.set_term_structure(curve);
let basis = h.implied_quote().expect("the basis solves");
assert!(basis.abs() > 1.0e-4, "flat 3% vs 3.5% is a real basis");
let npv = independent_swap_npv(&m, &h, basis);
assert!(npv.abs() < 1.0e-12, "swap at the implied basis: {npv}");
let off = independent_swap_npv(&m, &h, basis + 1.0e-4);
assert!(off.abs() > 1.0e-6, "a 1bp bump moves the swap: {off}");
}
}
#[test]
fn default_rule_places_the_stub_at_the_front() {
let m = market();
let tenor = Period::new(15, TimeUnit::Months);
let h = IborIborBasisSwapRateHelper::new(
Handle::new(shared(SimpleQuote::new(0.002)) as Shared<dyn Quote>),
tenor,
m.euribor3m.fixing_days(),
m.euribor3m.fixing_calendar(),
m.euribor3m.business_day_convention(),
m.euribor3m.end_of_month(),
&m.euribor3m,
&m.euribor6m,
m.discount.clone(),
true,
);
h.set_term_structure(&m.curve3m);
let basis = h.implied_quote().unwrap();
let backward = independent_swap_npv_on(&m, &h, basis, tenor, DateGeneration::Backward);
let forward = independent_swap_npv_on(&m, &h, basis, tenor, DateGeneration::Forward);
assert!(backward.abs() < 1e-12, "backward NPV {backward}");
assert!(forward.abs() > 1e-7, "forward NPV {forward}");
}
#[test]
fn initialize_dates_places_the_pillar_at_the_latest_relevant_date() {
let m = market();
let h = helper(&m, true);
let calendar = Target::new();
let spot = calendar.advance(
Date::new(23, Month::October, 2025),
2,
TimeUnit::Days,
BusinessDayConvention::Following,
false,
);
assert_eq!(h.earliest_date(), spot);
assert_eq!(
h.maturity_date(),
calendar.adjust(
spot + Period::new(5, TimeUnit::Years),
BusinessDayConvention::ModifiedFollowing
)
);
assert!(h.latest_relevant_date() >= h.maturity_date());
assert_eq!(h.pillar_date(), h.latest_relevant_date());
assert_eq!(h.latest_date(), h.latest_relevant_date());
}
#[test]
fn either_index_fixing_invalidates_and_recalibrates_the_live_curve() {
for bootstrap_base in [true, false] {
for change_base in [true, false] {
let m = market();
let h = helper(&m, bootstrap_base);
let today = m.settings.evaluation_date().unwrap();
let curve = PiecewiseYieldCurve::<Discount, LogLinear>::new(
today,
vec![Shared::clone(&h) as Shared<dyn RateHelper>],
Actual360::new(),
LogLinear,
)
.unwrap();
let before = curve.discount_date(h.pillar_date(), false).unwrap();
let helper_flag = Flag::new();
let curve_flag = Flag::new();
h.observable().register_observer(&as_observer(&helper_flag));
curve.register_observer(&as_observer(&curve_flag));
let index = if change_base {
&m.euribor3m
} else {
&m.euribor6m
};
index.add_fixing(today, 0.08).unwrap();
assert!(
Flag::is_up(&helper_flag),
"bootstrap_base={bootstrap_base}, change_base={change_base}"
);
assert!(Flag::is_up(&curve_flag));
let after = curve.discount_date(h.pillar_date(), false).unwrap();
assert!((after - before).abs() > 1e-4);
assert!(h.quote_error().unwrap().abs() < 1e-12);
let weak_curve = Shared::downgrade(&curve);
drop(curve);
assert!(weak_curve.upgrade().is_none());
assert!(h.implied_quote().is_err());
}
}
}
#[test]
fn fitted_curve_changes_and_relinks_do_not_notify_the_helper() {
for bootstrap_base in [true, false] {
let m = market();
let h = helper(&m, bootstrap_base);
let quote = shared(SimpleQuote::new(0.04));
let curve = shared(FlatForward::new(
m.settings.evaluation_date().unwrap(),
Handle::new(Shared::clone("e) as Shared<dyn Quote>),
Actual360::new(),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>;
let flag = Flag::new();
h.observable().register_observer(&as_observer(&flag));
h.set_term_structure(&curve);
assert!(!Flag::is_up(&flag));
let before = h.implied_quote().unwrap();
quote.set_value(0.05);
assert!(!Flag::is_up(&flag));
let after = h.implied_quote().unwrap();
assert!((after - before).abs() > 1e-4);
h.set_term_structure(&m.curve3m);
assert!(!Flag::is_up(&flag));
}
}
#[test]
fn implied_quote_on_a_dirty_curve_reprices_the_new_quote() {
for bootstrap_base_curve in [true, false] {
let m = market();
let today = m.settings.evaluation_date().unwrap();
let (dirty, fresh) = implied_quote_on_dirty_curve(today, 0.002, 0.003, |quote| {
IborIborBasisSwapRateHelper::new(
quote,
Period::new(5, TimeUnit::Years),
m.euribor3m.fixing_days(),
m.euribor3m.fixing_calendar(),
m.euribor3m.business_day_convention(),
m.euribor3m.end_of_month(),
&m.euribor3m,
&m.euribor6m,
m.discount.clone(),
bootstrap_base_curve,
) as Shared<dyn RateHelper>
});
assert!((dirty - fresh).abs() < 1e-12, "{dirty} vs {fresh}");
assert!((dirty - 0.003).abs() < 1e-10, "{dirty}");
}
}
#[test]
fn iboribor_bootstrap_reprices_fair_swaps_on_backward_schedules() {
use crate::indexes::ibor::usdlibor::UsdLibor;
use crate::math::interpolations::linear::Linear;
use crate::termstructures::bootstraptraits::ZeroYield;
use crate::time::calendars::unitedstates::{Market as UsMarket, UnitedStates};
use crate::time::daycounters::actual365fixed::Actual365Fixed;
let quotes = [
(1, 0.0010),
(2, 0.0012),
(3, 0.0015),
(5, 0.0015),
(8, 0.0018),
(10, 0.0020),
(15, 0.0021),
(20, 0.0021),
];
let calendar = UnitedStates::new(UsMarket::GovernmentBond);
let convention = BusinessDayConvention::Following;
for bootstrap_base_curve in [true, false] {
let settings = shared(Settings::<Date>::new());
let today = Date::new(23, Month::October, 2025);
settings.set_evaluation_date(today);
let flat365 = |rate: Real| {
Handle::new(shared(FlatForward::with_rate(
today,
rate,
Actual365Fixed::new(),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>)
};
let known = flat365(0.01);
let discount = flat365(0.005);
let libor = |months: Integer, curve: Handle<dyn YieldTermStructure>| {
shared(
UsdLibor::new(
Period::new(months, TimeUnit::Months),
curve,
Shared::clone(&settings),
)
.unwrap(),
)
};
let (base, other) = if bootstrap_base_curve {
(libor(3, Handle::empty()), libor(6, known.clone()))
} else {
(libor(3, known.clone()), libor(6, Handle::empty()))
};
let helpers = quotes
.iter()
.map(|&(years, basis)| {
IborIborBasisSwapRateHelper::new(
Handle::new(shared(SimpleQuote::new(basis)) as Shared<dyn Quote>),
Period::new(years, TimeUnit::Years),
2,
calendar.clone(),
convention,
false,
&base,
&other,
discount.clone(),
bootstrap_base_curve,
) as Shared<dyn RateHelper>
})
.collect();
let fitted = Handle::new(
PiecewiseYieldCurve::<ZeroYield, Linear>::new(
today,
helpers,
Actual365Fixed::new(),
Linear,
)
.unwrap() as Shared<dyn YieldTermStructure>,
);
let (base, other) = if bootstrap_base_curve {
(libor(3, fitted), libor(6, known.clone()))
} else {
(libor(3, known.clone()), libor(6, fitted))
};
let spot = calendar.advance(today, 2, TimeUnit::Days, convention, false);
for &(years, basis) in "es {
let leg = |index: &Shared<IborIndex>, spread: Real| {
let schedule = MakeSchedule::new()
.from(spot)
.to(spot + Period::new(years, TimeUnit::Years))
.with_tenor(index.tenor())
.with_calendar(calendar.clone())
.with_convention(convention)
.backwards()
.build();
IborLeg::new(schedule, Shared::clone(index))
.with_spread(spread)
.with_notional(100.0)
.build()
.unwrap()
};
let mut swap = Swap::two_leg(
leg(&base, basis),
leg(&other, 0.0),
Shared::clone(&settings),
);
swap.base_mut()
.set_pricing_engine(shared_mut(DiscountingSwapEngine::new(
discount.clone(),
None,
None,
None,
Shared::clone(&settings),
)) as SharedMut<dyn PricingEngine>);
let npv = swap.npv().unwrap();
assert!(
npv.abs() < 1e-8,
"base={bootstrap_base_curve}, {years}Y fair swap NPV {npv}"
);
}
}
}
}