use std::cell::Cell;
use std::cell::RefCell;
use std::rc::Weak;
use crate::cashflows::RateAveraging;
use crate::errors::QlResult;
use crate::handle::{Handle, RelinkableHandle};
use crate::indexes::OvernightIndex;
use crate::indexes::iborindex::IborIndex;
use crate::indexes::index::Index;
use crate::indexes::interestrateindex::InterestRateIndex;
use crate::instrument::Instrument;
use crate::instruments::{MakeOis, MakeVanillaSwap, OvernightIndexedSwap, VanillaSwap};
use crate::patterns::observable::{AsObservable, Observable};
use crate::quotes::{Quote, SimpleQuote};
use crate::settings::Settings;
use crate::shared::{Shared, shared};
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::dategenerationrule::DateGeneration;
use crate::time::daycounter::DayCounter;
use crate::time::frequency::Frequency;
use crate::time::period::Period;
use crate::time::timeunit::TimeUnit;
use crate::types::{Integer, Natural, Real};
pub struct DepositRateHelper {
base: BootstrapHelperBase,
index: IborIndex,
term_structure_handle: RelinkableHandle<dyn YieldTermStructure>,
fixing_date: Cell<Date>,
}
impl DepositRateHelper {
pub fn new(quote: Handle<dyn Quote>, index: &IborIndex) -> Shared<DepositRateHelper> {
Self::build(quote, index)
}
pub fn from_rate(rate: Real, index: &IborIndex) -> Shared<DepositRateHelper> {
let quote = Handle::new(shared(SimpleQuote::new(rate)) as Shared<dyn Quote>);
Self::build(quote, index)
}
fn build(quote: Handle<dyn Quote>, source_index: &IborIndex) -> Shared<DepositRateHelper> {
let settings = source_index.base().settings().clone();
Shared::new_cyclic(|weak: &Weak<DepositRateHelper>| {
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 index = source_index.clone_with(term_structure_handle.handle());
let base = BootstrapHelperBase::new_relative(quote, settings, true, on_eval_change);
let helper = DepositRateHelper {
base,
index,
term_structure_handle,
fixing_date: Cell::new(Date::null()),
};
helper.initialize_dates();
helper
})
}
}
impl AsObservable for DepositRateHelper {
fn observable(&self) -> &Observable {
self.base.observable()
}
}
impl RateHelper for DepositRateHelper {
fn base(&self) -> &BootstrapHelperBase {
&self.base
}
fn implied_quote(&self) -> QlResult<Real> {
self.base.term_structure()?;
self.index.fixing(self.fixing_date.get(), true)
}
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 RelativeDateRateHelper for DepositRateHelper {
fn initialize_dates(&self) {
let evaluation_date = self
.base
.evaluation_date()
.expect("a relative-date helper always tracks an evaluation date");
let reference_date = self
.index
.fixing_calendar()
.adjust(evaluation_date, BusinessDayConvention::Following);
let earliest = self
.index
.value_date(reference_date)
.expect("value date of an adjusted business day is valid");
self.fixing_date.set(self.index.fixing_date(earliest));
let maturity = self
.index
.maturity_date(earliest)
.expect("maturity date of a value date is valid");
self.base.set_earliest_date(earliest);
self.base.set_maturity_date(maturity);
self.base.set_pillar_date(maturity);
self.base.set_latest_date(maturity);
self.base.set_latest_relevant_date(maturity);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Pillar {
MaturityDate,
LastRelevantDate,
}
pub struct SwapRateHelper {
base: BootstrapHelperBase,
swap: RefCell<Option<VanillaSwap>>,
ibor_index: Shared<IborIndex>,
term_structure_handle: RelinkableHandle<dyn YieldTermStructure>,
discount_relinkable_handle: RelinkableHandle<dyn YieldTermStructure>,
discount_handle: Option<Handle<dyn YieldTermStructure>>,
spread: Handle<dyn Quote>,
settings: Shared<Settings<Date>>,
tenor: Period,
forward_start: Period,
calendar: Calendar,
fixed_frequency: Frequency,
fixed_convention: BusinessDayConvention,
fixed_day_count: DayCounter,
end_of_month: bool,
use_indexed_coupons: Option<bool>,
pillar: Pillar,
}
impl SwapRateHelper {
#[allow(clippy::too_many_arguments)]
pub fn new(
quote: Handle<dyn Quote>,
tenor: Period,
calendar: Calendar,
fixed_frequency: Frequency,
fixed_convention: BusinessDayConvention,
fixed_day_count: DayCounter,
ibor_index: &IborIndex,
) -> Shared<SwapRateHelper> {
Self::build(
quote,
tenor,
calendar,
fixed_frequency,
fixed_convention,
fixed_day_count,
ibor_index,
Handle::empty(),
Period::new(0, TimeUnit::Days),
None,
Pillar::LastRelevantDate,
)
}
#[allow(clippy::too_many_arguments)]
pub fn from_rate(
rate: Real,
tenor: Period,
calendar: Calendar,
fixed_frequency: Frequency,
fixed_convention: BusinessDayConvention,
fixed_day_count: DayCounter,
ibor_index: &IborIndex,
) -> Shared<SwapRateHelper> {
let quote = Handle::new(shared(SimpleQuote::new(rate)) as Shared<dyn Quote>);
Self::new(
quote,
tenor,
calendar,
fixed_frequency,
fixed_convention,
fixed_day_count,
ibor_index,
)
}
#[allow(clippy::too_many_arguments)]
pub fn with_details(
quote: Handle<dyn Quote>,
tenor: Period,
calendar: Calendar,
fixed_frequency: Frequency,
fixed_convention: BusinessDayConvention,
fixed_day_count: DayCounter,
ibor_index: &IborIndex,
spread: Handle<dyn Quote>,
forward_start: Period,
discounting_curve: Option<Handle<dyn YieldTermStructure>>,
pillar: Pillar,
) -> Shared<SwapRateHelper> {
Self::build(
quote,
tenor,
calendar,
fixed_frequency,
fixed_convention,
fixed_day_count,
ibor_index,
spread,
forward_start,
discounting_curve,
pillar,
)
}
#[allow(clippy::too_many_arguments)]
fn build(
quote: Handle<dyn Quote>,
tenor: Period,
calendar: Calendar,
fixed_frequency: Frequency,
fixed_convention: BusinessDayConvention,
fixed_day_count: DayCounter,
source_index: &IborIndex,
spread: Handle<dyn Quote>,
forward_start: Period,
discounting_curve: Option<Handle<dyn YieldTermStructure>>,
pillar: Pillar,
) -> Shared<SwapRateHelper> {
let settings = source_index.base().settings().clone();
Shared::new_cyclic(|weak: &Weak<SwapRateHelper>| {
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 ibor_index = shared(source_index.clone_with(term_structure_handle.handle()));
let base = BootstrapHelperBase::new_relative(
quote,
Shared::clone(&settings),
true,
on_eval_change,
);
let helper = SwapRateHelper {
base,
swap: RefCell::new(None),
ibor_index,
term_structure_handle,
discount_relinkable_handle: RelinkableHandle::<dyn YieldTermStructure>::empty(),
discount_handle: discounting_curve,
spread,
settings,
tenor,
forward_start,
calendar,
fixed_frequency,
fixed_convention,
fixed_day_count,
end_of_month: false,
use_indexed_coupons: None,
pillar,
};
helper.initialize_dates();
helper
})
}
}
impl AsObservable for SwapRateHelper {
fn observable(&self) -> &Observable {
self.base.observable()
}
}
impl RateHelper for SwapRateHelper {
fn base(&self) -> &BootstrapHelperBase {
&self.base
}
fn implied_quote(&self) -> QlResult<Real> {
self.base.term_structure()?;
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;
let floating_leg_npv = swap.fixed_vs_floating_mut().floating_leg_npv()?;
let spread = if self.spread.is_empty() {
0.0
} else {
self.spread.current_link()?.value()?
};
let spread_npv = swap.fixed_vs_floating_mut().floating_leg_bps()? / BASIS_POINT * spread;
let total_npv = -(floating_leg_npv + spread_npv);
let fixed_leg_bps = swap.fixed_vs_floating_mut().fixed_leg_bps()?;
Ok(total_npv / (fixed_leg_bps / BASIS_POINT))
}
fn set_term_structure(&self, term_structure: &Shared<dyn YieldTermStructure>) {
self.term_structure_handle
.link_to_weak(Shared::downgrade(term_structure));
match &self.discount_handle {
Some(discount) if !discount.is_empty() => {
let curve = discount
.current_link()
.expect("a non-empty discount handle resolves");
self.discount_relinkable_handle
.link_to_weak(Shared::downgrade(&curve));
}
_ => self
.discount_relinkable_handle
.link_to_weak(Shared::downgrade(term_structure)),
}
self.base.set_term_structure(term_structure);
}
}
impl RelativeDateRateHelper for SwapRateHelper {
fn initialize_dates(&self) {
let fixed_tenor = if self.fixed_frequency == Frequency::Once {
self.tenor
} else {
Period::try_from(self.fixed_frequency)
.expect("a swap's fixed frequency maps to a valid period")
};
let swap = MakeVanillaSwap::new(
self.tenor,
Shared::clone(&self.ibor_index),
Some(0.0),
self.forward_start,
Shared::clone(&self.settings),
)
.with_discounting_term_structure(self.discount_relinkable_handle.handle())
.with_fixed_leg_day_count(self.fixed_day_count.clone())
.with_fixed_leg_tenor(fixed_tenor)
.with_fixed_leg_convention(self.fixed_convention)
.with_fixed_leg_termination_date_convention(self.fixed_convention)
.with_fixed_leg_calendar(self.calendar.clone())
.with_fixed_leg_end_of_month(self.end_of_month)
.with_floating_leg_calendar(self.calendar.clone())
.with_floating_leg_end_of_month(self.end_of_month)
.with_indexed_coupons(self.use_indexed_coupons)
.build()
.expect("a 0% fixed-rate swap with a valid evaluation date builds without pricing");
let base = swap.fixed_vs_floating();
let earliest = base
.fixed_schedule()
.start_date()
.min(base.floating_schedule().start_date());
let maturity = base
.fixed_schedule()
.end_date()
.max(base.floating_schedule().end_date());
let latest_relevant = maturity;
self.base.set_earliest_date(earliest);
self.base.set_maturity_date(maturity);
self.base.set_latest_relevant_date(latest_relevant);
let pillar = match self.pillar {
Pillar::MaturityDate => maturity,
Pillar::LastRelevantDate => latest_relevant,
};
self.base.set_pillar_date(pillar);
self.base.set_latest_date(pillar);
*self.swap.borrow_mut() = Some(swap);
}
}
pub struct OISRateHelper {
base: BootstrapHelperBase,
swap: RefCell<Option<OvernightIndexedSwap>>,
overnight_index: Shared<OvernightIndex>,
term_structure_handle: RelinkableHandle<dyn YieldTermStructure>,
discount_relinkable_handle: RelinkableHandle<dyn YieldTermStructure>,
discount_handle: Option<Handle<dyn YieldTermStructure>>,
overnight_spread: Handle<dyn Quote>,
settings: Shared<Settings<Date>>,
settlement_days: Natural,
tenor: Period,
forward_start: Period,
payment_lag: Integer,
payment_convention: BusinessDayConvention,
payment_frequency: Frequency,
averaging_method: RateAveraging,
pillar: Pillar,
}
impl OISRateHelper {
#[allow(clippy::too_many_arguments)]
pub fn new(
settlement_days: Natural,
tenor: Period,
quote: Handle<dyn Quote>,
overnight_index: &OvernightIndex,
discounting_curve: Option<Handle<dyn YieldTermStructure>>,
payment_lag: Integer,
payment_convention: BusinessDayConvention,
payment_frequency: Frequency,
forward_start: Period,
overnight_spread: Handle<dyn Quote>,
pillar: Pillar,
averaging_method: RateAveraging,
settings: Shared<Settings<Date>>,
) -> Shared<OISRateHelper> {
Shared::new_cyclic(|weak: &Weak<OISRateHelper>| {
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 cloned_index = overnight_index.clone_with(term_structure_handle.handle());
let base = BootstrapHelperBase::new_relative(
quote,
Shared::clone(&settings),
true,
on_eval_change,
);
let helper = OISRateHelper {
base,
swap: RefCell::new(None),
overnight_index: cloned_index,
term_structure_handle,
discount_relinkable_handle: RelinkableHandle::<dyn YieldTermStructure>::empty(),
discount_handle: discounting_curve,
overnight_spread,
settings,
settlement_days,
tenor,
forward_start,
payment_lag,
payment_convention,
payment_frequency,
averaging_method,
pillar,
};
helper.initialize_dates();
helper
})
}
}
impl AsObservable for OISRateHelper {
fn observable(&self) -> &Observable {
self.base.observable()
}
}
impl RateHelper for OISRateHelper {
fn base(&self) -> &BootstrapHelperBase {
&self.base
}
fn implied_quote(&self) -> QlResult<Real> {
self.base.term_structure()?;
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;
let overnight_leg_npv = swap.overnight_leg_npv()?;
let spread = if self.overnight_spread.is_empty() {
0.0
} else {
self.overnight_spread.current_link()?.value()?
};
let spread_npv = swap.overnight_leg_bps()? / BASIS_POINT * spread;
let total_npv = -(overnight_leg_npv + spread_npv);
let fixed_leg_bps = swap.fixed_vs_floating_mut().fixed_leg_bps()?;
Ok(total_npv / (fixed_leg_bps / BASIS_POINT))
}
fn set_term_structure(&self, term_structure: &Shared<dyn YieldTermStructure>) {
self.term_structure_handle
.link_to_weak(Shared::downgrade(term_structure));
match &self.discount_handle {
Some(discount) if !discount.is_empty() => {
let curve = discount
.current_link()
.expect("a non-empty discount handle resolves");
self.discount_relinkable_handle
.link_to_weak(Shared::downgrade(&curve));
}
_ => self
.discount_relinkable_handle
.link_to_weak(Shared::downgrade(term_structure)),
}
self.base.set_term_structure(term_structure);
}
}
impl RelativeDateRateHelper for OISRateHelper {
fn initialize_dates(&self) {
let swap = MakeOis::new(
self.tenor,
Shared::clone(&self.overnight_index),
Some(0.0),
self.forward_start,
Shared::clone(&self.settings),
)
.with_discounting_term_structure(self.discount_relinkable_handle.handle())
.with_telescopic_value_dates(false)
.with_payment_lag(self.payment_lag)
.with_payment_adjustment(self.payment_convention)
.with_payment_frequency(self.payment_frequency)
.with_averaging_method(self.averaging_method)
.with_lookback_days(None)
.with_lockout_days(0)
.with_rule(DateGeneration::Backward)
.with_convention(BusinessDayConvention::ModifiedFollowing)
.with_termination_date_convention(BusinessDayConvention::ModifiedFollowing)
.with_observation_shift(false)
.with_settlement_days(self.settlement_days)
.build()
.expect("a 0% fixed-rate OIS with benign deferred knobs builds without pricing");
let base_swap = swap.fixed_vs_floating();
let earliest = swap
.overnight_schedule()
.start_date()
.min(base_swap.fixed_schedule().start_date());
let maturity = swap
.overnight_schedule()
.end_date()
.max(base_swap.fixed_schedule().end_date());
let last_overnight_payment = swap.overnight_leg().last().map_or(maturity, |cf| cf.date());
let last_fixed_payment = base_swap
.fixed_leg()
.last()
.map_or(maturity, |cf| cf.date());
let latest_relevant = maturity.max(last_overnight_payment).max(last_fixed_payment);
self.base.set_earliest_date(earliest);
self.base.set_maturity_date(maturity);
self.base.set_latest_relevant_date(latest_relevant);
self.base.set_latest_date(latest_relevant);
let pillar = match self.pillar {
Pillar::MaturityDate => maturity,
Pillar::LastRelevantDate => latest_relevant,
};
self.base.set_pillar_date(pillar);
*self.swap.borrow_mut() = Some(swap);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::interestrate::Compounding;
use crate::settings::Settings;
use crate::termstructures::yields::FlatForward;
use crate::test_support::{Flag, as_observer};
use crate::time::calendars::target::Target;
use crate::time::date::{Date, Month};
use crate::time::daycounters::actual360::Actual360;
use crate::time::frequency::Frequency;
use crate::time::period::Period;
use crate::time::timeunit::TimeUnit;
use crate::{currency::Currency, types::Rate};
fn settings_on(today: Date) -> Shared<Settings<Date>> {
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(today);
settings
}
fn euribor(
tenor: Period,
forwarding: Handle<dyn YieldTermStructure>,
settings: Shared<Settings<Date>>,
) -> IborIndex {
IborIndex::new(
"Euribor".into(),
tenor,
2,
Currency::eur(),
Target::new(),
BusinessDayConvention::Following,
false,
Actual360::new(),
forwarding,
settings,
)
}
fn flat_curve(reference: Date, rate: Rate) -> Shared<dyn YieldTermStructure> {
shared(FlatForward::with_rate(
reference,
rate,
Actual360::new(),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>
}
fn today() -> Date {
Date::new(15, Month::June, 2026)
}
#[test]
fn implied_quote_matches_closed_form_deposit_rate() {
let settings = settings_on(today());
let source = euribor(Period::new(6, TimeUnit::Months), Handle::empty(), settings);
let helper = DepositRateHelper::from_rate(0.02, &source);
let rate = 0.03;
let curve = flat_curve(today(), rate);
helper.set_term_structure(&curve);
let d1 = helper.earliest_date();
let d2 = helper.maturity_date();
let t = Actual360::new().year_fraction(d1, d2);
let implied = helper.implied_quote().unwrap();
let closed_form = ((rate * t).exp() - 1.0) / t;
assert!((implied - closed_form).abs() < 1e-12);
}
#[test]
fn initialize_dates_follows_the_index_conventions() {
let settings = settings_on(today());
let source = euribor(Period::new(6, TimeUnit::Months), Handle::empty(), settings);
let helper = DepositRateHelper::from_rate(0.02, &source);
let reference = source
.fixing_calendar()
.adjust(today(), BusinessDayConvention::Following);
let earliest = source.value_date(reference).unwrap();
let maturity = source.maturity_date(earliest).unwrap();
assert_eq!(helper.earliest_date(), earliest);
assert!(earliest > today(), "the value date is spot, past today");
assert_eq!(helper.maturity_date(), maturity);
assert_eq!(helper.pillar_date(), maturity);
assert_eq!(helper.latest_relevant_date(), maturity);
}
#[test]
fn helper_prices_off_its_own_handle_not_the_source_index() {
let settings = settings_on(today());
let source = euribor(Period::new(6, TimeUnit::Months), Handle::empty(), settings);
let helper = DepositRateHelper::from_rate(0.02, &source);
let curve = flat_curve(today(), 0.03);
helper.set_term_structure(&curve);
let implied_low = helper.implied_quote().unwrap();
let curve_high = flat_curve(today(), 0.06);
helper.set_term_structure(&curve_high);
let implied_high = helper.implied_quote().unwrap();
assert!(
implied_high > implied_low,
"relinking the helper's handle moves its implied quote"
);
assert!(
source.forecast_fixing(helper.earliest_date()).is_err(),
"the source index's own empty handle is untouched"
);
}
#[test]
fn quote_error_is_market_minus_implied() {
let settings = settings_on(today());
let source = euribor(Period::new(6, TimeUnit::Months), Handle::empty(), settings);
let helper = DepositRateHelper::from_rate(0.05, &source);
let curve = flat_curve(today(), 0.03);
helper.set_term_structure(&curve);
let implied = helper.implied_quote().unwrap();
assert!((helper.quote_error().unwrap() - (0.05 - implied)).abs() < 1e-15);
}
#[test]
fn evaluation_date_change_reinitializes_dates() {
let settings = settings_on(today());
let source = euribor(
Period::new(6, TimeUnit::Months),
Handle::empty(),
settings.clone(),
);
let helper = DepositRateHelper::from_rate(0.02, &source);
let before = helper.earliest_date();
let flag = Flag::new();
helper.observable().register_observer(&as_observer(&flag));
let moved = today() + 30;
settings.set_evaluation_date(moved);
assert!(Flag::is_up(&flag), "date change must notify observers");
assert!(
helper.earliest_date() > before,
"date change must rerun initialize_dates"
);
}
fn swap_setup() -> (Shared<Settings<Date>>, IborIndex) {
let settings = settings_on(today());
let source = euribor(
Period::new(6, TimeUnit::Months),
Handle::empty(),
settings.clone(),
);
(settings, source)
}
fn independent_swap(
source: &IborIndex,
tenor: Period,
calendar: Calendar,
convention: BusinessDayConvention,
curve: &Shared<dyn YieldTermStructure>,
settings: Shared<Settings<Date>>,
) -> VanillaSwap {
let curve_handle = Handle::new(Shared::clone(curve));
let index = shared(source.clone_with(curve_handle.clone()));
MakeVanillaSwap::new(
tenor,
index,
Some(0.0),
Period::new(0, TimeUnit::Days),
settings,
)
.with_discounting_term_structure(curve_handle)
.with_fixed_leg_day_count(Actual360::new())
.with_fixed_leg_tenor(Period::try_from(Frequency::Annual).unwrap())
.with_fixed_leg_convention(convention)
.with_fixed_leg_termination_date_convention(convention)
.with_fixed_leg_calendar(calendar.clone())
.with_fixed_leg_end_of_month(false)
.with_floating_leg_calendar(calendar)
.with_floating_leg_end_of_month(false)
.build()
.unwrap()
}
#[test]
fn implied_quote_matches_fair_rate_of_the_same_swap() {
let (settings, source) = swap_setup();
let tenor = Period::new(5, TimeUnit::Years);
let calendar = Target::new();
let convention = BusinessDayConvention::ModifiedFollowing;
let helper = SwapRateHelper::from_rate(
0.02,
tenor,
calendar.clone(),
Frequency::Annual,
convention,
Actual360::new(),
&source,
);
let curve = flat_curve(today(), 0.03);
helper.set_term_structure(&curve);
let implied = helper.implied_quote().unwrap();
let mut independent =
independent_swap(&source, tenor, calendar, convention, &curve, settings);
let fair = independent.fixed_vs_floating_mut().fair_rate().unwrap();
assert!(
(implied - fair).abs() < 1e-12,
"implied {implied} vs fair {fair}"
);
}
#[test]
fn nonzero_spread_shifts_the_implied_quote_by_the_bps_ratio() {
let (settings, source) = swap_setup();
let tenor = Period::new(5, TimeUnit::Years);
let calendar = Target::new();
let convention = BusinessDayConvention::ModifiedFollowing;
let curve = flat_curve(today(), 0.03);
let helper0 = SwapRateHelper::from_rate(
0.02,
tenor,
calendar.clone(),
Frequency::Annual,
convention,
Actual360::new(),
&source,
);
helper0.set_term_structure(&curve);
let implied0 = helper0.implied_quote().unwrap();
let spread = 0.001;
let spread_handle = Handle::new(shared(SimpleQuote::new(spread)) as Shared<dyn Quote>);
let helper_s = SwapRateHelper::with_details(
Handle::new(shared(SimpleQuote::new(0.02)) as Shared<dyn Quote>),
tenor,
calendar.clone(),
Frequency::Annual,
convention,
Actual360::new(),
&source,
spread_handle,
Period::new(0, TimeUnit::Days),
None,
Pillar::LastRelevantDate,
);
helper_s.set_term_structure(&curve);
let implied_s = helper_s.implied_quote().unwrap();
assert!(
(implied_s - implied0).abs() > 1e-8,
"the spread must move the implied quote"
);
let mut independent =
independent_swap(&source, tenor, calendar, convention, &curve, settings);
let floating_bps = independent
.fixed_vs_floating_mut()
.floating_leg_bps()
.unwrap();
let fixed_bps = independent.fixed_vs_floating_mut().fixed_leg_bps().unwrap();
let expected = implied0 - spread * floating_bps / fixed_bps;
assert!(
(implied_s - expected).abs() < 1e-12,
"implied_s {implied_s} vs expected {expected}"
);
}
#[test]
fn moving_the_curve_updates_the_quote_without_notifying_the_helper() {
let (_settings, source) = swap_setup();
let tenor = Period::new(5, TimeUnit::Years);
let helper = SwapRateHelper::from_rate(
0.02,
tenor,
Target::new(),
Frequency::Annual,
BusinessDayConvention::ModifiedFollowing,
Actual360::new(),
&source,
);
let quote = shared(SimpleQuote::new(0.03));
let curve: Shared<dyn YieldTermStructure> = shared(FlatForward::new(
today(),
Handle::new(Shared::clone("e) as Shared<dyn Quote>),
Actual360::new(),
Compounding::Continuous,
Frequency::Annual,
));
helper.set_term_structure(&curve);
let implied_before = helper.implied_quote().unwrap();
let flag = Flag::new();
helper.observable().register_observer(&as_observer(&flag));
quote.set_value(0.05);
assert!(
!Flag::is_up(&flag),
"the helper must not observe the bootstrapping curve"
);
let implied_after = helper.implied_quote().unwrap();
assert!(
(implied_after - implied_before).abs() > 1e-6,
"the forced recalculation must surface the curve move without a notification"
);
}
#[test]
fn initialize_dates_spot_starts_and_pillar_follows_the_choice() {
let (_settings, source) = swap_setup();
let tenor = Period::new(5, TimeUnit::Years);
let helper = SwapRateHelper::with_details(
Handle::new(shared(SimpleQuote::new(0.02)) as Shared<dyn Quote>),
tenor,
Target::new(),
Frequency::Annual,
BusinessDayConvention::ModifiedFollowing,
Actual360::new(),
&source,
Handle::empty(),
Period::new(0, TimeUnit::Days),
None,
Pillar::MaturityDate,
);
assert!(
helper.earliest_date() > today(),
"the swap starts spot, past today"
);
assert!(helper.maturity_date() > helper.earliest_date());
assert_eq!(
helper.pillar_date(),
helper.maturity_date(),
"the MaturityDate pillar equals the maturity"
);
}
#[test]
fn swap_quote_error_is_market_minus_implied() {
let (_settings, source) = swap_setup();
let tenor = Period::new(5, TimeUnit::Years);
let helper = SwapRateHelper::from_rate(
0.05,
tenor,
Target::new(),
Frequency::Annual,
BusinessDayConvention::ModifiedFollowing,
Actual360::new(),
&source,
);
let curve = flat_curve(today(), 0.03);
helper.set_term_structure(&curve);
let implied = helper.implied_quote().unwrap();
assert!((helper.quote_error().unwrap() - (0.05 - implied)).abs() < 1e-15);
}
const ESTR_SWAP_DATA: [(i32, TimeUnit, Real); 33] = [
(1, TimeUnit::Weeks, 1.245),
(2, TimeUnit::Weeks, 1.269),
(3, TimeUnit::Weeks, 1.277),
(1, TimeUnit::Months, 1.281),
(2, TimeUnit::Months, 1.18),
(3, TimeUnit::Months, 1.143),
(4, TimeUnit::Months, 1.125),
(5, TimeUnit::Months, 1.116),
(6, TimeUnit::Months, 1.111),
(7, TimeUnit::Months, 1.109),
(8, TimeUnit::Months, 1.111),
(9, TimeUnit::Months, 1.117),
(10, TimeUnit::Months, 1.129),
(11, TimeUnit::Months, 1.141),
(12, TimeUnit::Months, 1.153),
(15, TimeUnit::Months, 1.218),
(18, TimeUnit::Months, 1.308),
(21, TimeUnit::Months, 1.407),
(2, TimeUnit::Years, 1.510),
(3, TimeUnit::Years, 1.916),
(4, TimeUnit::Years, 2.254),
(5, TimeUnit::Years, 2.523),
(6, TimeUnit::Years, 2.746),
(7, TimeUnit::Years, 2.934),
(8, TimeUnit::Years, 3.092),
(9, TimeUnit::Years, 3.231),
(10, TimeUnit::Years, 3.380),
(11, TimeUnit::Years, 3.457),
(12, TimeUnit::Years, 3.544),
(15, TimeUnit::Years, 3.702),
(20, TimeUnit::Years, 3.703),
(25, TimeUnit::Years, 3.541),
(30, TimeUnit::Years, 3.369),
];
#[test]
fn ois_bootstrap_reprices_the_quotes() {
use crate::indexes::ibor::Estr;
use crate::math::interpolations::loglinear::LogLinear;
use crate::termstructures::bootstraptraits::Discount;
use crate::termstructures::yields::PiecewiseYieldCurve;
use crate::time::daycounters::actual365fixed::Actual365Fixed;
const PAYMENT_LAG: Integer = 2;
let today = Date::new(5, Month::February, 2009);
let settings = settings_on(today);
let calendar = Target::new();
let settlement = calendar.advance(
today,
2,
TimeUnit::Days,
BusinessDayConvention::Following,
false,
);
let estr = Estr::new(Handle::empty(), settings.clone());
let mut instruments: Vec<Shared<dyn RateHelper>> = Vec::new();
for (n, unit, rate) in ESTR_SWAP_DATA {
let quote = Handle::new(shared(SimpleQuote::new(rate / 100.0)) as Shared<dyn Quote>);
let helper = OISRateHelper::new(
2,
Period::new(n, unit),
quote,
&estr,
None,
PAYMENT_LAG,
BusinessDayConvention::Following,
Frequency::Annual,
Period::new(0, TimeUnit::Days),
Handle::empty(),
Pillar::LastRelevantDate,
RateAveraging::Compound,
settings.clone(),
);
instruments.push(helper as Shared<dyn RateHelper>);
}
let curve = PiecewiseYieldCurve::<Discount, LogLinear>::new(
today,
instruments,
Actual365Fixed::new(),
LogLinear,
)
.unwrap();
let handle: Handle<dyn YieldTermStructure> =
Handle::new(Shared::clone(&curve) as Shared<dyn YieldTermStructure>);
for (n, unit, rate) in ESTR_SWAP_DATA {
let priced_estr = shared(Estr::new(handle.clone(), settings.clone()));
let mut swap = MakeOis::new(
Period::new(n, unit),
priced_estr,
Some(0.0),
Period::new(0, TimeUnit::Days),
settings.clone(),
)
.with_effective_date(settlement)
.with_nominal(100.0)
.with_payment_lag(PAYMENT_LAG)
.with_discounting_term_structure(handle.clone())
.with_averaging_method(RateAveraging::Compound)
.build()
.unwrap();
let calculated = swap.fixed_vs_floating_mut().fair_rate().unwrap();
let expected = rate / 100.0;
assert!(
(calculated - expected).abs() < 1.0e-8,
"{n} {unit:?} OIS: calculated {calculated} vs expected {expected}"
);
}
}
}