use std::cell::RefCell;
use std::marker::PhantomData;
use std::rc::Weak;
use crate::errors::QlResult;
use crate::handle::Handle;
use crate::math::interpolations::Interpolator;
use crate::patterns::lazyobject::LazyObject;
use crate::patterns::observable::{AsObservable, Observable, Observer};
use crate::quotes::Quote;
use crate::require;
use crate::settings::Settings;
use crate::shared::{Shared, SharedMut, shared_mut};
use crate::termstructures::bootstraptraits::CurveData;
use crate::termstructures::credit::defaultdensitystructure::DefaultDensityStructure;
use crate::termstructures::credit::defaultprobabilityhelpers::DefaultProbabilityHelper;
use crate::termstructures::credit::defaulttermstructure::{
DefaultProbabilityJumps, DefaultProbabilityTermStructure,
};
use crate::termstructures::credit::hazardratestructure::HazardRateStructure;
use crate::termstructures::credit::interpolatedhazardratecurve::hazard_rate_from_nodes;
use crate::termstructures::credit::probabilitytraits::{
CreditBootstrapTraits, DefaultDensity, HazardRate, SurvivalProbability,
};
use crate::termstructures::credit::survivalprobabilitystructure::SurvivalProbabilityStructure;
use crate::termstructures::iterativebootstrap::{IterativeBootstrap, PiecewiseCurve};
use crate::termstructures::{TermStructure, TermStructureBase};
use crate::time::calendar::Calendar;
use crate::time::date::Date;
use crate::time::daycounter::DayCounter;
use crate::types::{Natural, 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: CreditBootstrapTraits, I: Interpolator> {
base: TermStructureBase,
instruments: Vec<Shared<dyn DefaultProbabilityHelper>>,
interpolator: I,
data: RefCell<CurveData<I>>,
lazy: SharedMut<LazyObject>,
observable: Shared<Observable>,
updater: SharedMut<CurveUpdater>,
jumps: Option<DefaultProbabilityJumps>,
bootstrap: IterativeBootstrap,
accuracy: Real,
self_weak: Weak<dyn DefaultProbabilityTermStructure>,
_traits: PhantomData<fn() -> T>,
}
impl<T: CreditBootstrapTraits + 'static, I: Interpolator + 'static> PiecewiseDefaultCurve<T, I> {
pub fn new(
reference_date: Date,
instruments: Vec<Shared<dyn DefaultProbabilityHelper>>,
day_counter: DayCounter,
interpolator: I,
) -> QlResult<Shared<PiecewiseDefaultCurve<T, I>>> {
Self::with_jumps(
reference_date,
instruments,
day_counter,
interpolator,
Vec::new(),
Vec::new(),
)
}
pub fn with_jumps(
reference_date: Date,
instruments: Vec<Shared<dyn DefaultProbabilityHelper>>,
day_counter: DayCounter,
interpolator: I,
jumps: Vec<Handle<dyn Quote>>,
jump_dates: Vec<Date>,
) -> QlResult<Shared<PiecewiseDefaultCurve<T, I>>> {
Self::build(
TermStructureBase::with_reference_date(reference_date, None, Some(day_counter)),
instruments,
interpolator,
jumps,
jump_dates,
)
}
pub fn moving(
settlement_days: Natural,
calendar: Calendar,
instruments: Vec<Shared<dyn DefaultProbabilityHelper>>,
day_counter: DayCounter,
interpolator: I,
settings: Shared<Settings<Date>>,
) -> QlResult<Shared<PiecewiseDefaultCurve<T, I>>> {
Self::moving_with_jumps(
settlement_days,
calendar,
instruments,
day_counter,
interpolator,
settings,
Vec::new(),
Vec::new(),
)
}
#[allow(clippy::too_many_arguments)]
pub fn moving_with_jumps(
settlement_days: Natural,
calendar: Calendar,
instruments: Vec<Shared<dyn DefaultProbabilityHelper>>,
day_counter: DayCounter,
interpolator: I,
settings: Shared<Settings<Date>>,
jumps: Vec<Handle<dyn Quote>>,
jump_dates: Vec<Date>,
) -> QlResult<Shared<PiecewiseDefaultCurve<T, I>>> {
let curve = Self::build(
TermStructureBase::moving(
settlement_days,
calendar,
Some(day_counter),
Shared::clone(&settings),
),
instruments,
interpolator,
jumps,
jump_dates,
)?;
settings.register_eval_date_observer(
&(SharedMut::clone(&curve.updater) as SharedMut<dyn Observer>),
);
Ok(curve)
}
fn build(
base: TermStructureBase,
instruments: Vec<Shared<dyn DefaultProbabilityHelper>>,
interpolator: I,
jumps: Vec<Handle<dyn Quote>>,
jump_dates: Vec<Date>,
) -> QlResult<Shared<PiecewiseDefaultCurve<T, I>>> {
require!(!instruments.is_empty(), "no bootstrap helpers given");
let lazy = shared_mut(LazyObject::new(true));
let observable = lazy.borrow().observable_handle();
let updater = shared_mut(CurveUpdater {
lazy: SharedMut::clone(&lazy),
});
let jumps = if jumps.is_empty() && jump_dates.is_empty() {
None
} else {
let observer = SharedMut::clone(&updater) as SharedMut<dyn Observer>;
Some(DefaultProbabilityJumps::with_observer(
&base, &observer, jumps, jump_dates,
)?)
};
let curve = Shared::new_cyclic(|weak: &Weak<PiecewiseDefaultCurve<T, I>>| {
let self_weak: Weak<dyn DefaultProbabilityTermStructure> = weak.clone();
PiecewiseDefaultCurve {
base,
instruments,
interpolator,
data: RefCell::new(CurveData::new()),
lazy,
observable,
updater,
jumps,
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<T: CreditBootstrapTraits, I: Interpolator> AsObservable for PiecewiseDefaultCurve<T, I> {
fn observable(&self) -> &Observable {
&self.observable
}
}
impl<T: CreditBootstrapTraits + 'static, I: Interpolator + 'static> TermStructure
for PiecewiseDefaultCurve<T, I>
{
fn base(&self) -> &TermStructureBase {
&self.base
}
fn register_upstream(&self, observer: &SharedMut<dyn Observer>) {
for helper in &self.instruments {
helper.observable().register_observer(observer);
}
if let Some(jumps) = &self.jumps {
jumps.register_observer(observer);
}
if let Some(settings) = self.base.moving_settings() {
settings.register_eval_date_observer(observer);
}
}
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> DefaultDensityStructure
for PiecewiseDefaultCurve<DefaultDensity, I>
{
}
impl<T: CreditBootstrapTraits + 'static, I: Interpolator + 'static> DefaultProbabilityTermStructure
for PiecewiseDefaultCurve<T, I>
{
fn as_any(&self) -> Option<&dyn std::any::Any> {
Some(self)
}
fn jumps(&self) -> Option<&DefaultProbabilityJumps> {
self.jumps.as_ref()
}
fn survival_probability_impl(&self, t: Time) -> QlResult<Probability> {
self.calculate()?;
let data = self.data.borrow();
T::survival(data.interpolation()?, t)
}
fn default_density_impl(&self, t: Time) -> QlResult<Real> {
self.calculate()?;
T::density(self.data.borrow().interpolation()?, t)
}
fn hazard_rate_impl(&self, t: Time) -> QlResult<Rate> {
self.calculate()?;
T::hazard(self.data.borrow().interpolation()?, t)
}
}
impl<T: CreditBootstrapTraits + 'static, I: Interpolator + 'static> PiecewiseCurve
for PiecewiseDefaultCurve<T, I>
{
type Traits = T;
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"),
}
}
}
impl<I: Interpolator + 'static> SurvivalProbabilityStructure
for PiecewiseDefaultCurve<SurvivalProbability, I>
{
}
#[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, DefaultProbabilityHelperBase, 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<T: CreditBootstrapTraits = HazardRate, I: Interpolator = BackwardFlat> {
settings: Shared<Settings<Date>>,
discount: Handle<dyn YieldTermStructure>,
helpers: Vec<Shared<dyn DefaultProbabilityHelper>>,
curve: Shared<PiecewiseDefaultCurve<T, I>>,
}
fn fixture() -> Fixture {
fixture_with::<HazardRate, _>(BackwardFlat)
}
fn fixture_with<T: CreditBootstrapTraits + 'static, I: Interpolator + 'static>(
interpolator: I,
) -> Fixture<T, I> {
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::<T, I>::new(
today(),
helpers.clone(),
day_counter(),
interpolator,
)
.unwrap();
Fixture {
settings,
discount,
helpers,
curve,
}
}
#[test]
fn bootstrapped_curve_reproduces_the_input_cds_spreads() {
assert_the_spreads_reproduce(&fixture());
}
fn assert_the_spreads_reproduce<
T: CreditBootstrapTraits + 'static,
I: Interpolator + 'static,
>(
fixture: &Fixture<T, I>,
) {
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}"
);
}
fn implied_quote_on_dirty_curve(
quote_before: Real,
quote_after: Real,
make: impl Fn(Handle<dyn Quote>) -> Shared<dyn DefaultProbabilityHelper>,
) -> (Real, Real) {
let bootstrap = |quote: &Shared<SimpleQuote>| {
let helper = make(Handle::new(Shared::clone(quote) as Shared<dyn Quote>));
let curve = PiecewiseDefaultCurve::<HazardRate, BackwardFlat>::new(
today(),
vec![Shared::clone(&helper)],
day_counter(),
BackwardFlat,
)
.unwrap();
curve.calculate().unwrap();
(helper, curve)
};
let quote = shared(SimpleQuote::new(quote_before));
let (helper, _curve) = bootstrap("e);
quote.set_value(quote_after);
let dirty = helper.implied_quote().unwrap();
let (fresh_helper, _fresh_curve) = bootstrap(&shared(SimpleQuote::new(quote_after)));
(dirty, fresh_helper.implied_quote().unwrap())
}
#[test]
fn spread_implied_quote_on_a_dirty_curve_reprices_the_new_quote() {
let settings = settings_at(today());
let discount = discount_curve(today());
let (dirty, fresh) = implied_quote_on_dirty_curve(0.005, 0.02, |quote| {
SpreadCdsHelper::new(
quote,
Period::new(5, TimeUnit::Years),
SETTLEMENT_DAYS,
Target::new(),
Frequency::Quarterly,
BusinessDayConvention::Following,
DateGeneration::TwentiethIMM,
day_counter(),
RECOVERY_RATE,
discount.clone(),
Shared::clone(&settings),
)
.unwrap() as Shared<dyn DefaultProbabilityHelper>
});
assert!((dirty - fresh).abs() < 1e-12, "{dirty} vs {fresh}");
assert!((dirty - 0.02).abs() <= TOLERANCE, "{dirty}");
}
#[test]
fn upfront_implied_quote_on_a_dirty_curve_reprices_the_new_quote() {
let settings = settings_at(today());
let discount = discount_curve(today());
let (dirty, fresh) = implied_quote_on_dirty_curve(0.02, 0.04, |quote| {
UpfrontCdsHelper::with_terms(
quote,
RUNNING_SPREAD,
Period::new(5, TimeUnit::Years),
SETTLEMENT_DAYS,
Target::new(),
Frequency::Quarterly,
BusinessDayConvention::ModifiedFollowing,
DateGeneration::CDS,
Actual360::new(),
RECOVERY_RATE,
discount.clone(),
UPFRONT_SETTLEMENT_DAYS,
CdsHelperTerms::default(),
Shared::clone(&settings),
)
.unwrap() as Shared<dyn DefaultProbabilityHelper>
});
assert!((dirty - fresh).abs() < 1e-12, "{dirty} vs {fresh}");
assert!((dirty - 0.04).abs() <= TOLERANCE, "{dirty}");
}
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 {
upfront_fixture_with::<HazardRate, _>(settings, BackwardFlat)
}
fn upfront_fixture_with<T: CreditBootstrapTraits + 'static, I: Interpolator + 'static>(
settings: &Shared<Settings<Date>>,
interpolator: I,
) -> Fixture<T, I> {
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::<T, I>::new(
today(),
helpers.clone(),
day_counter(),
interpolator,
)
.unwrap();
Fixture {
settings: Shared::clone(settings),
discount,
helpers,
curve,
}
}
fn assert_the_upfronts_reproduce<
T: CreditBootstrapTraits + 'static,
I: Interpolator + 'static,
>(
fixture: &Fixture<T, I>,
) {
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));
}
#[test]
fn loglinear_survival_reproduces_spread_and_upfront_quotes() {
let fixture = fixture_with::<SurvivalProbability, _>(LogLinear);
assert_the_spreads_reproduce(&fixture);
let upfront =
upfront_fixture_with::<SurvivalProbability, _>(&settings_at(today()), LogLinear);
assert_the_upfronts_reproduce(&upfront);
for curve in [&fixture.curve, &upfront.curve] {
let data = curve.data().unwrap();
assert_eq!(data[0], 1.0);
assert!(
data.windows(2)
.all(|pair| pair[1] > 0.0 && pair[1] <= pair[0])
);
}
}
#[test]
fn loglinear_survival_single_helper_recalibrates_after_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::<SurvivalProbability, LogLinear>::new(
today(),
vec![Shared::clone(&helper) as Shared<dyn DefaultProbabilityHelper>],
day_counter(),
LogLinear,
)
.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_eq!(curve.dates().unwrap().len(), 2);
assert!((helper.implied_quote().unwrap() - 0.005).abs() <= TOLERANCE);
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);
assert!((helper.implied_quote().unwrap() - 0.02).abs() <= TOLERANCE);
assert_eq!(curve.data().unwrap()[0], 1.0);
quote.set_value(-0.01);
assert!(curve.calculate().is_err());
assert!(!curve.lazy.borrow().is_calculated());
quote.set_value(0.01);
curve.calculate().unwrap();
assert!((helper.implied_quote().unwrap() - 0.01).abs() <= TOLERANCE);
}
#[test]
fn bootstrapped_loglinear_survival_feeds_the_isda_grid() {
let fixture = fixture_with::<SurvivalProbability, _>(LogLinear);
let credit = Handle::new(
Shared::clone(&fixture.curve) as Shared<dyn DefaultProbabilityTermStructure>
);
assert_eq!(
isda_node_grid(&fixture.discount, &credit, today() + 10_000).unwrap(),
fixture.curve.dates().unwrap()
);
}
const MOVING_SETTLEMENT_DAYS: Natural = 1;
fn business_days_after(date: Date, days: Integer) -> Date {
Target::new().advance(
date,
days,
TimeUnit::Days,
BusinessDayConvention::Following,
false,
)
}
fn spread_helpers_on(
settings: &Shared<Settings<Date>>,
discount: &Handle<dyn YieldTermStructure>,
) -> (
Vec<Shared<SimpleQuote>>,
Vec<Shared<dyn DefaultProbabilityHelper>>,
) {
let quotes: Vec<_> = QUOTES
.iter()
.map(|q| shared(SimpleQuote::new(*q)))
.collect();
let helpers = quotes
.iter()
.zip(TENORS)
.map(|(quote, n)| {
SpreadCdsHelper::new(
Handle::new(Shared::clone(quote) as Shared<dyn Quote>),
Period::new(n, TimeUnit::Years),
SETTLEMENT_DAYS,
Target::new(),
Frequency::Quarterly,
BusinessDayConvention::Following,
DateGeneration::TwentiethIMM,
day_counter(),
RECOVERY_RATE,
discount.clone(),
Shared::clone(settings),
)
.unwrap() as Shared<dyn DefaultProbabilityHelper>
})
.collect();
(quotes, helpers)
}
fn moving_settings() -> Shared<Settings<Date>> {
let settings = settings_at(today());
settings.set_include_todays_cash_flows(Some(true));
settings
}
fn moving_curve(
helpers: Vec<Shared<dyn DefaultProbabilityHelper>>,
settings: &Shared<Settings<Date>>,
) -> Shared<PiecewiseDefaultCurve<HazardRate, BackwardFlat>> {
PiecewiseDefaultCurve::moving(
MOVING_SETTLEMENT_DAYS,
Target::new(),
helpers,
day_counter(),
BackwardFlat,
settings.clone(),
)
.unwrap()
}
struct FixedSurvivalHelper {
base: DefaultProbabilityHelperBase,
date: Date,
}
impl AsObservable for FixedSurvivalHelper {
fn observable(&self) -> &Observable {
self.base.observable()
}
}
impl DefaultProbabilityHelper for FixedSurvivalHelper {
fn base(&self) -> &DefaultProbabilityHelperBase {
&self.base
}
fn implied_quote(&self) -> QlResult<Real> {
self.base
.term_structure()?
.survival_probability_date(self.date, true)
}
}
fn fixed_survival_helpers() -> Vec<Shared<dyn DefaultProbabilityHelper>> {
[(1, 0.99), (2, 0.97)]
.into_iter()
.map(|(years, probability)| {
let base = DefaultProbabilityHelperBase::new(Handle::new(shared(SimpleQuote::new(
probability,
))
as Shared<dyn Quote>));
let date = today() + Period::new(years, TimeUnit::Years);
base.set_latest_date(date);
base.set_pillar_date(date);
shared(FixedSurvivalHelper { base, date }) as Shared<dyn DefaultProbabilityHelper>
})
.collect()
}
#[test]
fn rolled_moving_curve_matches_a_fresh_fixed_curve() {
let settings = moving_settings();
let discount = discount_curve(today());
let (_, helpers) = spread_helpers_on(&settings, &discount);
let curve = moving_curve(helpers, &settings);
curve.calculate().unwrap();
let new_today = business_days_after(today(), 15);
settings.set_evaluation_date(new_today);
let reference = business_days_after(new_today, MOVING_SETTLEMENT_DAYS as Integer);
assert_eq!(TermStructure::reference_date(&*curve).unwrap(), reference);
let (_, fresh_helpers) = spread_helpers_on(&settings, &discount);
let fresh = PiecewiseDefaultCurve::<HazardRate, BackwardFlat>::new(
reference,
fresh_helpers.clone(),
day_counter(),
BackwardFlat,
)
.unwrap();
let (rolled_nodes, fresh_nodes) = (curve.nodes().unwrap(), fresh.nodes().unwrap());
assert_eq!(rolled_nodes.len(), fresh_nodes.len());
for ((rolled_date, rolled), (fresh_date, value)) in rolled_nodes.iter().zip(&fresh_nodes) {
assert_eq!(rolled_date, fresh_date);
assert!((rolled - value).abs() <= 1.0e-10, "{rolled} vs {value}");
}
for helper in &fresh_helpers {
let date = helper.latest_date();
let rolled = curve.survival_probability_date(date, false).unwrap();
let value = fresh.survival_probability_date(date, false).unwrap();
assert!((rolled - value).abs() <= 1.0e-10, "{rolled} vs {value}");
}
}
#[test]
fn moving_curve_with_fixed_date_helpers_rebootstraps_on_roll() {
let settings = moving_settings();
let curve = moving_curve(fixed_survival_helpers(), &settings);
let dates = curve.dates().unwrap();
let data = curve.data().unwrap();
assert_eq!(dates[0], business_days_after(today(), 1));
let new_today = business_days_after(today(), 15);
settings.set_evaluation_date(new_today);
assert!(!curve.lazy.borrow().is_calculated());
let new_dates = curve.dates().unwrap();
assert_eq!(new_dates[0], business_days_after(new_today, 1));
assert_eq!(new_dates[1..], dates[1..]);
assert!(curve.data().unwrap()[1] > data[1]);
}
#[test]
fn register_upstream_includes_the_evaluation_date_only_when_moving() {
use crate::test_support::{Flag, as_observer};
let settings = moving_settings();
let moving = moving_curve(fixed_survival_helpers(), &settings);
let fixed = PiecewiseDefaultCurve::<HazardRate, BackwardFlat>::new(
today(),
fixed_survival_helpers(),
day_counter(),
BackwardFlat,
)
.unwrap();
let (moving_flag, fixed_flag) = (Flag::new(), Flag::new());
moving.register_upstream(&as_observer(&moving_flag));
fixed.register_upstream(&as_observer(&fixed_flag));
settings.set_evaluation_date(business_days_after(today(), 15));
assert!(
Flag::is_up(&moving_flag),
"moving curve upstream missed the roll"
);
assert!(
!Flag::is_up(&fixed_flag),
"fixed curve upstream heard the roll"
);
}
#[test]
fn moving_curve_observability() {
use crate::test_support::{Flag, as_observer};
let settings = moving_settings();
let discount = discount_curve(today());
let (quotes, helpers) = spread_helpers_on(&settings, &discount);
let curve = moving_curve(helpers.clone(), &settings);
curve.lazy.borrow_mut().forward_first_notification_only();
let flag = Flag::new();
curve.register_observer(&as_observer(&flag));
for (quote, helper) in quotes.iter().zip(&helpers) {
let date = helper.latest_date();
let survival = curve.survival_probability_date(date, false).unwrap();
Flag::lower(&flag);
quote.set_value(quote.value().unwrap() * 1.01);
assert!(Flag::is_up(&flag), "not notified of quote change");
assert_ne!(
curve.survival_probability_date(date, false).unwrap(),
survival,
"no recalculation"
);
quote.set_value(quote.value().unwrap() / 1.01);
}
curve.max_date();
Flag::lower(&flag);
settings.set_evaluation_date(business_days_after(today(), 15));
assert!(Flag::is_up(&flag), "not notified of date change");
Flag::lower(&flag);
settings.set_evaluation_date(today());
assert!(
!Flag::is_up(&flag),
"notified of date change without an intervening recalculation"
);
}
#[test]
fn moving_curve_without_an_evaluation_date_errs() {
let settings = shared(Settings::<Date>::new());
let curve = moving_curve(fixed_survival_helpers(), &settings);
assert!(TermStructure::reference_date(&*curve).is_err());
assert!(curve.calculate().is_err());
assert!(curve.survival_probability(1.0, true).is_err());
}
const JUMPS: [Real; 2] = [0.999, 0.998];
fn jump_dates() -> Vec<Date> {
vec![
Date::new(31, Month::December, 2006),
Date::new(31, Month::December, 2007),
]
}
fn jump_quotes(values: &[Real]) -> (Vec<Shared<SimpleQuote>>, Vec<Handle<dyn Quote>>) {
let quotes: Vec<_> = values
.iter()
.map(|v| shared(SimpleQuote::new(*v)))
.collect();
let handles = quotes
.iter()
.map(|q| Handle::new(Shared::clone(q) as Shared<dyn Quote>))
.collect();
(quotes, handles)
}
fn jumped_fixture(values: &[Real]) -> (Vec<Shared<SimpleQuote>>, Fixture) {
let settings = moving_settings();
let discount = discount_curve(today());
let (_, helpers) = spread_helpers_on(&settings, &discount);
let (quotes, handles) = jump_quotes(values);
let curve = PiecewiseDefaultCurve::with_jumps(
today(),
helpers.clone(),
day_counter(),
BackwardFlat,
handles,
jump_dates(),
)
.unwrap();
let fixture = Fixture {
settings,
discount,
helpers,
curve,
};
(quotes, fixture)
}
#[test]
fn a_jumped_curve_reproduces_the_input_cds_spreads() {
let (_, fixture) = jumped_fixture(&JUMPS);
assert_the_spreads_reproduce(&fixture);
}
#[test]
fn unit_jumps_reproduce_the_jump_free_curve() {
let (_, jumped) = jumped_fixture(&[1.0, 1.0]);
let plain = fixture();
for t in [0.1, 0.5, 1.0, 1.5, 2.0, 3.0, 4.5] {
let (a, b) = (
jumped.curve.survival_probability(t, false).unwrap(),
plain.curve.survival_probability(t, false).unwrap(),
);
assert!((a - b).abs() <= 1.0e-14, "t = {t}: {a} vs {b}");
}
}
#[test]
fn survival_applies_the_jumps_over_the_bootstrapped_nodes() {
let (_, fixture) = jumped_fixture(&JUMPS);
let curve = &fixture.curve;
let jump_times: Vec<Time> = jump_dates()
.into_iter()
.map(|date| TermStructure::time_from_reference(&**curve, date).unwrap())
.collect();
assert!(jump_times[1] < 2.0);
for t in [0.1, 0.5, 1.0, 1.5, 2.0, 3.0, 4.5] {
let expected: Real = jump_times
.iter()
.zip(JUMPS)
.filter(|(time, _)| **time < t)
.map(|(_, jump)| jump)
.product();
let ratio = curve.survival_probability(t, false).unwrap()
/ curve.survival_probability_impl(t).unwrap();
assert!(
(ratio - expected).abs() <= 1.0e-14 * expected,
"t = {t}: {ratio} vs {expected}"
);
}
}
#[test]
fn a_jump_quote_change_rebootstraps() {
use crate::test_support::{Flag, as_observer};
let (quotes, fixture) = jumped_fixture(&JUMPS);
let curve = &fixture.curve;
let flag = Flag::new();
curve.register_upstream(&as_observer(&flag));
let nodes = curve.data().unwrap();
quotes[1].set_value(0.996);
assert!(!curve.lazy.borrow().is_calculated());
assert!(Flag::is_up(&flag), "upstream subscriber missed the jump");
let rebuilt = curve.data().unwrap();
assert_eq!(rebuilt.len(), nodes.len());
assert!(
nodes
.iter()
.zip(&rebuilt)
.any(|(a, b)| (a - b).abs() > 1.0e-6),
"nodes did not move"
);
assert_the_spreads_reproduce(&fixture);
}
#[test]
fn a_rolled_moving_jumped_curve_matches_a_fresh_fixed_one() {
let settings = moving_settings();
let discount = discount_curve(today());
let (_, helpers) = spread_helpers_on(&settings, &discount);
let curve = PiecewiseDefaultCurve::<HazardRate, BackwardFlat>::moving_with_jumps(
MOVING_SETTLEMENT_DAYS,
Target::new(),
helpers,
day_counter(),
BackwardFlat,
settings.clone(),
jump_quotes(&JUMPS).1,
Vec::new(),
)
.unwrap();
assert_eq!(curve.jump_dates(), jump_dates());
let old_reference = TermStructure::reference_date(&*curve).unwrap();
let times = curve.jump_times().unwrap();
curve.calculate().unwrap();
settings.set_evaluation_date(business_days_after(today(), 15));
assert!(!curve.lazy.borrow().is_calculated());
let reference = TermStructure::reference_date(&*curve).unwrap();
assert_eq!(curve.jump_dates(), jump_dates());
let new_times = curve.jump_times().unwrap();
for ((old, new), date) in times.iter().zip(&new_times).zip(jump_dates()) {
assert!((old - day_counter().year_fraction(old_reference, date)).abs() <= 1.0e-14);
assert!((new - day_counter().year_fraction(reference, date)).abs() <= 1.0e-14);
}
assert!(new_times[0] < times[0]);
let (_, fresh_helpers) = spread_helpers_on(&settings, &discount);
let fresh = PiecewiseDefaultCurve::<HazardRate, BackwardFlat>::with_jumps(
reference,
fresh_helpers,
day_counter(),
BackwardFlat,
jump_quotes(&JUMPS).1,
jump_dates(),
)
.unwrap();
let probe = 0.5 * (times[0] + new_times[0]);
for t in [probe, 0.1, 1.0, 1.5, 2.0, 3.0, 4.5] {
let (a, b) = (
curve.survival_probability(t, false).unwrap(),
fresh.survival_probability(t, false).unwrap(),
);
assert!((a - b).abs() <= 1.0e-10, "t = {t}: {a} vs {b}");
}
}
}
#[cfg(test)]
#[path = "isdahelper_tests.rs"]
mod isdahelper_tests;