use std::cell::RefCell;
use std::marker::PhantomData;
use std::rc::Weak;
use crate::errors::QlResult;
use crate::math::interpolations::Interpolator;
use crate::patterns::lazyobject::LazyObject;
use crate::patterns::observable::{AsObservable, Observable, Observer};
use crate::require;
use crate::shared::{Shared, SharedMut, shared_mut};
use crate::termstructures::bootstraptraits::{BootstrapTraits, CurveData};
use crate::termstructures::credit::defaultprobabilityhelpers::DefaultProbabilityHelper;
use crate::termstructures::credit::defaulttermstructure::DefaultProbabilityTermStructure;
use crate::termstructures::credit::hazardratestructure::HazardRateStructure;
use crate::termstructures::credit::interpolatedhazardratecurve::{
hazard_rate_from_nodes, survival_probability_from_nodes,
};
use crate::termstructures::credit::probabilitytraits::HazardRate;
use crate::termstructures::iterativebootstrap::{IterativeBootstrap, PiecewiseCurve};
use crate::termstructures::{TermStructure, TermStructureBase};
use crate::time::date::Date;
use crate::time::daycounter::DayCounter;
use crate::types::{Probability, Rate, Real, Time};
struct CurveUpdater {
lazy: SharedMut<LazyObject>,
}
impl Observer for CurveUpdater {
fn update(&mut self) {
if let Some(update) = LazyObject::deferred_update(&self.lazy) {
update.notify_observers();
}
}
}
pub struct PiecewiseDefaultCurve<T: BootstrapTraits, I: Interpolator> {
base: TermStructureBase,
instruments: Vec<Shared<dyn DefaultProbabilityHelper>>,
interpolator: I,
data: RefCell<CurveData<I>>,
lazy: SharedMut<LazyObject>,
observable: Shared<Observable>,
updater: SharedMut<CurveUpdater>,
bootstrap: IterativeBootstrap,
accuracy: Real,
self_weak: Weak<dyn DefaultProbabilityTermStructure>,
_traits: PhantomData<fn() -> T>,
}
impl<I: Interpolator + 'static> PiecewiseDefaultCurve<HazardRate, I> {
pub fn new(
reference_date: Date,
instruments: Vec<Shared<dyn DefaultProbabilityHelper>>,
day_counter: DayCounter,
interpolator: I,
) -> QlResult<Shared<PiecewiseDefaultCurve<HazardRate, I>>> {
require!(!instruments.is_empty(), "no bootstrap helpers given");
let curve = Shared::new_cyclic(|weak: &Weak<PiecewiseDefaultCurve<HazardRate, I>>| {
let self_weak: Weak<dyn DefaultProbabilityTermStructure> = weak.clone();
let lazy = shared_mut(LazyObject::new(true));
let observable = lazy.borrow().observable_handle();
let updater = shared_mut(CurveUpdater {
lazy: SharedMut::clone(&lazy),
});
PiecewiseDefaultCurve {
base: TermStructureBase::with_reference_date(
reference_date,
None,
Some(day_counter),
),
instruments,
interpolator,
data: RefCell::new(CurveData::new()),
lazy,
observable,
updater,
bootstrap: IterativeBootstrap::new(),
accuracy: 1.0e-12,
self_weak,
_traits: PhantomData,
}
});
let observer = SharedMut::clone(&curve.updater) as SharedMut<dyn Observer>;
for helper in &curve.instruments {
helper.observable().register_observer(&observer);
}
Ok(curve)
}
pub fn calculate(&self) -> QlResult<()> {
if self.lazy.borrow().is_calculated() {
return Ok(());
}
if !self.lazy.borrow_mut().start_calculation() {
return Ok(());
}
let result = self.bootstrap.calculate(self);
self.lazy.borrow_mut().finish_calculation(&result);
result
}
pub fn times(&self) -> QlResult<Vec<Time>> {
self.calculate()?;
Ok(self.data.borrow().times().to_vec())
}
pub fn dates(&self) -> QlResult<Vec<Date>> {
self.calculate()?;
Ok(self.data.borrow().dates().to_vec())
}
pub fn data(&self) -> QlResult<Vec<Real>> {
self.calculate()?;
Ok(self.data.borrow().data().to_vec())
}
pub fn nodes(&self) -> QlResult<Vec<(Date, Real)>> {
self.calculate()?;
Ok(self.data.borrow().nodes())
}
pub fn register_observer(&self, observer: &SharedMut<dyn Observer>) -> bool {
self.observable.register_observer(observer)
}
}
impl<I: Interpolator> AsObservable for PiecewiseDefaultCurve<HazardRate, I> {
fn observable(&self) -> &Observable {
&self.observable
}
}
impl<I: Interpolator + 'static> TermStructure for PiecewiseDefaultCurve<HazardRate, I> {
fn base(&self) -> &TermStructureBase {
&self.base
}
fn max_date(&self) -> Date {
let _ = self.calculate();
self.data
.borrow()
.max_date()
.or_else(|| self.base.reference_date().ok())
.unwrap_or_else(Date::null)
}
}
impl<I: Interpolator + 'static> HazardRateStructure for PiecewiseDefaultCurve<HazardRate, I> {
fn hazard_rate_curve_impl(&self, t: Time) -> QlResult<Rate> {
self.calculate()?;
let data = self.data.borrow();
hazard_rate_from_nodes(data.interpolation()?, t)
}
}
impl<I: Interpolator + 'static> DefaultProbabilityTermStructure
for PiecewiseDefaultCurve<HazardRate, I>
{
fn as_any(&self) -> Option<&dyn std::any::Any> {
Some(self)
}
fn survival_probability_impl(&self, t: Time) -> QlResult<Probability> {
self.calculate()?;
let data = self.data.borrow();
survival_probability_from_nodes(data.interpolation()?, t)
}
fn default_density_impl(&self, t: Time) -> QlResult<Real> {
self.default_density_from_hazard_rate(t)
}
fn hazard_rate_impl(&self, t: Time) -> QlResult<Rate> {
self.hazard_rate_curve_impl(t)
}
}
impl<I: Interpolator + 'static> PiecewiseCurve for PiecewiseDefaultCurve<HazardRate, I> {
type Traits = HazardRate;
type Interp = I;
type TS = dyn DefaultProbabilityTermStructure;
type Helper = dyn DefaultProbabilityHelper;
fn instruments(&self) -> &[Shared<dyn DefaultProbabilityHelper>] {
&self.instruments
}
fn interpolator(&self) -> &I {
&self.interpolator
}
fn curve_data(&self) -> &RefCell<CurveData<I>> {
&self.data
}
fn accuracy(&self) -> Real {
self.accuracy
}
fn reference_date(&self) -> QlResult<Date> {
self.base.reference_date()
}
fn time_from_reference(&self, date: Date) -> QlResult<Time> {
TermStructure::time_from_reference(self, date)
}
fn term_structure_shared(&self) -> QlResult<Shared<dyn DefaultProbabilityTermStructure>> {
match self.self_weak.upgrade() {
Some(curve) => Ok(curve),
None => crate::fail!("curve dropped before bootstrap"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::handle::Handle;
use crate::indexes::ibor::euribor::Euribor;
use crate::instrument::Instrument;
use crate::instruments::{CdsTerms, CreditDefaultSwap, ProtectionSide, cds_maturity};
use crate::interestrate::Compounding;
use crate::math::interpolations::flat::BackwardFlat;
use crate::math::interpolations::loglinear::LogLinear;
use crate::pricingengine::PricingEngine;
use crate::pricingengines::credit::{MidPointCdsEngine, isda_node_grid};
use crate::quotes::{Quote, SimpleQuote};
use crate::settings::Settings;
use crate::shared::shared;
use crate::termstructures::RateHelper;
use crate::termstructures::bootstraptraits::Discount;
use crate::termstructures::credit::defaultprobabilityhelpers::{
CdsHelperTerms, SpreadCdsHelper, UpfrontCdsHelper,
};
use crate::termstructures::yields::{DepositRateHelper, FlatForward, PiecewiseYieldCurve};
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::time::businessdayconvention::BusinessDayConvention;
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::daycounters::thirty360::{Convention, Thirty360};
use crate::time::frequency::Frequency;
use crate::time::period::Period;
use crate::time::schedule::Schedule;
use crate::time::timeunit::TimeUnit;
use crate::types::{Integer, Natural, Rate};
const QUOTES: [Real; 4] = [0.005, 0.006, 0.007, 0.009];
const TENORS: [i32; 4] = [1, 2, 3, 5];
const RECOVERY_RATE: Real = 0.4;
const TOLERANCE: Real = 1.0e-6;
fn today() -> Date {
Date::new(9, Month::June, 2006)
}
fn settings_at(evaluation_date: Date) -> Shared<Settings<Date>> {
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(evaluation_date);
settings
}
fn day_counter() -> DayCounter {
Thirty360::with_convention(Convention::BondBasis)
}
fn discount_curve(reference_date: Date) -> Handle<dyn YieldTermStructure> {
Handle::new(shared(FlatForward::with_rate(
reference_date,
0.06,
Actual360::new(),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>)
}
fn spread_helper(
quote: Real,
tenor: Period,
settlement_days: Integer,
discount: &Handle<dyn YieldTermStructure>,
settings: &Shared<Settings<Date>>,
) -> Shared<dyn DefaultProbabilityHelper> {
SpreadCdsHelper::new(
Handle::new(shared(SimpleQuote::new(quote)) as Shared<dyn Quote>),
tenor,
settlement_days,
Target::new(),
Frequency::Quarterly,
BusinessDayConvention::Following,
DateGeneration::TwentiethIMM,
day_counter(),
RECOVERY_RATE,
discount.clone(),
Shared::clone(settings),
)
.expect("TwentiethIMM is an accepted date-generation rule")
as Shared<dyn DefaultProbabilityHelper>
}
fn round_trip_schedule(today: Date, tenor: Period) -> Schedule {
let calendar = Target::new();
let start_date = calendar.adjust(today + SETTLEMENT_DAYS, BusinessDayConvention::Following);
Schedule::new(
start_date,
today + tenor,
Period::try_from(Frequency::Quarterly).expect("a quarterly period"),
calendar,
BusinessDayConvention::Following,
BusinessDayConvention::Unadjusted,
DateGeneration::TwentiethIMM,
false,
Date::null(),
Date::null(),
)
}
fn rolled_back_date(schedule: &Schedule) -> Date {
Target::new().adjust(
schedule.date(schedule.len() - 1),
BusinessDayConvention::Following,
)
}
const SETTLEMENT_DAYS: Integer = 1;
struct Fixture {
settings: Shared<Settings<Date>>,
discount: Handle<dyn YieldTermStructure>,
helpers: Vec<Shared<dyn DefaultProbabilityHelper>>,
curve: Shared<PiecewiseDefaultCurve<HazardRate, BackwardFlat>>,
}
fn fixture() -> Fixture {
assert!(
Target::new().is_business_day(today()),
"the fixed evaluation date must be a TARGET business day, as the \
C++ fixture's clock-derived one is rolled to be"
);
let settings = settings_at(today());
settings.set_include_todays_cash_flows(Some(true));
let discount = discount_curve(today());
let helpers: Vec<Shared<dyn DefaultProbabilityHelper>> = QUOTES
.iter()
.zip(TENORS)
.map(|(quote, n)| {
spread_helper(
*quote,
Period::new(n, TimeUnit::Years),
SETTLEMENT_DAYS,
&discount,
&settings,
)
})
.collect();
let curve = PiecewiseDefaultCurve::<HazardRate, BackwardFlat>::new(
today(),
helpers.clone(),
day_counter(),
BackwardFlat,
)
.unwrap();
Fixture {
settings,
discount,
helpers,
curve,
}
}
#[test]
fn bootstrapped_curve_reproduces_the_input_cds_spreads() {
let fixture = fixture();
let curve: Handle<dyn DefaultProbabilityTermStructure> = Handle::new(Shared::clone(
&fixture.curve,
)
as Shared<dyn DefaultProbabilityTermStructure>);
for (quote, n) in QUOTES.iter().zip(TENORS) {
let tenor = Period::new(n, TimeUnit::Years);
let protection_start = today() + SETTLEMENT_DAYS;
let mut cds = CreditDefaultSwap::with_terms(
ProtectionSide::Buyer,
1.0,
*quote,
round_trip_schedule(today(), tenor),
BusinessDayConvention::Following,
day_counter(),
CdsTerms {
protection_start: Some(protection_start),
..CdsTerms::default()
},
Shared::clone(&fixture.settings),
)
.unwrap();
let engine = MidPointCdsEngine::new(
curve.clone(),
RECOVERY_RATE,
fixture.discount.clone(),
None,
Shared::clone(&fixture.settings),
);
cds.base_mut()
.set_pricing_engine(shared_mut(engine) as SharedMut<dyn PricingEngine>);
let computed = cds.fair_spread().unwrap();
assert!(
(computed - quote).abs() <= TOLERANCE,
"failed to reproduce the fair spread for the {n}Y credit-default swap: \
computed {computed}, input {quote}"
);
}
}
#[test]
fn bootstrapped_curve_feeds_the_isda_node_grid() {
let fixture = fixture();
let curve: Handle<dyn DefaultProbabilityTermStructure> = Handle::new(Shared::clone(
&fixture.curve,
)
as Shared<dyn DefaultProbabilityTermStructure>);
let pillars = fixture.curve.dates().expect("the bootstrap succeeds");
assert_eq!(
pillars.len(),
TENORS.len() + 1,
"the reference date plus a pillar per tenor"
);
let grid = isda_node_grid(&fixture.discount, &curve, today() + 10_000)
.expect("a bootstrapped backward-flat hazard-rate curve is an ISDA curve");
assert_eq!(grid, pillars);
}
#[test]
fn two_bootstrapped_curves_merge_into_one_sorted_grid() {
let fixture = fixture();
let credit: Handle<dyn DefaultProbabilityTermStructure> = Handle::new(Shared::clone(
&fixture.curve,
)
as Shared<dyn DefaultProbabilityTermStructure>);
let deposits: Vec<Shared<dyn RateHelper>> = [(3, TimeUnit::Months), (6, TimeUnit::Months)]
.iter()
.map(|(n, units)| {
let quote = Handle::new(shared(SimpleQuote::new(0.04)) as Shared<dyn Quote>);
let index = Euribor::new(
Period::new(*n, *units),
Handle::empty(),
Shared::clone(&fixture.settings),
)
.expect("the deposit tenor is valid");
DepositRateHelper::new(quote, &index) as Shared<dyn RateHelper>
})
.collect();
let yield_curve = PiecewiseYieldCurve::<Discount, LogLinear>::new(
today(),
deposits,
Actual360::new(),
LogLinear,
)
.expect("the deposit helpers bootstrap");
let yield_pillars = yield_curve.dates().expect("the yield bootstrap succeeds");
let credit_pillars = fixture
.curve
.dates()
.expect("the credit bootstrap succeeds");
assert_eq!(
yield_pillars.len(),
3,
"the reference date plus two deposits"
);
assert_eq!(
credit_pillars.len(),
TENORS.len() + 1,
"the reference date plus a pillar per tenor"
);
let grid = isda_node_grid(
&Handle::new(yield_curve as Shared<dyn YieldTermStructure>),
&credit,
today() + 10_000,
)
.expect("two bootstrapped ISDA curves are supported");
let shared_dates = yield_pillars
.iter()
.filter(|date| credit_pillars.contains(date))
.count();
assert_eq!(
shared_dates, 1,
"the two curves share only the reference date"
);
assert_eq!(
grid.len(),
yield_pillars.len() + credit_pillars.len() - shared_dates,
"the shared reference date collapses exactly once"
);
assert!(
grid.windows(2).all(|pair| pair[0] < pair[1]),
"the grid is strictly increasing: {grid:?}"
);
assert!(
grid.contains(&yield_pillars[1]) && grid.contains(&credit_pillars[1]),
"both curves contribute a pillar of their own"
);
for date in yield_pillars.iter().chain(credit_pillars.iter()) {
assert!(grid.contains(date), "the grid drops the pillar {date:?}");
}
}
#[test]
fn the_helper_and_the_round_trip_agree_on_the_imm_maturity() {
let fixture = fixture();
for (helper, n) in fixture.helpers.iter().zip(TENORS) {
let schedule = round_trip_schedule(today(), Period::new(n, TimeUnit::Years));
assert_eq!(
helper.latest_date(),
rolled_back_date(&schedule),
"the {n}Y helper's pillar and the round-trip contract's maturity diverge"
);
}
}
#[test]
fn a_single_helper_bootstraps() {
let settings = settings_at(today());
let discount = discount_curve(today());
let helper = spread_helper(
0.005,
Period::new(2, TimeUnit::Years),
0,
&discount,
&settings,
);
let curve = PiecewiseDefaultCurve::<HazardRate, BackwardFlat>::new(
today(),
vec![helper],
day_counter(),
BackwardFlat,
)
.unwrap();
curve.calculate().unwrap();
assert_eq!(
curve.dates().unwrap().len(),
2,
"a lone pillar lays down the reference node and its own"
);
}
#[test]
fn the_bootstrap_is_lazy_and_reruns_on_a_quote_change() {
let settings = settings_at(today());
let discount = discount_curve(today());
let quote = shared(SimpleQuote::new(0.005));
let helper = SpreadCdsHelper::new(
Handle::new(Shared::clone("e) as Shared<dyn Quote>),
Period::new(5, TimeUnit::Years),
SETTLEMENT_DAYS,
Target::new(),
Frequency::Quarterly,
BusinessDayConvention::Following,
DateGeneration::TwentiethIMM,
day_counter(),
RECOVERY_RATE,
discount,
Shared::clone(&settings),
)
.unwrap();
let curve = PiecewiseDefaultCurve::<HazardRate, BackwardFlat>::new(
today(),
vec![Shared::clone(&helper) as Shared<dyn DefaultProbabilityHelper>],
day_counter(),
BackwardFlat,
)
.unwrap();
assert!(!curve.lazy.borrow().is_calculated());
let first = curve
.survival_probability_date(helper.latest_date(), false)
.unwrap();
assert!(curve.lazy.borrow().is_calculated());
assert!(first < 1.0 && first > 0.0);
quote.set_value(0.02);
assert!(!curve.lazy.borrow().is_calculated());
let second = curve
.survival_probability_date(helper.latest_date(), false)
.unwrap();
assert!(
second < first,
"a wider spread must lower the survival probability: {second} vs {first}"
);
}
const UPFRONT_QUOTES: [Real; 4] = [0.01, 0.02, 0.04, 0.06];
const UPFRONT_TENORS: [i32; 4] = [2, 3, 5, 7];
const RUNNING_SPREAD: Rate = 0.05;
const UPFRONT_SETTLEMENT_DAYS: Natural = 3;
fn upfront_helper(
quote: Real,
tenor: Period,
discount: &Handle<dyn YieldTermStructure>,
settings: &Shared<Settings<Date>>,
) -> Shared<dyn DefaultProbabilityHelper> {
UpfrontCdsHelper::with_terms(
Handle::new(shared(SimpleQuote::new(quote)) as Shared<dyn Quote>),
RUNNING_SPREAD,
tenor,
SETTLEMENT_DAYS,
Target::new(),
Frequency::Quarterly,
BusinessDayConvention::ModifiedFollowing,
DateGeneration::CDS,
Actual360::new(),
RECOVERY_RATE,
discount.clone(),
UPFRONT_SETTLEMENT_DAYS,
CdsHelperTerms {
last_period_day_counter: Some(Actual360::with_last_day(true)),
..CdsHelperTerms::default()
},
Shared::clone(settings),
)
.expect("the CDS rule rolls every tenor quoted here")
as Shared<dyn DefaultProbabilityHelper>
}
fn upfront_schedule(today: Date, tenor: Period) -> Schedule {
Schedule::new(
today + SETTLEMENT_DAYS,
cds_maturity(today, tenor, DateGeneration::CDS)
.unwrap()
.expect("a live CDS maturity"),
Period::try_from(Frequency::Quarterly).expect("a quarterly period"),
Target::new(),
BusinessDayConvention::ModifiedFollowing,
BusinessDayConvention::Unadjusted,
DateGeneration::CDS,
false,
Date::null(),
Date::null(),
)
}
fn upfront_fixture(settings: &Shared<Settings<Date>>) -> Fixture {
let discount = discount_curve(today());
let helpers: Vec<Shared<dyn DefaultProbabilityHelper>> = UPFRONT_QUOTES
.iter()
.zip(UPFRONT_TENORS)
.map(|(quote, n)| {
upfront_helper(*quote, Period::new(n, TimeUnit::Years), &discount, settings)
})
.collect();
let curve = PiecewiseDefaultCurve::<HazardRate, BackwardFlat>::new(
today(),
helpers.clone(),
day_counter(),
BackwardFlat,
)
.unwrap();
Fixture {
settings: Shared::clone(settings),
discount,
helpers,
curve,
}
}
fn assert_the_upfronts_reproduce(fixture: &Fixture) {
let curve: Handle<dyn DefaultProbabilityTermStructure> = Handle::new(Shared::clone(
&fixture.curve,
)
as Shared<dyn DefaultProbabilityTermStructure>);
let restore = fixture.settings.include_todays_cash_flows();
fixture.settings.set_include_todays_cash_flows(Some(true));
for (quote, n) in UPFRONT_QUOTES.iter().zip(UPFRONT_TENORS) {
let tenor = Period::new(n, TimeUnit::Years);
let mut cds = CreditDefaultSwap::with_upfront_and_terms(
ProtectionSide::Buyer,
1.0,
*quote,
RUNNING_SPREAD,
upfront_schedule(today(), tenor),
BusinessDayConvention::ModifiedFollowing,
Actual360::new(),
CdsTerms {
protection_start: Some(today() + SETTLEMENT_DAYS),
upfront_date: Some(Target::new().advance(
today(),
UPFRONT_SETTLEMENT_DAYS as Integer,
TimeUnit::Days,
BusinessDayConvention::ModifiedFollowing,
false,
)),
last_period_day_counter: Some(Actual360::with_last_day(true)),
trade_date: Some(today()),
..CdsTerms::default()
},
Shared::clone(&fixture.settings),
)
.unwrap();
let engine = MidPointCdsEngine::new(
curve.clone(),
RECOVERY_RATE,
fixture.discount.clone(),
Some(true),
Shared::clone(&fixture.settings),
);
cds.base_mut()
.set_pricing_engine(shared_mut(engine) as SharedMut<dyn PricingEngine>);
let computed = cds.fair_upfront().unwrap();
assert!(
(computed - quote).abs() <= TOLERANCE,
"failed to reproduce the fair upfront for the {n}Y credit-default swap: \
computed {computed}, input {quote}"
);
}
fixture.settings.set_include_todays_cash_flows(restore);
}
#[test]
fn bootstrapped_curve_reproduces_the_input_cds_upfronts() {
assert_the_upfronts_reproduce(&upfront_fixture(&settings_at(today())));
}
#[test]
fn the_upfront_bootstrap_leaves_the_cash_flow_flag_as_it_found_it() {
let settings = settings_at(today());
settings.set_include_todays_cash_flows(Some(false));
let fixture = upfront_fixture(&settings);
fixture.curve.calculate().unwrap();
assert_eq!(
settings.include_todays_cash_flows(),
Some(false),
"the helper's own write must be unwound, not left on the caller's settings"
);
assert_the_upfronts_reproduce(&fixture);
assert_eq!(settings.include_todays_cash_flows(), Some(false));
}
}