use std::cell::{Cell, Ref, RefCell};
use std::rc::Weak;
use crate::errors::{QlError, QlResult};
use crate::handle::{Handle, RelinkableHandle};
use crate::instrument::Instrument;
use crate::instruments::{CdsTerms, CreditDefaultSwap, PricingModel, ProtectionSide, cds_maturity};
use crate::patterns::observable::{AsObservable, Observable};
use crate::pricingengine::PricingEngine;
use crate::pricingengines::credit::MidPointCdsEngine;
use crate::quotes::Quote;
use crate::settings::Settings;
use crate::shared::{Shared, SharedMut, shared_mut};
use crate::termstructures::bootstraphelper::{BootstrapHelperBase, BootstrapHelperShared};
use crate::termstructures::credit::defaulttermstructure::DefaultProbabilityTermStructure;
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::schedule::{MakeSchedule, Schedule};
use crate::time::timeunit::TimeUnit;
use crate::types::{Integer, Natural, Rate, Real};
use crate::{fail, require};
pub type DefaultProbabilityHelperBase = BootstrapHelperBase<dyn DefaultProbabilityTermStructure>;
pub trait DefaultProbabilityHelper: AsObservable {
fn base(&self) -> &DefaultProbabilityHelperBase;
fn implied_quote(&self) -> QlResult<Real>;
fn quote(&self) -> &Handle<dyn Quote> {
self.base().quote()
}
fn quote_error(&self) -> QlResult<Real> {
Ok(self.base().quote_value()? - self.implied_quote()?)
}
fn set_term_structure(&self, term_structure: &Shared<dyn DefaultProbabilityTermStructure>) {
self.base().set_term_structure(term_structure);
}
fn earliest_date(&self) -> Date {
self.base().earliest_date()
}
fn maturity_date(&self) -> Date {
self.base().maturity_date()
}
fn latest_relevant_date(&self) -> Date {
self.base().latest_relevant_date()
}
fn pillar_date(&self) -> Date {
self.base().pillar_date()
}
fn latest_date(&self) -> Date {
self.base().latest_date()
}
}
pub trait RelativeDateDefaultProbabilityHelper: DefaultProbabilityHelper {
fn initialize_dates(&self) -> QlResult<()>;
}
impl BootstrapHelperShared for dyn DefaultProbabilityHelper {
type TS = dyn DefaultProbabilityTermStructure;
fn set_term_structure(&self, term_structure: &Shared<dyn DefaultProbabilityTermStructure>) {
DefaultProbabilityHelper::set_term_structure(self, term_structure);
}
fn quote_value(&self) -> QlResult<Real> {
self.base().quote_value()
}
fn quote_error(&self) -> QlResult<Real> {
DefaultProbabilityHelper::quote_error(self)
}
fn pillar_date(&self) -> Date {
DefaultProbabilityHelper::pillar_date(self)
}
fn latest_relevant_date(&self) -> Date {
DefaultProbabilityHelper::latest_relevant_date(self)
}
fn maturity_date(&self) -> Date {
DefaultProbabilityHelper::maturity_date(self)
}
}
pub struct CdsHelperTerms {
pub model: PricingModel,
pub settles_accrual: bool,
pub pays_at_default_time: bool,
pub start_date: Option<Date>,
pub last_period_day_counter: Option<DayCounter>,
pub rebates_accrual: bool,
}
impl Default for CdsHelperTerms {
fn default() -> CdsHelperTerms {
CdsHelperTerms {
model: PricingModel::Midpoint,
settles_accrual: true,
pays_at_default_time: true,
start_date: None,
last_period_day_counter: None,
rebates_accrual: true,
}
}
}
pub struct CdsHelperBase {
base: DefaultProbabilityHelperBase,
tenor: Period,
settlement_days: Integer,
calendar: Calendar,
frequency: Frequency,
payment_convention: BusinessDayConvention,
rule: DateGeneration,
day_counter: DayCounter,
recovery_rate: Real,
discount_curve: Handle<dyn YieldTermStructure>,
settles_accrual: bool,
pays_at_default_time: bool,
start_date: Option<Date>,
last_period_day_counter: Option<DayCounter>,
rebates_accrual: bool,
model: PricingModel,
settings: Shared<Settings<Date>>,
schedule: RefCell<Schedule>,
protection_start: Cell<Date>,
probability: RelinkableHandle<dyn DefaultProbabilityTermStructure>,
swap: RefCell<QlResult<CreditDefaultSwap>>,
}
fn engine_not_reset() -> QlError {
QlError::new(
"the helper's credit default swap is built when the bootstrapping curve is set",
file!(),
line!(),
)
}
impl CdsHelperBase {
#[allow(clippy::too_many_arguments)]
fn new(
quote: Handle<dyn Quote>,
tenor: Period,
settlement_days: Integer,
calendar: Calendar,
frequency: Frequency,
payment_convention: BusinessDayConvention,
rule: DateGeneration,
day_counter: DayCounter,
recovery_rate: Real,
discount_curve: Handle<dyn YieldTermStructure>,
terms: CdsHelperTerms,
settings: Shared<Settings<Date>>,
on_eval_change: Box<dyn Fn()>,
) -> CdsHelperBase {
let base = BootstrapHelperBase::new_relative(
quote,
Shared::clone(&settings),
true,
on_eval_change,
);
discount_curve.register_observer(&base.observer());
CdsHelperBase {
base,
tenor,
settlement_days,
calendar,
frequency,
payment_convention,
rule,
day_counter,
recovery_rate,
discount_curve,
settles_accrual: terms.settles_accrual,
pays_at_default_time: terms.pays_at_default_time,
start_date: terms.start_date,
last_period_day_counter: terms.last_period_day_counter,
rebates_accrual: terms.rebates_accrual,
model: terms.model,
settings,
schedule: RefCell::new(Schedule::from_dates(Vec::new())),
protection_start: Cell::new(Date::null()),
probability: RelinkableHandle::empty(),
swap: RefCell::new(Err(engine_not_reset())),
}
}
pub fn base(&self) -> &DefaultProbabilityHelperBase {
&self.base
}
pub fn swap(&self) -> Ref<'_, QlResult<CreditDefaultSwap>> {
self.swap.borrow()
}
pub fn schedule(&self) -> Ref<'_, Schedule> {
self.schedule.borrow()
}
pub fn protection_start(&self) -> Date {
self.protection_start.get()
}
fn evaluation_date(&self) -> Date {
self.base
.evaluation_date()
.expect("a relative-date helper always tracks an evaluation date")
}
fn set_swap(&self, swap: QlResult<CreditDefaultSwap>) {
*self.swap.borrow_mut() = swap;
}
fn set_term_structure(&self, term_structure: &Shared<dyn DefaultProbabilityTermStructure>) {
self.base.set_term_structure(term_structure);
self.probability
.link_to_weak(Shared::downgrade(term_structure));
}
fn contract_terms(&self) -> CdsTerms {
CdsTerms {
settles_accrual: self.settles_accrual,
pays_at_default_time: self.pays_at_default_time,
protection_start: Some(self.protection_start.get()),
last_period_day_counter: self.last_period_day_counter.clone(),
rebates_accrual: self.rebates_accrual,
trade_date: Some(self.evaluation_date()),
..CdsTerms::default()
}
}
fn install_engine(&self, swap: &mut CreditDefaultSwap) -> QlResult<()> {
require!(
self.model == PricingModel::Midpoint,
"the ISDA arm of resetEngine (defaultprobabilityhelpers.cpp:143-148) needs the \
IsdaCdsEngine, which is not ported yet (#783)"
);
let engine = MidPointCdsEngine::new(
self.probability.handle(),
self.recovery_rate,
self.discount_curve.clone(),
None,
Shared::clone(&self.settings),
);
swap.base_mut()
.set_pricing_engine(shared_mut(engine) as SharedMut<dyn PricingEngine>);
Ok(())
}
fn initialize_dates(&self) -> QlResult<()> {
let evaluation_date = self.evaluation_date();
let protection_start = evaluation_date + self.settlement_days;
self.protection_start.set(protection_start);
let mut start_date = self.start_date.unwrap_or(protection_start);
if self.rule != DateGeneration::CDS && self.rule != DateGeneration::CDS2015 {
start_date = self.calendar.adjust(start_date, self.payment_convention);
}
let end_date = if matches!(
self.rule,
DateGeneration::CDS2015 | DateGeneration::CDS | DateGeneration::OldCDS
) {
let reference_date = self.start_date.unwrap_or(evaluation_date);
match cds_maturity(reference_date, self.tenor, self.rule)? {
Some(date) => date,
None => fail!(
"the CDS2015 contract quoted at a zero tenor on {reference_date} has already \
matured (creditdefaultswap.cpp:494)"
),
}
} else {
let reference_date = match self.start_date {
Some(date) => date + self.settlement_days,
None => protection_start,
};
reference_date + self.tenor
};
let schedule = MakeSchedule::new()
.from(start_date)
.to(end_date)
.with_frequency(self.frequency)
.with_calendar(self.calendar.clone())
.with_convention(self.payment_convention)
.with_termination_date_convention(BusinessDayConvention::Unadjusted)
.with_rule(self.rule)
.build();
let mut latest_date = self
.calendar
.adjust(schedule.date(schedule.len() - 1), self.payment_convention);
if self.model == PricingModel::Isda {
latest_date += 1;
}
self.base.set_earliest_date(schedule.date(0));
self.base.set_latest_date(latest_date);
*self.schedule.borrow_mut() = schedule;
Ok(())
}
}
pub struct SpreadCdsHelper {
cds: CdsHelperBase,
}
impl SpreadCdsHelper {
#[allow(clippy::too_many_arguments)]
pub fn new(
running_spread: Handle<dyn Quote>,
tenor: Period,
settlement_days: Integer,
calendar: Calendar,
frequency: Frequency,
payment_convention: BusinessDayConvention,
rule: DateGeneration,
day_counter: DayCounter,
recovery_rate: Real,
discount_curve: Handle<dyn YieldTermStructure>,
settings: Shared<Settings<Date>>,
) -> QlResult<Shared<SpreadCdsHelper>> {
SpreadCdsHelper::with_terms(
running_spread,
tenor,
settlement_days,
calendar,
frequency,
payment_convention,
rule,
day_counter,
recovery_rate,
discount_curve,
CdsHelperTerms::default(),
settings,
)
}
#[allow(clippy::too_many_arguments)]
pub fn with_terms(
running_spread: Handle<dyn Quote>,
tenor: Period,
settlement_days: Integer,
calendar: Calendar,
frequency: Frequency,
payment_convention: BusinessDayConvention,
rule: DateGeneration,
day_counter: DayCounter,
recovery_rate: Real,
discount_curve: Handle<dyn YieldTermStructure>,
terms: CdsHelperTerms,
settings: Shared<Settings<Date>>,
) -> QlResult<Shared<SpreadCdsHelper>> {
let helper = Shared::new_cyclic(|weak: &Weak<SpreadCdsHelper>| {
let weak = weak.clone();
let on_eval_change = Box::new(move || {
if let Some(helper) = weak.upgrade() {
match helper.cds.initialize_dates() {
Ok(()) => helper.reset_engine(),
Err(error) => helper.cds.set_swap(Err(error)),
}
}
});
SpreadCdsHelper {
cds: CdsHelperBase::new(
running_spread,
tenor,
settlement_days,
calendar,
frequency,
payment_convention,
rule,
day_counter,
recovery_rate,
discount_curve,
terms,
settings,
on_eval_change,
),
}
});
helper.cds.initialize_dates()?;
Ok(helper)
}
pub fn protection_start(&self) -> Date {
self.cds.protection_start()
}
fn reset_engine(&self) {
self.cds.set_swap(self.build_swap());
}
fn build_swap(&self) -> QlResult<CreditDefaultSwap> {
let mut swap = CreditDefaultSwap::with_terms(
ProtectionSide::Buyer,
100.0,
0.01,
self.cds.schedule().clone(),
self.cds.payment_convention,
self.cds.day_counter.clone(),
self.cds.contract_terms(),
Shared::clone(&self.cds.settings),
)?;
self.cds.install_engine(&mut swap)?;
Ok(swap)
}
}
impl AsObservable for SpreadCdsHelper {
fn observable(&self) -> &Observable {
self.cds.base.observable()
}
}
impl DefaultProbabilityHelper for SpreadCdsHelper {
fn base(&self) -> &DefaultProbabilityHelperBase {
self.cds.base()
}
fn implied_quote(&self) -> QlResult<Real> {
let mut swap = self.cds.swap.borrow_mut();
let swap = swap.as_mut().map_err(|error| error.clone())?;
swap.recalculate()?;
swap.fair_spread()
}
fn set_term_structure(&self, term_structure: &Shared<dyn DefaultProbabilityTermStructure>) {
self.cds.set_term_structure(term_structure);
self.reset_engine();
}
}
impl RelativeDateDefaultProbabilityHelper for SpreadCdsHelper {
fn initialize_dates(&self) -> QlResult<()> {
self.cds.initialize_dates()
}
}
pub struct UpfrontCdsHelper {
cds: CdsHelperBase,
running_spread: Rate,
upfront_settlement_days: Natural,
upfront_date: Cell<Date>,
}
impl UpfrontCdsHelper {
#[allow(clippy::too_many_arguments)]
pub fn new(
upfront: Handle<dyn Quote>,
running_spread: Rate,
tenor: Period,
settlement_days: Integer,
calendar: Calendar,
frequency: Frequency,
payment_convention: BusinessDayConvention,
rule: DateGeneration,
day_counter: DayCounter,
recovery_rate: Real,
discount_curve: Handle<dyn YieldTermStructure>,
settings: Shared<Settings<Date>>,
) -> QlResult<Shared<UpfrontCdsHelper>> {
UpfrontCdsHelper::with_terms(
upfront,
running_spread,
tenor,
settlement_days,
calendar,
frequency,
payment_convention,
rule,
day_counter,
recovery_rate,
discount_curve,
3,
CdsHelperTerms::default(),
settings,
)
}
#[allow(clippy::too_many_arguments)]
pub fn with_terms(
upfront: Handle<dyn Quote>,
running_spread: Rate,
tenor: Period,
settlement_days: Integer,
calendar: Calendar,
frequency: Frequency,
payment_convention: BusinessDayConvention,
rule: DateGeneration,
day_counter: DayCounter,
recovery_rate: Real,
discount_curve: Handle<dyn YieldTermStructure>,
upfront_settlement_days: Natural,
terms: CdsHelperTerms,
settings: Shared<Settings<Date>>,
) -> QlResult<Shared<UpfrontCdsHelper>> {
let helper = Shared::new_cyclic(|weak: &Weak<UpfrontCdsHelper>| {
let weak = weak.clone();
let on_eval_change = Box::new(move || {
if let Some(helper) = weak.upgrade() {
match helper.initialize_dates() {
Ok(()) => helper.reset_engine(),
Err(error) => helper.cds.set_swap(Err(error)),
}
}
});
UpfrontCdsHelper {
cds: CdsHelperBase::new(
upfront,
tenor,
settlement_days,
calendar,
frequency,
payment_convention,
rule,
day_counter,
recovery_rate,
discount_curve,
terms,
settings,
on_eval_change,
),
running_spread,
upfront_settlement_days,
upfront_date: Cell::new(Date::null()),
}
});
helper.initialize_dates()?;
Ok(helper)
}
pub fn upfront_date(&self) -> Date {
self.upfront_date.get()
}
pub fn running_spread(&self) -> Rate {
self.running_spread
}
fn reset_engine(&self) {
self.cds.set_swap(self.build_swap());
}
fn build_swap(&self) -> QlResult<CreditDefaultSwap> {
let mut swap = CreditDefaultSwap::with_upfront_and_terms(
ProtectionSide::Buyer,
100.0,
0.01,
self.running_spread,
self.cds.schedule().clone(),
self.cds.payment_convention,
self.cds.day_counter.clone(),
CdsTerms {
upfront_date: Some(self.upfront_date.get()),
..self.cds.contract_terms()
},
Shared::clone(&self.cds.settings),
)?;
self.cds.install_engine(&mut swap)?;
Ok(swap)
}
fn fair_upfront(&self) -> QlResult<Real> {
let mut swap = self.cds.swap.borrow_mut();
let swap = swap.as_mut().map_err(|error| error.clone())?;
swap.recalculate()?;
swap.fair_upfront()
}
}
impl AsObservable for UpfrontCdsHelper {
fn observable(&self) -> &Observable {
self.cds.base.observable()
}
}
impl DefaultProbabilityHelper for UpfrontCdsHelper {
fn base(&self) -> &DefaultProbabilityHelperBase {
self.cds.base()
}
fn implied_quote(&self) -> QlResult<Real> {
let restore = self.cds.settings.include_todays_cash_flows();
self.cds.settings.set_include_todays_cash_flows(Some(true));
let quote = self.fair_upfront();
self.cds.settings.set_include_todays_cash_flows(restore);
quote
}
fn set_term_structure(&self, term_structure: &Shared<dyn DefaultProbabilityTermStructure>) {
self.cds.set_term_structure(term_structure);
self.reset_engine();
}
}
impl RelativeDateDefaultProbabilityHelper for UpfrontCdsHelper {
fn initialize_dates(&self) -> QlResult<()> {
self.cds.initialize_dates()?;
let upfront_date = self.cds.calendar.advance(
self.cds.evaluation_date(),
self.upfront_settlement_days as Integer,
TimeUnit::Days,
self.cds.payment_convention,
false,
);
self.upfront_date.set(upfront_date);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn credit_helpers_satisfy_the_driver_bound() {
fn accepts_driver_helper<H>()
where
H: BootstrapHelperShared<TS = dyn DefaultProbabilityTermStructure> + ?Sized,
{
}
accepts_driver_helper::<dyn DefaultProbabilityHelper>();
}
use crate::event::Event;
use crate::interestrate::Compounding;
use crate::quotes::SimpleQuote;
use crate::shared::shared;
use crate::termstructures::credit::flathazardrate::FlatHazardRate;
use crate::termstructures::yields::FlatForward;
use crate::test_support::{Flag, as_observer};
use crate::time::calendars::target::Target;
use crate::time::date::Month;
use crate::time::daycounters::actual360::Actual360;
use crate::time::daycounters::actual365fixed::Actual365Fixed;
use crate::time::timeunit::TimeUnit;
fn today() -> Date {
Date::new(15, Month::June, 2026)
}
fn five_years() -> Period {
Period::new(5, TimeUnit::Years)
}
fn settings_at(evaluation_date: Date) -> Shared<Settings<Date>> {
let settings = shared(Settings::new());
settings.set_evaluation_date(evaluation_date);
settings
}
fn discount(settlement: Date) -> Handle<dyn YieldTermStructure> {
Handle::new(shared(FlatForward::with_rate(
settlement,
0.03,
Actual365Fixed::new(),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>)
}
fn helper(settings: &Shared<Settings<Date>>, terms: CdsHelperTerms) -> Shared<SpreadCdsHelper> {
SpreadCdsHelper::with_terms(
Handle::new(shared(SimpleQuote::new(0.01)) as Shared<dyn Quote>),
five_years(),
1,
Target::new(),
Frequency::Quarterly,
BusinessDayConvention::Following,
DateGeneration::TwentiethIMM,
Actual360::new(),
0.4,
discount(today()),
terms,
Shared::clone(settings),
)
.unwrap()
}
fn expected_schedule(start_date: Date, end_date: Date) -> Schedule {
MakeSchedule::new()
.from(Target::new().adjust(start_date, BusinessDayConvention::Following))
.to(end_date)
.with_frequency(Frequency::Quarterly)
.with_calendar(Target::new())
.with_convention(BusinessDayConvention::Following)
.with_termination_date_convention(BusinessDayConvention::Unadjusted)
.with_rule(DateGeneration::TwentiethIMM)
.build()
}
fn last_date(schedule: &Schedule) -> Date {
schedule.date(schedule.len() - 1)
}
#[test]
fn initialize_dates_spans_protection_start_to_the_tenor() {
let settings = settings_at(today());
let helper = helper(&settings, CdsHelperTerms::default());
let calendar = Target::new();
let protection_start = today() + 1;
assert_eq!(helper.protection_start(), protection_start);
let schedule = expected_schedule(protection_start, protection_start + five_years());
assert_eq!(
helper.earliest_date(),
calendar.adjust(protection_start, BusinessDayConvention::Following)
);
assert_eq!(helper.earliest_date(), schedule.date(0));
assert_eq!(
helper.latest_date(),
calendar.adjust(last_date(&schedule), BusinessDayConvention::Following)
);
}
#[test]
fn the_node_sits_on_the_rolled_maturity() {
let settings = settings_at(today());
let helper = helper(&settings, CdsHelperTerms::default());
let schedule = expected_schedule(today() + 1, today() + 1 + five_years());
let rolled = Target::new().adjust(last_date(&schedule), BusinessDayConvention::Following);
assert_eq!(helper.latest_date(), rolled);
assert_eq!(helper.pillar_date(), rolled);
assert_eq!(helper.latest_relevant_date(), rolled);
assert_eq!(helper.maturity_date(), rolled);
}
#[test]
fn an_explicit_start_date_offsets_the_maturity_by_the_settlement_days() {
let settings = settings_at(today());
let start_date = Date::new(20, Month::March, 2026);
let helper = helper(
&settings,
CdsHelperTerms {
start_date: Some(start_date),
..CdsHelperTerms::default()
},
);
let schedule = expected_schedule(start_date, start_date + 1 + five_years());
assert_eq!(helper.earliest_date(), schedule.date(0));
assert_eq!(
helper.latest_date(),
Target::new().adjust(last_date(&schedule), BusinessDayConvention::Following)
);
assert_eq!(helper.protection_start(), today() + 1);
}
#[test]
fn an_evaluation_date_move_rebuilds_the_schedule() {
let settings = settings_at(today());
let helper = helper(&settings, CdsHelperTerms::default());
let (earliest, latest) = (helper.earliest_date(), helper.latest_date());
let moved = Date::new(15, Month::December, 2026);
settings.set_evaluation_date(moved);
assert_eq!(helper.protection_start(), moved + 1);
assert!(helper.earliest_date() > earliest);
assert!(helper.latest_date() > latest);
assert_eq!(
helper.earliest_date(),
Target::new().adjust(moved + 1, BusinessDayConvention::Following)
);
}
#[test]
fn the_cds_rules_roll_the_maturity_off_the_evaluation_date() {
let settings = settings_at(today());
let calendar = Target::new();
let anchor = Date::new(20, Month::March, 2026);
for (years, maturity) in [
(2, Date::new(20, Month::June, 2028)),
(3, Date::new(20, Month::June, 2029)),
(5, Date::new(20, Month::June, 2031)),
(7, Date::new(20, Month::June, 2033)),
] {
let helper = SpreadCdsHelper::new(
Handle::new(shared(SimpleQuote::new(0.01)) as Shared<dyn Quote>),
Period::new(years, TimeUnit::Years),
1,
calendar.clone(),
Frequency::Quarterly,
BusinessDayConvention::Following,
DateGeneration::CDS,
Actual360::new(),
0.4,
discount(today()),
Shared::clone(&settings),
)
.unwrap();
let schedule = helper.cds.schedule();
assert_eq!(schedule.date(0), anchor);
assert_eq!(last_date(&schedule), maturity);
assert_eq!(helper.earliest_date(), anchor);
assert_eq!(
helper.latest_date(),
calendar.adjust(maturity, BusinessDayConvention::Following)
);
assert_eq!(helper.protection_start(), today() + 1);
}
}
#[test]
fn the_cds_reference_date_is_the_evaluation_date_not_the_protection_start() {
let evaluation_date = Date::new(19, Month::March, 2026);
let calendar = Target::new();
let helper = SpreadCdsHelper::new(
Handle::new(shared(SimpleQuote::new(0.01)) as Shared<dyn Quote>),
five_years(),
1,
calendar.clone(),
Frequency::Quarterly,
BusinessDayConvention::Following,
DateGeneration::CDS,
Actual360::new(),
0.4,
discount(evaluation_date),
settings_at(evaluation_date),
)
.unwrap();
let maturity = Date::new(20, Month::March, 2031);
assert_eq!(helper.protection_start(), Date::new(20, Month::March, 2026));
assert_eq!(last_date(&helper.cds.schedule()), maturity);
assert_eq!(
helper.latest_date(),
calendar.adjust(maturity, BusinessDayConvention::Following)
);
assert_ne!(
helper.latest_date(),
Date::new(20, Month::June, 2031),
"the maturity rolled off the protection start rather than the \
evaluation date"
);
}
#[test]
fn implied_quote_reprices_a_curve_it_does_not_observe() {
let settings = settings_at(today());
let helper = helper(&settings, CdsHelperTerms::default());
let hazard = shared(SimpleQuote::new(0.02));
let curve: Shared<dyn DefaultProbabilityTermStructure> = shared(FlatHazardRate::new(
today(),
Handle::new(Shared::clone(&hazard) as Shared<dyn Quote>),
Actual365Fixed::new(),
));
helper.set_term_structure(&curve);
let first = helper.implied_quote().unwrap();
assert!(first.is_finite() && first > 0.0);
assert_eq!(helper.implied_quote().unwrap(), first);
hazard.set_value(0.05);
assert!(
helper.cds.swap().as_ref().unwrap().base().is_calculated(),
"the weak link must leave the contract unnotified by the curve"
);
let second = helper.implied_quote().unwrap();
assert!(
second > first,
"a higher hazard rate must widen the fair spread, not repeat {first}"
);
}
#[test]
fn an_evaluation_date_move_rebuilds_the_contract_too() {
let settings = settings_at(today());
let helper = helper(&settings, CdsHelperTerms::default());
let curve: Shared<dyn DefaultProbabilityTermStructure> = shared(FlatHazardRate::with_rate(
today(),
0.02,
Actual365Fixed::new(),
));
helper.set_term_structure(&curve);
helper.implied_quote().unwrap();
let moved = Date::new(15, Month::December, 2026);
settings.set_evaluation_date(moved);
let schedule = expected_schedule(moved + 1, moved + 1 + five_years());
let swap = helper.cds.swap();
let swap = swap.as_ref().unwrap();
assert_eq!(swap.protection_start_date(), moved + 1);
assert_eq!(swap.maturity(), last_date(&schedule));
}
#[test]
fn a_discount_curve_move_notifies_the_helper() {
let settings = settings_at(today());
let rate = shared(SimpleQuote::new(0.03));
let discount_curve = Handle::new(shared(FlatForward::new(
today(),
Handle::new(Shared::clone(&rate) as Shared<dyn Quote>),
Actual365Fixed::new(),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>);
let helper = SpreadCdsHelper::new(
Handle::new(shared(SimpleQuote::new(0.01)) as Shared<dyn Quote>),
five_years(),
1,
Target::new(),
Frequency::Quarterly,
BusinessDayConvention::Following,
DateGeneration::TwentiethIMM,
Actual360::new(),
0.4,
discount_curve,
Shared::clone(&settings),
)
.unwrap();
let flag = Flag::new();
helper.observable().register_observer(&as_observer(&flag));
rate.set_value(0.04);
assert!(Flag::is_up(&flag));
}
#[test]
fn a_matured_zero_tenor_is_refused_rather_than_scheduled() {
let settings = settings_at(Date::new(15, Month::July, 2026));
let result = SpreadCdsHelper::new(
Handle::new(shared(SimpleQuote::new(0.01)) as Shared<dyn Quote>),
Period::new(0, TimeUnit::Months),
1,
Target::new(),
Frequency::Quarterly,
BusinessDayConvention::Following,
DateGeneration::CDS2015,
Actual360::new(),
0.4,
discount(today()),
settings,
);
assert!(
result
.err()
.is_some_and(|error| error.message().contains("has already matured"))
);
}
#[test]
fn the_isda_model_moves_the_pillar_and_defers_the_engine() {
let settings = settings_at(today());
let isda = helper(
&settings,
CdsHelperTerms {
model: PricingModel::Isda,
..CdsHelperTerms::default()
},
);
assert_eq!(
isda.latest_date(),
helper(&settings, CdsHelperTerms::default()).latest_date() + 1
);
isda.set_term_structure(&hazard_curve());
assert!(
isda.implied_quote()
.err()
.is_some_and(|error| error.message().contains("#783"))
);
}
fn upfront_helper(settings: &Shared<Settings<Date>>, lag: Natural) -> Shared<UpfrontCdsHelper> {
UpfrontCdsHelper::with_terms(
Handle::new(shared(SimpleQuote::new(0.01)) as Shared<dyn Quote>),
0.05,
five_years(),
1,
Target::new(),
Frequency::Quarterly,
BusinessDayConvention::Following,
DateGeneration::CDS,
Actual360::new(),
0.4,
discount(today()),
lag,
CdsHelperTerms::default(),
Shared::clone(settings),
)
.unwrap()
}
fn hazard_curve() -> Shared<dyn DefaultProbabilityTermStructure> {
shared(FlatHazardRate::with_rate(
today(),
0.02,
Actual365Fixed::new(),
))
}
#[test]
fn the_upfront_settles_on_the_helpers_own_lag() {
let settings = settings_at(today());
let helper = upfront_helper(&settings, 5);
let calendar = Target::new();
let expected = calendar.advance(
today(),
5,
TimeUnit::Days,
BusinessDayConvention::Following,
false,
);
assert_ne!(
expected,
calendar.advance(
today(),
3,
TimeUnit::Days,
BusinessDayConvention::Following,
false
),
"the fixture's lag must differ from the contract's own default, or \
this pins nothing"
);
assert_eq!(helper.upfront_date(), expected);
helper.set_term_structure(&hazard_curve());
let swap = helper.cds.swap();
let swap = swap.as_ref().unwrap();
assert_eq!(swap.upfront_payment().date(), expected);
assert_eq!(swap.running_spread(), 0.05);
assert_eq!(swap.trade_date(), today());
}
#[test]
fn an_evaluation_date_move_rebuilds_the_upfront_date() {
let settings = settings_at(today());
let helper = upfront_helper(&settings, 5);
helper.set_term_structure(&hazard_curve());
let moved = Date::new(15, Month::December, 2026);
settings.set_evaluation_date(moved);
let expected = Target::new().advance(
moved,
5,
TimeUnit::Days,
BusinessDayConvention::Following,
false,
);
assert_eq!(helper.upfront_date(), expected);
assert_eq!(
helper.cds.swap().as_ref().unwrap().upfront_payment().date(),
expected
);
}
#[test]
fn the_cash_flow_flag_is_restored_when_the_contract_cannot_price() {
let settings = settings_at(today());
settings.set_include_todays_cash_flows(Some(false));
let helper = upfront_helper(&settings, 3);
assert!(
helper
.implied_quote()
.err()
.is_some_and(|error| error.message().contains("bootstrapping curve is set"))
);
assert_eq!(settings.include_todays_cash_flows(), Some(false));
}
#[test]
fn implied_quote_without_a_curve_reports_the_missing_contract() {
let settings = settings_at(today());
let helper = helper(&settings, CdsHelperTerms::default());
assert!(
helper
.implied_quote()
.err()
.is_some_and(|error| error.message().contains("bootstrapping curve is set"))
);
}
}