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 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::instrument::Instrument;
use crate::instruments::{CdsTerms, CreditDefaultSwap, ProtectionSide};
use crate::interestrate::Compounding;
use crate::math::interpolations::flat::BackwardFlat;
use crate::pricingengine::PricingEngine;
use crate::pricingengines::credit::MidPointCdsEngine;
use crate::quotes::{Quote, SimpleQuote};
use crate::settings::Settings;
use crate::shared::shared;
use crate::termstructures::credit::defaultprobabilityhelpers::SpreadCdsHelper;
use crate::termstructures::yields::FlatForward;
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;
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 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}"
);
}
}