use std::cell::{Cell, RefCell};
use std::rc::Weak;
use crate::errors::{QlError, QlResult};
use crate::handle::{Handle, RelinkableHandle};
use crate::instrument::Instrument;
use crate::instruments::{CdsTerms, CreditDefaultSwap, ProtectionSide};
use crate::patterns::observable::{AsObservable, Observable};
use crate::pricingengine::PricingEngine;
use crate::pricingengines::credit::MidPointCdsEngine;
use crate::quotes::Quote;
use crate::require;
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::types::{Integer, Real};
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);
}
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 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 {
settles_accrual: true,
pays_at_default_time: true,
start_date: None,
last_period_day_counter: None,
rebates_accrual: true,
}
}
}
pub struct SpreadCdsHelper {
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,
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 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>> {
require!(
!matches!(
rule,
DateGeneration::CDS | DateGeneration::CDS2015 | DateGeneration::OldCDS
),
"the post-Big-Bang date-generation rules need cdsMaturity, which is not ported yet \
(defaultprobabilityhelpers.cpp:85-88)"
);
Ok(Shared::new_cyclic(|weak: &Weak<SpreadCdsHelper>| {
let weak = weak.clone();
let on_eval_change = Box::new(move || {
if let Some(helper) = weak.upgrade() {
helper.initialize_dates();
helper.reset_engine();
}
});
let base = BootstrapHelperBase::new_relative(
running_spread,
Shared::clone(&settings),
true,
on_eval_change,
);
discount_curve.register_observer(&base.observer());
let helper = SpreadCdsHelper {
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,
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())),
};
helper.initialize_dates();
helper
}))
}
pub fn protection_start(&self) -> Date {
self.protection_start.get()
}
fn reset_engine(&self) {
*self.swap.borrow_mut() = self.build_swap();
}
fn build_swap(&self) -> QlResult<CreditDefaultSwap> {
let mut swap = CreditDefaultSwap::with_terms(
ProtectionSide::Buyer,
100.0,
0.01,
self.schedule.borrow().clone(),
self.payment_convention,
self.day_counter.clone(),
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,
..CdsTerms::default()
},
Shared::clone(&self.settings),
)?;
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(swap)
}
}
impl AsObservable for SpreadCdsHelper {
fn observable(&self) -> &Observable {
self.base.observable()
}
}
impl DefaultProbabilityHelper for SpreadCdsHelper {
fn base(&self) -> &DefaultProbabilityHelperBase {
&self.base
}
fn implied_quote(&self) -> QlResult<Real> {
let mut swap = self.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.base.set_term_structure(term_structure);
self.probability
.link_to_weak(Shared::downgrade(term_structure));
self.reset_engine();
}
}
impl RelativeDateDefaultProbabilityHelper for SpreadCdsHelper {
fn initialize_dates(&self) {
let evaluation_date = self
.base
.evaluation_date()
.expect("a relative-date helper always tracks an 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 reference_date = match self.start_date {
Some(date) => date + self.settlement_days,
None => protection_start,
};
let end_date = 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();
self.base.set_earliest_date(schedule.date(0));
self.base.set_latest_date(
self.calendar
.adjust(schedule.date(schedule.len() - 1), self.payment_convention),
);
*self.schedule.borrow_mut() = schedule;
}
}
#[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::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_post_big_bang_rules_are_refused() {
let settings = settings_at(today());
for rule in [
DateGeneration::CDS,
DateGeneration::CDS2015,
DateGeneration::OldCDS,
] {
let result = SpreadCdsHelper::new(
Handle::new(shared(SimpleQuote::new(0.01)) as Shared<dyn Quote>),
five_years(),
1,
Target::new(),
Frequency::Quarterly,
BusinessDayConvention::Following,
rule,
Actual360::new(),
0.4,
discount(today()),
Shared::clone(&settings),
);
assert!(
result
.err()
.is_some_and(|error| { error.message().contains("cdsMaturity") })
);
}
}
#[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
.swap
.borrow()
.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.swap.borrow();
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 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"))
);
}
}