use crate::cashflow::CashFlow;
use crate::errors::QlResult;
use crate::event::Event;
use crate::instruments::{CdsArguments, CdsEngine, CdsResults, ProtectionSide};
use crate::patterns::observable::{AsObservable, Observable};
use crate::pricingengine::{Arguments, PricingEngine, Results};
use crate::settings::Settings;
use crate::shared::Shared;
use crate::termstructures::credit::defaulttermstructure::DefaultProbabilityTermStructure;
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::time::date::Date;
use crate::time::period::Period;
use crate::types::{Rate, Real};
use crate::{fail, handle::Handle, require};
const BASIS_POINT: Rate = 1.0e-4;
pub struct IntegralCdsEngine {
base: CdsEngine,
integration_step: Period,
probability: Handle<dyn DefaultProbabilityTermStructure>,
recovery_rate: Real,
discount_curve: Handle<dyn YieldTermStructure>,
include_settlement_date_flows: Option<bool>,
settings: Shared<Settings<Date>>,
}
impl IntegralCdsEngine {
pub fn new(
integration_step: Period,
probability: Handle<dyn DefaultProbabilityTermStructure>,
recovery_rate: Real,
discount_curve: Handle<dyn YieldTermStructure>,
include_settlement_date_flows: Option<bool>,
settings: Shared<Settings<Date>>,
) -> IntegralCdsEngine {
let base = CdsEngine::new(CdsArguments::default(), CdsResults::default());
probability.register_observer(&base.observer());
discount_curve.register_observer(&base.observer());
IntegralCdsEngine {
base,
integration_step,
probability,
recovery_rate,
discount_curve,
include_settlement_date_flows,
settings,
}
}
}
impl AsObservable for IntegralCdsEngine {
fn observable(&self) -> &Observable {
self.base.observable()
}
}
impl PricingEngine for IntegralCdsEngine {
fn arguments_mut(&mut self) -> &mut dyn Arguments {
self.base.arguments_mut()
}
fn results(&self) -> &dyn Results {
self.base.results()
}
fn reset(&mut self) {
self.base.reset();
}
fn calculate(&mut self) -> QlResult<()> {
let step = self.integration_step;
require!(step != Period::default(), "null period set");
require!(
!self.discount_curve.is_empty(),
"no discount term structure set"
);
require!(
!self.probability.is_empty(),
"no probability term structure set"
);
let discount = self.discount_curve.current_link()?;
let probability = self.probability.current_link()?;
let Some(today) = self.settings.evaluation_date() else {
fail!("no evaluation date set: the integral CDS engine needs today's date");
};
let settlement_date = discount.reference_date()?;
let arguments = self.base.arguments();
let (Some(side), Some(notional), Some(spread)) =
(arguments.side, arguments.notional, arguments.spread)
else {
fail!("side, notional or spread not set");
};
let Some(claim) = arguments.claim.as_ref() else {
fail!("claim not set");
};
let Some(protection_start) = arguments.protection_start else {
fail!("protection start date not set");
};
let Some(upfront_payment) = arguments.upfront_payment.as_ref() else {
fail!("upfront payment not set");
};
let mut upfront_pvo1 = 0.0;
let mut upfront_npv = 0.0;
if !upfront_payment.has_occurred(
&self.settings,
Some(settlement_date),
self.include_settlement_date_flows,
)? {
upfront_pvo1 = discount.discount_date(upfront_payment.date(), false)?;
upfront_npv = upfront_pvo1 * upfront_payment.amount()?;
}
let mut accrual_rebate_npv = 0.0;
if let Some(rebate) = arguments.accrual_rebate.as_ref()
&& !rebate.has_occurred(
&self.settings,
Some(settlement_date),
self.include_settlement_date_flows,
)?
{
accrual_rebate_npv = discount.discount_date(rebate.date(), false)? * rebate.amount()?;
}
let mut coupon_leg_npv = 0.0;
let mut default_leg_npv = 0.0;
for (i, flow) in arguments.leg.iter().enumerate() {
if flow.has_occurred(
&self.settings,
Some(settlement_date),
self.include_settlement_date_flows,
)? {
continue;
}
let Some(coupon) = flow.as_coupon() else {
fail!("premium leg flow #{} is not a coupon", i + 1);
};
let payment_date = flow.date();
let end_date = coupon.accrual_end_date();
let start_date = if i == 0 {
protection_start
} else {
coupon.accrual_start_date()
};
let effective_start_date = if start_date <= today && today <= end_date {
today
} else {
start_date
};
let coupon_amount = coupon.amount()?;
let end_discount = discount.discount_date(payment_date, false)?;
let survival = probability.survival_probability_date(payment_date, false)?;
coupon_leg_npv += survival * coupon_amount * end_discount;
let mut d0 = effective_start_date;
let mut d1 = std::cmp::min(d0 + step, end_date);
let mut p0 = probability.default_probability_date(d0, false)?;
loop {
let discount_factor = if arguments.pays_at_default_time {
discount.discount_date(d1, false)?
} else {
end_discount
};
let p1 = probability.default_probability_date(d1, false)?;
let default = p1 - p0;
if arguments.settles_accrual {
let accrual = if arguments.pays_at_default_time {
coupon.accrued_amount(d1)?
} else {
coupon_amount
};
coupon_leg_npv += accrual * discount_factor * default;
}
let claim_amount = claim.amount(&d1, notional, self.recovery_rate)?;
default_leg_npv += claim_amount * discount_factor * default;
p0 = p1;
d0 = d1;
d1 = std::cmp::min(d0 + step, end_date);
if d0 >= end_date {
break;
}
}
}
let mut upfront_sign = 1.0;
match side {
ProtectionSide::Seller => {
default_leg_npv *= -1.0;
accrual_rebate_npv *= -1.0;
}
ProtectionSide::Buyer => {
coupon_leg_npv *= -1.0;
upfront_npv *= -1.0;
upfront_sign = -1.0;
}
}
let fair_spread = if coupon_leg_npv != 0.0 {
Some(-default_leg_npv * spread / (coupon_leg_npv + accrual_rebate_npv))
} else {
None
};
let fair_upfront = if upfront_pvo1 > 0.0 {
Some(
-upfront_sign * (default_leg_npv + coupon_leg_npv + accrual_rebate_npv)
/ (upfront_pvo1 * notional),
)
} else {
None
};
let coupon_leg_bps = if spread != 0.0 {
Some(coupon_leg_npv * BASIS_POINT / spread)
} else {
None
};
let upfront_bps = match arguments.upfront {
Some(upfront) if upfront != 0.0 => Some(upfront_npv * BASIS_POINT / upfront),
_ => None,
};
let results = self.base.results_mut();
results.instrument.value =
Some(default_leg_npv + coupon_leg_npv + upfront_npv + accrual_rebate_npv);
results.instrument.error_estimate = None;
results.coupon_leg_npv = Some(coupon_leg_npv);
results.default_leg_npv = Some(default_leg_npv);
results.upfront_npv = Some(upfront_npv);
results.accrual_rebate_npv = Some(accrual_rebate_npv);
results.fair_spread = fair_spread;
results.fair_upfront = fair_upfront;
results.coupon_leg_bps = coupon_leg_bps;
results.upfront_bps = upfront_bps;
Ok(())
}
}
#[cfg(test)]
mod oracle {
use super::*;
use crate::instrument::Instrument;
use crate::instruments::CreditDefaultSwap;
use crate::interestrate::Compounding;
use crate::quotes::{Quote, SimpleQuote};
use crate::shared::{SharedMut, shared, shared_mut};
use crate::termstructures::credit::flathazardrate::FlatHazardRate;
use crate::termstructures::yields::FlatForward;
use crate::time::{
businessdayconvention::BusinessDayConvention, calendar::Calendar,
calendars::target::Target, date::Month, dategenerationrule::DateGeneration,
daycounters::actual360::Actual360, frequency::Frequency, schedule::Schedule,
timeunit::TimeUnit,
};
const NOTIONAL: Real = 10_000.0;
const CACHED_NPV: Real = 295.015_339_8;
const CACHED_FAIR_RATE: Rate = 0.007_517_539_081;
fn priced(step: Period) -> CreditDefaultSwap {
let today = Date::new(9, Month::June, 2006);
let settings = shared(Settings::new());
settings.set_evaluation_date(today);
let calendar: Calendar = Target::new();
let following = BusinessDayConvention::Following;
let convention = BusinessDayConvention::ModifiedFollowing;
let hazard_rate = Handle::new(shared(SimpleQuote::new(0.01234)) as Shared<dyn Quote>);
let probability = Handle::new(shared(FlatHazardRate::moving(
0,
calendar.clone(),
hazard_rate,
Actual360::new(),
Shared::clone(&settings),
)) as Shared<dyn DefaultProbabilityTermStructure>);
let discount = Handle::new(shared(FlatForward::with_rate(
today,
0.06,
Actual360::new(),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>);
let issue_date = calendar.advance(today, -1, TimeUnit::Years, following, false);
let maturity = calendar.advance(issue_date, 10, TimeUnit::Years, following, false);
let schedule = Schedule::new(
issue_date,
maturity,
Period::try_from(Frequency::Semiannual).unwrap(),
calendar,
convention,
convention,
DateGeneration::Forward,
false,
Date::null(),
Date::null(),
);
let mut cds = CreditDefaultSwap::new(
ProtectionSide::Seller,
NOTIONAL,
0.0120,
schedule,
convention,
Actual360::new(),
true,
true,
Shared::clone(&settings),
)
.unwrap();
let engine = IntegralCdsEngine::new(step, probability, 0.4, discount, None, settings);
cds.base_mut()
.set_pricing_engine(shared_mut(engine) as SharedMut<dyn PricingEngine>);
cds
}
#[test]
fn both_integration_steps_reproduce_the_cached_value() {
let mut daily = priced(Period::new(1, TimeUnit::Days));
let mut weekly = priced(Period::new(1, TimeUnit::Weeks));
let daily_npv = daily.npv().unwrap();
let weekly_npv = weekly.npv().unwrap();
for (grid, npv, fair_rate) in [
("1 day", daily_npv, daily.fair_spread().unwrap()),
("1 week", weekly_npv, weekly.fair_spread().unwrap()),
] {
assert!(
(npv - CACHED_NPV).abs() <= NOTIONAL * 1.0e-5 * 10.0,
"the integral engine on a {grid} grid priced the cached contract at {npv}, not {CACHED_NPV}"
);
assert!(
(fair_rate - CACHED_FAIR_RATE).abs() <= 1.0e-5,
"the integral engine on a {grid} grid quoted a fair spread of {fair_rate}, not {CACHED_FAIR_RATE}"
);
}
assert!(
(daily_npv - CACHED_NPV).abs() > 1.0e-9,
"a daily grid reproduced {CACHED_NPV} exactly: the engine collapsed onto the mid-point"
);
assert!(
daily_npv != weekly_npv,
"the daily and weekly grids both priced {daily_npv}: the engine ignores its step"
);
}
#[test]
fn a_null_integration_step_is_rejected() {
assert_eq!(
priced(Period::default()).npv().unwrap_err().message(),
"null period set"
);
}
}