use std::any::Any;
use crate::cashflow::Leg;
use crate::cashflows::{FixedRateLeg, SimpleCashFlow};
use crate::errors::QlResult;
use crate::handle::Handle;
use crate::instrument::{Instrument, InstrumentBase, InstrumentResults};
use crate::instruments::claim::{Claim, FaceValueClaim};
use crate::instruments::protection::ProtectionSide;
use crate::interestrate::Compounding;
use crate::math::solver1d::Solver1D;
use crate::math::solvers1d::brent::Brent;
use crate::pricingengine::{Arguments, GenericEngine, PricingEngine, Results};
use crate::pricingengines::credit::{
AccrualBias, ForwardsInCouponPeriod, IsdaCdsEngine, MidPointCdsEngine, NumericalFix,
};
use crate::quotes::{Quote, SimpleQuote};
use crate::settings::Settings;
use crate::shared::{Shared, shared};
use crate::termstructures::credit::defaulttermstructure::DefaultProbabilityTermStructure;
use crate::termstructures::credit::flathazardrate::FlatHazardRate;
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::calendars::weekendsonly::WeekendsOnly;
use crate::time::date::{Date, Month};
use crate::time::dategenerationrule::DateGeneration;
use crate::time::daycounter::DayCounter;
use crate::time::frequency::Frequency;
use crate::time::period::Period;
use crate::time::schedule::{Schedule, previous_twentieth};
use crate::time::timeunit::TimeUnit;
use crate::types::{Integer, Natural, Rate, Real};
use crate::{fail, require};
pub struct CdsTerms {
pub settles_accrual: bool,
pub pays_at_default_time: bool,
pub protection_start: Option<Date>,
pub upfront_date: Option<Date>,
pub claim: Option<Shared<dyn Claim>>,
pub last_period_day_counter: Option<DayCounter>,
pub rebates_accrual: bool,
pub trade_date: Option<Date>,
pub cash_settlement_days: Natural,
}
impl Default for CdsTerms {
fn default() -> CdsTerms {
CdsTerms {
settles_accrual: true,
pays_at_default_time: true,
protection_start: None,
upfront_date: None,
claim: None,
last_period_day_counter: None,
rebates_accrual: true,
trade_date: None,
cash_settlement_days: 3,
}
}
}
#[derive(Default)]
pub struct CdsArguments {
pub side: Option<ProtectionSide>,
pub notional: Option<Real>,
pub upfront: Option<Rate>,
pub spread: Option<Rate>,
pub leg: Leg,
pub upfront_payment: Option<Shared<SimpleCashFlow>>,
pub accrual_rebate: Option<Shared<SimpleCashFlow>>,
pub settles_accrual: bool,
pub pays_at_default_time: bool,
pub claim: Option<Shared<dyn Claim>>,
pub protection_start: Option<Date>,
pub maturity: Option<Date>,
}
impl Arguments for CdsArguments {
fn validate(&self) -> QlResult<()> {
require!(self.side.is_some(), "side not set");
let Some(notional) = self.notional else {
fail!("notional not set");
};
require!(notional != 0.0, "null notional set");
require!(self.spread.is_some(), "spread not set");
require!(!self.leg.is_empty(), "coupons not set");
require!(self.upfront_payment.is_some(), "upfront payment not set");
require!(self.claim.is_some(), "claim not set");
require!(
self.protection_start.is_some(),
"protection start date not set"
);
require!(self.maturity.is_some(), "maturity date not set");
Ok(())
}
}
#[derive(Default)]
pub struct CdsResults {
pub instrument: InstrumentResults,
pub fair_spread: Option<Rate>,
pub fair_upfront: Option<Rate>,
pub coupon_leg_bps: Option<Real>,
pub coupon_leg_npv: Option<Real>,
pub default_leg_npv: Option<Real>,
pub upfront_bps: Option<Real>,
pub upfront_npv: Option<Real>,
pub accrual_rebate_npv: Option<Real>,
}
impl Results for CdsResults {
fn reset(&mut self) {
self.instrument.reset();
self.fair_spread = None;
self.fair_upfront = None;
self.coupon_leg_bps = None;
self.coupon_leg_npv = None;
self.default_leg_npv = None;
self.upfront_bps = None;
self.upfront_npv = None;
self.accrual_rebate_npv = None;
}
fn as_instrument_results(&self) -> Option<&InstrumentResults> {
Some(&self.instrument)
}
}
pub type CdsEngine = GenericEngine<CdsArguments, CdsResults>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PricingModel {
Midpoint,
Isda,
}
pub struct CreditDefaultSwap {
base: InstrumentBase,
settings: Shared<Settings<Date>>,
side: ProtectionSide,
notional: Real,
upfront: Option<Rate>,
running_spread: Rate,
settles_accrual: bool,
pays_at_default_time: bool,
claim: Shared<dyn Claim>,
protection_start: Date,
trade_date: Date,
cash_settlement_days: Natural,
leg: Leg,
upfront_payment: Shared<SimpleCashFlow>,
accrual_rebate: Option<Shared<SimpleCashFlow>>,
maturity: Date,
fair_spread: Option<Rate>,
fair_upfront: Option<Rate>,
coupon_leg_bps: Option<Real>,
coupon_leg_npv: Option<Real>,
default_leg_npv: Option<Real>,
upfront_bps: Option<Real>,
upfront_npv: Option<Real>,
accrual_rebate_npv: Option<Real>,
}
impl CreditDefaultSwap {
#[allow(clippy::too_many_arguments)]
pub fn new(
side: ProtectionSide,
notional: Real,
spread: Rate,
schedule: Schedule,
payment_convention: BusinessDayConvention,
day_counter: DayCounter,
settles_accrual: bool,
pays_at_default_time: bool,
settings: Shared<Settings<Date>>,
) -> QlResult<CreditDefaultSwap> {
CreditDefaultSwap::with_terms(
side,
notional,
spread,
schedule,
payment_convention,
day_counter,
CdsTerms {
settles_accrual,
pays_at_default_time,
..CdsTerms::default()
},
settings,
)
}
#[allow(clippy::too_many_arguments)]
pub fn with_terms(
side: ProtectionSide,
notional: Real,
spread: Rate,
schedule: Schedule,
payment_convention: BusinessDayConvention,
day_counter: DayCounter,
terms: CdsTerms,
settings: Shared<Settings<Date>>,
) -> QlResult<CreditDefaultSwap> {
CreditDefaultSwap::build(
side,
notional,
None,
spread,
schedule,
payment_convention,
day_counter,
terms,
settings,
)
}
#[allow(clippy::too_many_arguments)]
pub fn with_upfront(
side: ProtectionSide,
notional: Real,
upfront: Rate,
spread: Rate,
schedule: Schedule,
payment_convention: BusinessDayConvention,
day_counter: DayCounter,
settles_accrual: bool,
pays_at_default_time: bool,
settings: Shared<Settings<Date>>,
) -> QlResult<CreditDefaultSwap> {
CreditDefaultSwap::with_upfront_and_terms(
side,
notional,
upfront,
spread,
schedule,
payment_convention,
day_counter,
CdsTerms {
settles_accrual,
pays_at_default_time,
..CdsTerms::default()
},
settings,
)
}
#[allow(clippy::too_many_arguments)]
pub fn with_upfront_and_terms(
side: ProtectionSide,
notional: Real,
upfront: Rate,
spread: Rate,
schedule: Schedule,
payment_convention: BusinessDayConvention,
day_counter: DayCounter,
terms: CdsTerms,
settings: Shared<Settings<Date>>,
) -> QlResult<CreditDefaultSwap> {
CreditDefaultSwap::build(
side,
notional,
Some(upfront),
spread,
schedule,
payment_convention,
day_counter,
terms,
settings,
)
}
#[allow(clippy::too_many_arguments)]
fn build(
side: ProtectionSide,
notional: Real,
upfront: Option<Rate>,
spread: Rate,
schedule: Schedule,
payment_convention: BusinessDayConvention,
day_counter: DayCounter,
terms: CdsTerms,
settings: Shared<Settings<Date>>,
) -> QlResult<CreditDefaultSwap> {
require!(
!schedule.is_empty(),
"CreditDefaultSwap needs a non-empty schedule."
);
let protection_start = terms.protection_start.unwrap_or_else(|| schedule.date(0));
let post_big_bang = schedule.has_rule()
&& matches!(
schedule.rule(),
DateGeneration::CDS | DateGeneration::CDS2015
);
if !post_big_bang {
require!(
protection_start <= schedule.date(0),
"protection can not start after accrual"
);
}
let mut builder = FixedRateLeg::new(schedule.clone())
.with_notional(notional)
.with_coupon_rate(spread, day_counter, Compounding::Simple, Frequency::Annual)?
.with_payment_adjustment(payment_convention);
if let Some(day_counter) = terms.last_period_day_counter {
builder = builder.with_last_period_day_counter(day_counter);
}
let leg = builder.build()?;
let trade_date = terms.trade_date.unwrap_or_else(|| {
if post_big_bang {
protection_start
} else {
protection_start - 1
}
});
let effective_upfront_date = terms.upfront_date.unwrap_or_else(|| {
schedule.calendar().advance(
trade_date,
terms.cash_settlement_days as Integer,
TimeUnit::Days,
payment_convention,
false,
)
});
require!(
effective_upfront_date >= protection_start,
"The cash settlement date must not be before the protection start date."
);
let upfront_amount = upfront.map_or(0.0, |upfront| upfront * notional);
let upfront_payment = shared(SimpleCashFlow::new(upfront_amount, effective_upfront_date)?);
let accrual_rebate = if terms.rebates_accrual {
let mut rebate_amount = 0.0;
let reference_date = trade_date + 1;
if trade_date >= schedule.date(0) {
let last = leg.len() - 1;
for (i, flow) in leg.iter().enumerate() {
let payment_date = flow.date();
if reference_date > payment_date {
continue;
}
let Some(coupon) = flow.as_coupon() else {
fail!("premium leg flow #{} is not a coupon", i + 1);
};
if reference_date == payment_date {
if i == last {
rebate_amount = coupon.amount()?;
}
} else {
rebate_amount = coupon.accrued_amount(reference_date)?;
}
break;
}
}
Some(shared(SimpleCashFlow::new(
rebate_amount,
effective_upfront_date,
)?))
} else {
None
};
let base = InstrumentBase::new();
settings.register_eval_date_observer(&base.observer());
Ok(CreditDefaultSwap {
base,
settings,
side,
notional,
upfront,
running_spread: spread,
settles_accrual: terms.settles_accrual,
pays_at_default_time: terms.pays_at_default_time,
claim: terms.claim.unwrap_or_else(|| shared(FaceValueClaim)),
protection_start,
trade_date,
cash_settlement_days: terms.cash_settlement_days,
leg,
upfront_payment,
accrual_rebate,
maturity: schedule.date(schedule.len() - 1),
fair_spread: None,
fair_upfront: None,
coupon_leg_bps: None,
coupon_leg_npv: None,
default_leg_npv: None,
upfront_bps: None,
upfront_npv: None,
accrual_rebate_npv: None,
})
}
pub fn side(&self) -> ProtectionSide {
self.side
}
pub fn notional(&self) -> Real {
self.notional
}
pub fn running_spread(&self) -> Rate {
self.running_spread
}
pub fn upfront(&self) -> Option<Rate> {
self.upfront
}
pub fn settles_accrual(&self) -> bool {
self.settles_accrual
}
pub fn pays_at_default_time(&self) -> bool {
self.pays_at_default_time
}
pub fn claim(&self) -> &Shared<dyn Claim> {
&self.claim
}
pub fn coupons(&self) -> &Leg {
&self.leg
}
pub fn protection_start_date(&self) -> Date {
self.protection_start
}
pub fn maturity(&self) -> Date {
self.maturity
}
pub fn upfront_payment(&self) -> &Shared<SimpleCashFlow> {
&self.upfront_payment
}
pub fn accrual_rebate(&self) -> Option<&Shared<SimpleCashFlow>> {
self.accrual_rebate.as_ref()
}
pub fn rebates_accrual(&self) -> bool {
self.accrual_rebate.is_some()
}
pub fn trade_date(&self) -> Date {
self.trade_date
}
pub fn cash_settlement_days(&self) -> Natural {
self.cash_settlement_days
}
pub fn fair_spread(&mut self) -> QlResult<Rate> {
self.calculate()?;
let Some(value) = self.fair_spread else {
fail!("fair spread not available");
};
Ok(value)
}
pub fn fair_upfront(&mut self) -> QlResult<Rate> {
self.calculate()?;
let Some(value) = self.fair_upfront else {
fail!("fair upfront not available");
};
Ok(value)
}
pub fn coupon_leg_bps(&mut self) -> QlResult<Real> {
self.calculate()?;
let Some(value) = self.coupon_leg_bps else {
fail!("coupon-leg BPS not available");
};
Ok(value)
}
pub fn coupon_leg_npv(&mut self) -> QlResult<Real> {
self.calculate()?;
let Some(value) = self.coupon_leg_npv else {
fail!("coupon-leg NPV not available");
};
Ok(value)
}
pub fn default_leg_npv(&mut self) -> QlResult<Real> {
self.calculate()?;
let Some(value) = self.default_leg_npv else {
fail!("default-leg NPV not available");
};
Ok(value)
}
pub fn upfront_npv(&mut self) -> QlResult<Real> {
self.calculate()?;
let Some(value) = self.upfront_npv else {
fail!("upfront NPV not available");
};
Ok(value)
}
pub fn upfront_bps(&mut self) -> QlResult<Real> {
self.calculate()?;
let Some(value) = self.upfront_bps else {
fail!("upfront BPS not available");
};
Ok(value)
}
pub fn accrual_rebate_npv(&mut self) -> QlResult<Real> {
self.calculate()?;
let Some(value) = self.accrual_rebate_npv else {
fail!("accrual Rebate NPV not available");
};
Ok(value)
}
pub fn implied_hazard_rate(
&self,
target_npv: Real,
discount_curve: &Handle<dyn YieldTermStructure>,
day_counter: DayCounter,
recovery_rate: Real,
accuracy: Real,
model: PricingModel,
) -> QlResult<Rate> {
let flat_rate = shared(SimpleQuote::new(0.0));
let probability = Handle::new(shared(FlatHazardRate::moving(
0,
WeekendsOnly::new(),
Handle::new(Shared::clone(&flat_rate) as Shared<dyn Quote>),
day_counter,
Shared::clone(&self.settings),
)) as Shared<dyn DefaultProbabilityTermStructure>);
let mut engine: Box<dyn PricingEngine> = match model {
PricingModel::Midpoint => Box::new(MidPointCdsEngine::new(
probability,
recovery_rate,
discount_curve.clone(),
None,
Shared::clone(&self.settings),
)),
PricingModel::Isda => Box::new(
IsdaCdsEngine::new(
probability,
recovery_rate,
discount_curve.clone(),
None,
Shared::clone(&self.settings),
)
.with_fidelity(
NumericalFix::Taylor,
AccrualBias::HalfDayBias,
ForwardsInCouponPeriod::Piecewise,
),
),
};
self.setup_arguments(engine.arguments_mut())?;
engine.arguments_mut().validate()?;
let objective = |hazard_rate: Rate| -> Real {
flat_rate.set_value(hazard_rate);
if engine.calculate().is_err() {
return Real::NAN;
}
match (engine.results() as &dyn Any).downcast_ref::<CdsResults>() {
Some(results) => results.instrument.value.unwrap_or(Real::NAN) - target_npv,
None => Real::NAN,
}
};
let guess = self.running_spread / (1.0 - recovery_rate) * 365.0 / 360.0;
Brent::new().solve(objective, accuracy, guess, 0.1 * guess)
}
pub fn conventional_spread(
&self,
conventional_recovery: Real,
discount_curve: &Handle<dyn YieldTermStructure>,
day_counter: DayCounter,
model: PricingModel,
) -> QlResult<Rate> {
let flat_rate = shared(SimpleQuote::new(0.0));
let probability = Handle::new(shared(FlatHazardRate::moving(
0,
WeekendsOnly::new(),
Handle::new(Shared::clone(&flat_rate) as Shared<dyn Quote>),
day_counter,
Shared::clone(&self.settings),
)) as Shared<dyn DefaultProbabilityTermStructure>);
let mut engine: Box<dyn PricingEngine> = match model {
PricingModel::Midpoint => Box::new(MidPointCdsEngine::new(
probability,
conventional_recovery,
discount_curve.clone(),
None,
Shared::clone(&self.settings),
)),
PricingModel::Isda => Box::new(
IsdaCdsEngine::new(
probability,
conventional_recovery,
discount_curve.clone(),
None,
Shared::clone(&self.settings),
)
.with_fidelity(
NumericalFix::Taylor,
AccrualBias::HalfDayBias,
ForwardsInCouponPeriod::Piecewise,
),
),
};
self.setup_arguments(engine.arguments_mut())?;
engine.arguments_mut().validate()?;
let objective = |hazard_rate: Rate| -> Real {
flat_rate.set_value(hazard_rate);
if engine.calculate().is_err() {
return Real::NAN;
}
match (engine.results() as &dyn Any).downcast_ref::<CdsResults>() {
Some(results) => results.instrument.value.unwrap_or(Real::NAN),
None => Real::NAN,
}
};
let guess = self.running_spread / (1.0 - conventional_recovery) * 365.0 / 360.0;
Brent::new().solve(objective, 1.0e-9, guess, 0.1 * guess)?;
match (engine.results() as &dyn Any).downcast_ref::<CdsResults>() {
Some(results) => match results.fair_spread {
Some(fair_spread) => Ok(fair_spread),
None => fail!("the engine reported no fair spread at the conventional hazard rate"),
},
None => fail!("the engine did not report credit-default-swap results"),
}
}
}
impl Instrument for CreditDefaultSwap {
fn base(&self) -> &InstrumentBase {
&self.base
}
fn base_mut(&mut self) -> &mut InstrumentBase {
&mut self.base
}
fn is_expired(&self) -> QlResult<bool> {
for flow in self.leg.iter().rev() {
if !flow.has_occurred(&self.settings, None, None)? {
return Ok(false);
}
}
Ok(true)
}
fn setup_arguments(&self, arguments: &mut dyn Arguments) -> QlResult<()> {
let Some(arguments) = (arguments as &mut dyn Any).downcast_mut::<CdsArguments>() else {
fail!("wrong argument type");
};
arguments.side = Some(self.side);
arguments.notional = Some(self.notional);
arguments.leg = self.leg.clone();
arguments.upfront_payment = Some(Shared::clone(&self.upfront_payment));
arguments.accrual_rebate = self.accrual_rebate.as_ref().map(Shared::clone);
arguments.settles_accrual = self.settles_accrual;
arguments.pays_at_default_time = self.pays_at_default_time;
arguments.claim = Some(Shared::clone(&self.claim));
arguments.upfront = self.upfront;
arguments.spread = Some(self.running_spread);
arguments.protection_start = Some(self.protection_start);
arguments.maturity = Some(self.maturity);
Ok(())
}
fn setup_expired(&mut self) {
let expired = InstrumentResults {
value: Some(0.0),
error_estimate: Some(0.0),
..InstrumentResults::default()
};
self.base_mut().store_results(&expired);
self.fair_spread = Some(0.0);
self.fair_upfront = Some(0.0);
self.coupon_leg_bps = Some(0.0);
self.upfront_bps = Some(0.0);
self.coupon_leg_npv = Some(0.0);
self.default_leg_npv = Some(0.0);
self.upfront_npv = Some(0.0);
}
fn fetch_results(&mut self, results: &dyn Results) -> QlResult<()> {
let Some(results) = (results as &dyn Any).downcast_ref::<CdsResults>() else {
fail!("wrong result type");
};
self.base_mut().store_results(&results.instrument);
self.fair_spread = results.fair_spread;
self.fair_upfront = results.fair_upfront;
self.coupon_leg_bps = results.coupon_leg_bps;
self.coupon_leg_npv = results.coupon_leg_npv;
self.default_leg_npv = results.default_leg_npv;
self.upfront_npv = results.upfront_npv;
self.upfront_bps = results.upfront_bps;
self.accrual_rebate_npv = results.accrual_rebate_npv;
Ok(())
}
}
pub fn cds_maturity(
trade_date: Date,
tenor: Period,
rule: DateGeneration,
) -> QlResult<Option<Date>> {
require!(
matches!(
rule,
DateGeneration::CDS2015 | DateGeneration::CDS | DateGeneration::OldCDS
),
"cds_maturity should only be used with date generation rule CDS2015, CDS or OldCDS"
);
require!(
tenor.units() == TimeUnit::Years
|| (tenor.units() == TimeUnit::Months && tenor.length() % 3 == 0),
"cds_maturity expects a tenor that is a multiple of 3 months."
);
if rule == DateGeneration::OldCDS {
require!(
tenor != Period::new(0, TimeUnit::Months),
"A tenor of 0M is not supported for OldCDS."
);
}
let mut anchor_date = previous_twentieth(trade_date, rule);
if rule == DateGeneration::CDS2015
&& (anchor_date == Date::new(20, Month::December, anchor_date.year())
|| anchor_date == Date::new(20, Month::June, anchor_date.year()))
{
if tenor.length() == 0 {
return Ok(None);
}
anchor_date = anchor_date - Period::new(3, TimeUnit::Months);
}
let maturity = anchor_date + tenor + Period::new(3, TimeUnit::Months);
require!(
maturity > trade_date,
"error calculating CDS maturity. Tenor is {tenor}, trade date is {trade_date} \
generating a maturity of {maturity} <= trade date."
);
Ok(Some(maturity))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cashflow::CashFlow;
use crate::event::Event;
use crate::math::interpolations::flat::BackwardFlat;
use crate::patterns::observable::{AsObservable, Observable};
use crate::shared::{SharedMut, shared_mut};
use crate::termstructures::credit::interpolatedhazardratecurve::InterpolatedHazardRateCurve;
use crate::termstructures::yields::FlatForward;
use crate::time::calendars::target::Target;
use crate::time::daycounters::actual360::Actual360;
use crate::time::daycounters::actual365fixed::Actual365Fixed;
use crate::time::schedule::MakeSchedule;
const NOTIONAL: Real = 10_000_000.0;
const SPREAD: Rate = 0.01;
fn today() -> Date {
Date::new(19, Month::June, 2026)
}
fn settings_today() -> Shared<Settings<Date>> {
let settings = shared(Settings::new());
settings.set_evaluation_date(today());
settings
}
fn ten_year_schedule() -> Schedule {
MakeSchedule::new()
.from(Date::new(20, Month::June, 2026))
.to(Date::new(20, Month::June, 2036))
.with_frequency(Frequency::Semiannual)
.with_calendar(WeekendsOnly::new())
.with_convention(BusinessDayConvention::Following)
.with_termination_date_convention(BusinessDayConvention::Unadjusted)
.backwards()
.build()
}
fn contract(terms: CdsTerms) -> QlResult<CreditDefaultSwap> {
contract_priced_on(terms, settings_today())
}
fn contract_priced_on(
terms: CdsTerms,
settings: Shared<Settings<Date>>,
) -> QlResult<CreditDefaultSwap> {
CreditDefaultSwap::with_terms(
ProtectionSide::Buyer,
NOTIONAL,
SPREAD,
ten_year_schedule(),
BusinessDayConvention::Following,
Actual360::new(),
terms,
settings,
)
}
#[test]
fn the_premium_leg_spans_the_schedule_and_the_contract_matures_with_it() {
let schedule = ten_year_schedule();
let cds = CreditDefaultSwap::new(
ProtectionSide::Buyer,
NOTIONAL,
SPREAD,
schedule.clone(),
BusinessDayConvention::Following,
Actual360::new(),
true,
true,
settings_today(),
)
.unwrap();
assert_eq!(cds.coupons().len(), schedule.len() - 1);
assert_eq!(cds.coupons().len(), 20);
assert_eq!(
Event::date(cds.coupons()[0].as_ref()),
WeekendsOnly::new().adjust(schedule.date(1), BusinessDayConvention::Following)
);
assert_eq!(cds.maturity(), *schedule.dates().last().unwrap());
assert_eq!(cds.side(), ProtectionSide::Buyer);
assert_eq!(cds.notional(), NOTIONAL);
assert_eq!(cds.running_spread(), SPREAD);
assert!(cds.settles_accrual());
assert!(cds.pays_at_default_time());
assert_eq!(cds.cash_settlement_days(), 3);
}
#[test]
fn the_trade_date_defaults_to_the_day_before_the_protection_start() {
let cds = contract(CdsTerms::default()).unwrap();
assert_eq!(cds.trade_date(), cds.protection_start_date() - 1);
}
#[test]
fn the_protection_starts_on_the_first_accrual_date_unless_given() {
let schedule = ten_year_schedule();
assert_eq!(
contract(CdsTerms::default())
.unwrap()
.protection_start_date(),
schedule.date(0)
);
let earlier = schedule.date(0) - 10;
let cds = contract(CdsTerms {
protection_start: Some(earlier),
..CdsTerms::default()
})
.unwrap();
assert_eq!(cds.protection_start_date(), earlier);
assert_eq!(cds.trade_date(), earlier - 1);
}
#[test]
fn the_upfront_payment_is_a_zero_flow_on_the_cash_settlement_date() {
let cds = contract(CdsTerms::default()).unwrap();
let expected = WeekendsOnly::new().advance(
cds.trade_date(),
3,
TimeUnit::Days,
BusinessDayConvention::Following,
false,
);
assert_eq!(cds.upfront(), None);
assert_eq!(cds.upfront_payment().amount().unwrap(), 0.0);
assert_eq!(Event::date(cds.upfront_payment().as_ref()), expected);
assert!(expected >= cds.protection_start_date());
}
#[test]
fn the_upfront_constructor_quotes_the_upfront_and_pays_it_unsigned() {
for side in [ProtectionSide::Buyer, ProtectionSide::Seller] {
let cds = CreditDefaultSwap::with_upfront(
side,
NOTIONAL,
0.001,
SPREAD,
ten_year_schedule(),
BusinessDayConvention::Following,
Actual360::new(),
true,
true,
settings_today(),
)
.unwrap();
assert_eq!(cds.upfront(), Some(0.001));
assert_eq!(cds.upfront_payment().amount().unwrap(), NOTIONAL * 0.001);
assert_eq!(arguments(&cds).upfront, Some(0.001));
}
}
#[test]
fn an_upfront_date_given_outright_settles_both_cash_flows() {
let deduced = contract(CdsTerms::default()).unwrap();
let given = Event::date(deduced.upfront_payment().as_ref()) + 7;
let cds = contract(CdsTerms {
upfront_date: Some(given),
..CdsTerms::default()
})
.unwrap();
assert_eq!(Event::date(cds.upfront_payment().as_ref()), given);
assert_eq!(Event::date(cds.accrual_rebate().unwrap().as_ref()), given);
}
#[test]
fn the_accrual_rebate_matches_the_isda_amounts() {
let maturity = Date::new(20, Month::June, 2014);
let expected = [
(Date::new(18, Month::March, 2009), 24_166.67),
(Date::new(19, Month::March, 2009), 0.00),
(Date::new(20, Month::March, 2009), 277.78),
(Date::new(23, Month::March, 2009), 1_111.11),
(Date::new(19, Month::June, 2009), 25_555.56),
(Date::new(20, Month::June, 2009), 25_833.33),
(Date::new(21, Month::June, 2009), 0.00),
(Date::new(22, Month::June, 2009), 277.78),
(Date::new(18, Month::June, 2014), 25_277.78),
(Date::new(19, Month::June, 2014), 25_555.56),
];
for (trade_date, amount) in expected {
let settings = shared(Settings::new());
settings.set_evaluation_date(trade_date);
let calendar = WeekendsOnly::new();
let schedule = Schedule::new(
trade_date,
maturity,
Period::new(3, TimeUnit::Months),
calendar.clone(),
BusinessDayConvention::Following,
BusinessDayConvention::Unadjusted,
DateGeneration::CDS,
false,
Date::null(),
Date::null(),
);
let cds = CreditDefaultSwap::with_upfront_and_terms(
ProtectionSide::Buyer,
NOTIONAL,
0.0,
SPREAD,
schedule,
BusinessDayConvention::Following,
Actual360::new(),
CdsTerms {
protection_start: Some(trade_date),
upfront_date: Some(calendar.advance(
trade_date,
3,
TimeUnit::Days,
BusinessDayConvention::Following,
false,
)),
last_period_day_counter: Some(Actual360::with_last_day(true)),
trade_date: Some(trade_date),
..CdsTerms::default()
},
settings,
)
.unwrap();
let rebate = cds.accrual_rebate().unwrap().amount().unwrap();
assert!(
(rebate - amount).abs() < 0.01,
"a contract traded on {trade_date} rebated {rebate} rather than {amount}"
);
}
}
#[test]
fn the_accrual_rebate_is_a_zero_flow_when_rebated_and_absent_otherwise() {
let rebated = contract(CdsTerms::default()).unwrap();
let rebate = rebated.accrual_rebate().unwrap();
assert!(rebated.rebates_accrual());
assert_eq!(rebate.amount().unwrap(), 0.0);
assert_eq!(
Event::date(rebate.as_ref()),
Event::date(rebated.upfront_payment().as_ref())
);
let bare = contract(CdsTerms {
rebates_accrual: false,
..CdsTerms::default()
})
.unwrap();
assert!(bare.accrual_rebate().is_none());
assert!(!bare.rebates_accrual());
}
#[test]
fn an_empty_schedule_is_an_error_not_a_panic() {
let empty = CreditDefaultSwap::with_terms(
ProtectionSide::Buyer,
NOTIONAL,
SPREAD,
Schedule::from_dates(Vec::new()),
BusinessDayConvention::Following,
Actual360::new(),
CdsTerms::default(),
settings_today(),
);
assert!(empty.is_err());
}
#[test]
fn protection_can_not_start_after_the_first_accrual_date() {
let late = ten_year_schedule().date(0) + 1;
assert!(
contract(CdsTerms {
protection_start: Some(late),
..CdsTerms::default()
})
.is_err()
);
}
#[test]
fn only_a_trade_date_on_or_after_the_first_accrual_date_rebates_anything() {
let first_accrual = ten_year_schedule().date(0);
let before = contract(CdsTerms::default()).unwrap();
assert!(before.trade_date() < first_accrual);
assert_eq!(before.accrual_rebate().unwrap().amount().unwrap(), 0.0);
let on = contract(CdsTerms {
trade_date: Some(first_accrual),
..CdsTerms::default()
})
.unwrap();
let accrued = on.coupons()[0]
.as_coupon()
.unwrap()
.accrued_amount(first_accrual + 1)
.unwrap();
assert!(accrued > 0.0);
assert_eq!(on.accrual_rebate().unwrap().amount().unwrap(), accrued);
}
#[test]
fn a_post_big_bang_contract_trades_on_the_protection_start() {
let schedule = MakeSchedule::new()
.from(Date::new(20, Month::June, 2026))
.to(Date::new(20, Month::June, 2036))
.with_frequency(Frequency::Quarterly)
.with_calendar(WeekendsOnly::new())
.with_convention(BusinessDayConvention::Following)
.with_rule(DateGeneration::CDS)
.build();
let build = |terms| {
CreditDefaultSwap::with_terms(
ProtectionSide::Buyer,
NOTIONAL,
SPREAD,
schedule.clone(),
BusinessDayConvention::Following,
Actual360::new(),
terms,
settings_today(),
)
};
let cds = build(CdsTerms::default()).unwrap();
assert_eq!(cds.trade_date(), cds.protection_start_date());
assert_eq!(cds.protection_start_date(), schedule.date(0));
}
#[test]
fn the_claim_defaults_to_the_face_value() {
let cds = contract(CdsTerms::default()).unwrap();
assert_eq!(
cds.claim().amount(&cds.maturity(), NOTIONAL, 0.4).unwrap(),
NOTIONAL * 0.6
);
}
#[test]
fn the_last_period_day_counter_reaches_the_last_coupon_only() {
let plain = contract(CdsTerms::default()).unwrap();
let overridden = contract(CdsTerms {
last_period_day_counter: Some(Actual365Fixed::new()),
..CdsTerms::default()
})
.unwrap();
let amount = |cds: &CreditDefaultSwap, i: usize| cds.coupons()[i].amount().unwrap();
let last = plain.coupons().len() - 1;
assert_ne!(amount(&plain, last), amount(&overridden, last));
assert_eq!(amount(&plain, 0), amount(&overridden, 0));
}
fn arguments(cds: &CreditDefaultSwap) -> CdsArguments {
let mut arguments = CdsArguments::default();
cds.setup_arguments(&mut arguments).unwrap();
arguments
}
struct StubEngine {
base: CdsEngine,
fills_results: bool,
}
impl AsObservable for StubEngine {
fn observable(&self) -> &Observable {
self.base.observable()
}
}
impl PricingEngine for StubEngine {
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<()> {
if !self.fills_results {
return Ok(());
}
let results = self.base.results_mut();
results.instrument.value = Some(1.0);
results.fair_spread = Some(0.02);
results.fair_upfront = Some(0.03);
results.coupon_leg_bps = Some(4.0);
results.coupon_leg_npv = Some(5.0);
results.default_leg_npv = Some(6.0);
results.upfront_bps = Some(7.0);
results.upfront_npv = Some(8.0);
results.accrual_rebate_npv = Some(9.0);
Ok(())
}
}
fn engine(fills_results: bool) -> SharedMut<StubEngine> {
shared_mut(StubEngine {
base: CdsEngine::new(CdsArguments::default(), CdsResults::default()),
fills_results,
})
}
#[test]
fn setup_arguments_round_trips_the_contract_into_the_bundle() {
let cds = contract(CdsTerms::default()).unwrap();
let arguments = arguments(&cds);
assert_eq!(arguments.side, Some(ProtectionSide::Buyer));
assert_eq!(arguments.notional, Some(NOTIONAL));
assert_eq!(arguments.spread, Some(SPREAD));
assert_eq!(arguments.upfront, None);
assert_eq!(arguments.leg.len(), cds.coupons().len());
assert!(Shared::ptr_eq(&arguments.leg[0], &cds.coupons()[0]));
assert!(Shared::ptr_eq(
arguments.upfront_payment.as_ref().unwrap(),
cds.upfront_payment()
));
assert!(Shared::ptr_eq(
arguments.accrual_rebate.as_ref().unwrap(),
cds.accrual_rebate().unwrap()
));
assert!(arguments.settles_accrual);
assert!(arguments.pays_at_default_time);
assert_eq!(
arguments
.claim
.as_ref()
.unwrap()
.amount(&cds.maturity(), NOTIONAL, 0.4)
.unwrap(),
NOTIONAL * 0.6
);
assert_eq!(
arguments.protection_start,
Some(cds.protection_start_date())
);
assert_eq!(arguments.maturity, Some(cds.maturity()));
}
#[test]
fn an_unrebated_contract_leaves_the_bundle_rebate_empty() {
let cds = contract(CdsTerms {
rebates_accrual: false,
..CdsTerms::default()
})
.unwrap();
let arguments = arguments(&cds);
assert!(arguments.accrual_rebate.is_none());
assert!(arguments.validate().is_ok());
}
#[test]
fn validate_rejects_each_unset_field_with_the_cpp_message() {
let cds = contract(CdsTerms::default()).unwrap();
assert!(arguments(&cds).validate().is_ok());
let rejects = |breaks: fn(&mut CdsArguments), message: &str| {
let mut arguments = arguments(&cds);
breaks(&mut arguments);
assert_eq!(arguments.validate().unwrap_err().message(), message);
};
rejects(|a| a.side = None, "side not set");
rejects(|a| a.notional = None, "notional not set");
rejects(|a| a.notional = Some(0.0), "null notional set");
rejects(|a| a.spread = None, "spread not set");
rejects(|a| a.leg.clear(), "coupons not set");
rejects(|a| a.upfront_payment = None, "upfront payment not set");
rejects(|a| a.claim = None, "claim not set");
rejects(
|a| a.protection_start = None,
"protection start date not set",
);
rejects(|a| a.maturity = None, "maturity date not set");
}
#[test]
fn reset_clears_every_result() {
let mut results = CdsResults {
instrument: InstrumentResults {
value: Some(1.0),
..InstrumentResults::default()
},
fair_spread: Some(0.02),
fair_upfront: Some(0.03),
coupon_leg_bps: Some(4.0),
coupon_leg_npv: Some(5.0),
default_leg_npv: Some(6.0),
upfront_bps: Some(7.0),
upfront_npv: Some(8.0),
accrual_rebate_npv: Some(9.0),
};
results.reset();
assert_eq!(results.instrument.value, None);
assert_eq!(results.fair_spread, None);
assert_eq!(results.fair_upfront, None);
assert_eq!(results.coupon_leg_bps, None);
assert_eq!(results.coupon_leg_npv, None);
assert_eq!(results.default_leg_npv, None);
assert_eq!(results.upfront_bps, None);
assert_eq!(results.upfront_npv, None);
assert_eq!(results.accrual_rebate_npv, None);
}
#[test]
fn the_accessors_read_the_engine_results() {
let mut cds = contract(CdsTerms::default()).unwrap();
cds.base_mut().set_pricing_engine(engine(true));
assert_eq!(cds.npv().unwrap(), 1.0);
assert_eq!(cds.fair_spread().unwrap(), 0.02);
assert_eq!(cds.fair_upfront().unwrap(), 0.03);
assert_eq!(cds.coupon_leg_bps().unwrap(), 4.0);
assert_eq!(cds.coupon_leg_npv().unwrap(), 5.0);
assert_eq!(cds.default_leg_npv().unwrap(), 6.0);
assert_eq!(cds.upfront_bps().unwrap(), 7.0);
assert_eq!(cds.upfront_npv().unwrap(), 8.0);
assert_eq!(cds.accrual_rebate_npv().unwrap(), 9.0);
}
#[test]
fn unprovided_results_are_not_available() {
let mut cds = contract(CdsTerms::default()).unwrap();
cds.base_mut().set_pricing_engine(engine(false));
let message = |result: QlResult<Real>| result.unwrap_err().message().to_string();
assert_eq!(message(cds.fair_spread()), "fair spread not available");
assert_eq!(message(cds.fair_upfront()), "fair upfront not available");
assert_eq!(
message(cds.coupon_leg_bps()),
"coupon-leg BPS not available"
);
assert_eq!(
message(cds.coupon_leg_npv()),
"coupon-leg NPV not available"
);
assert_eq!(
message(cds.default_leg_npv()),
"default-leg NPV not available"
);
assert_eq!(message(cds.upfront_bps()), "upfront BPS not available");
assert_eq!(message(cds.upfront_npv()), "upfront NPV not available");
assert_eq!(
message(cds.accrual_rebate_npv()),
"accrual Rebate NPV not available"
);
}
#[test]
fn an_accessor_on_an_unpriced_contract_reports_the_missing_engine() {
let mut cds = contract(CdsTerms::default()).unwrap();
assert_eq!(
cds.fair_spread().unwrap_err().message(),
"null pricing engine"
);
}
#[test]
fn an_expired_contract_zeroes_seven_results_and_leaves_the_rebate_unavailable() {
let settings = shared(Settings::new());
settings.set_evaluation_date(Date::new(21, Month::June, 2036));
let mut cds = contract_priced_on(CdsTerms::default(), settings).unwrap();
assert!(cds.is_expired().unwrap());
assert_eq!(cds.npv().unwrap(), 0.0);
assert_eq!(cds.fair_spread().unwrap(), 0.0);
assert_eq!(cds.fair_upfront().unwrap(), 0.0);
assert_eq!(cds.coupon_leg_bps().unwrap(), 0.0);
assert_eq!(cds.coupon_leg_npv().unwrap(), 0.0);
assert_eq!(cds.default_leg_npv().unwrap(), 0.0);
assert_eq!(cds.upfront_bps().unwrap(), 0.0);
assert_eq!(cds.upfront_npv().unwrap(), 0.0);
assert_eq!(
cds.accrual_rebate_npv().unwrap_err().message(),
"accrual Rebate NPV not available"
);
}
#[test]
fn is_expired_tracks_the_premium_legs_flows() {
assert!(!contract(CdsTerms::default()).unwrap().is_expired().unwrap());
let settings = shared(Settings::new());
settings.set_evaluation_date(Date::new(19, Month::June, 2036));
let last_coupon_due = contract_priced_on(CdsTerms::default(), settings).unwrap();
assert!(
!last_coupon_due.is_expired().unwrap(),
"the final premium flow has not paid yet"
);
let settings = shared(Settings::new());
settings.set_evaluation_date(Date::new(21, Month::June, 2036));
let matured = contract_priced_on(CdsTerms::default(), settings).unwrap();
assert!(
matured.is_expired().unwrap(),
"the final premium flow has paid"
);
}
#[test]
fn an_evaluation_date_change_invalidates_the_contract() {
let settings = settings_today();
let mut cds = contract_priced_on(CdsTerms::default(), Shared::clone(&settings)).unwrap();
cds.base_mut().set_pricing_engine(engine(true));
cds.npv().unwrap();
assert!(cds.base().is_calculated());
settings.set_evaluation_date(today() + 1);
assert!(
!cds.base().is_calculated(),
"an evaluation-date change invalidates the contract"
);
}
fn rejection(tenor: Period, rule: DateGeneration) -> String {
cds_maturity(Date::new(1, Month::March, 2017), tenor, rule)
.unwrap_err()
.message()
.to_string()
}
#[test]
fn only_the_three_cds_rules_imply_a_maturity() {
let message = rejection(
Period::new(5, TimeUnit::Years),
DateGeneration::TwentiethIMM,
);
assert!(message.contains("CDS2015, CDS or OldCDS"), "{message}");
}
#[test]
fn a_tenor_that_is_not_a_whole_number_of_quarters_is_rejected() {
let message = rejection(Period::new(4, TimeUnit::Months), DateGeneration::CDS2015);
assert!(message.contains("multiple of 3 months"), "{message}");
}
#[test]
fn a_zero_tenor_is_rejected_under_the_old_rule_alone() {
let zero = Period::new(0, TimeUnit::Months);
let message = rejection(zero, DateGeneration::OldCDS);
assert!(
message.contains("0M is not supported for OldCDS"),
"{message}"
);
let message = rejection(Period::new(0, TimeUnit::Years), DateGeneration::OldCDS);
assert!(
message.contains("0M is not supported for OldCDS"),
"a zero tenor quoted in years is the same zero: {message}"
);
let live = cds_maturity(
Date::new(1, Month::December, 2016),
zero,
DateGeneration::CDS2015,
);
assert_eq!(live.unwrap(), Some(Date::new(20, Month::December, 2016)));
}
#[test]
fn a_maturity_on_or_before_the_trade_date_is_rejected() {
let message = rejection(Period::new(-1, TimeUnit::Years), DateGeneration::CDS);
assert!(message.contains("<= trade date"), "{message}");
}
#[test]
fn the_implied_hazard_rate_brackets_the_curve_and_reprices_the_contract() {
const H1: Rate = 0.30;
const H2: Rate = 0.40;
const RECOVERY: Real = 0.4;
let calendar = Target::new();
let today = Date::new(15, Month::June, 2026);
let settings = shared(Settings::new());
settings.set_evaluation_date(today);
let probability_curve = Handle::new(shared(
InterpolatedHazardRateCurve::new(
vec![
today,
today + Period::new(5, TimeUnit::Years),
today + Period::new(10, TimeUnit::Years),
],
vec![H1, H1, H2],
Actual365Fixed::new(),
BackwardFlat,
)
.unwrap(),
)
as Shared<dyn DefaultProbabilityTermStructure>);
let discount_curve = Handle::new(shared(FlatForward::with_rate(
today,
0.03,
Actual360::new(),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>);
let issue_date = calendar.advance(
today,
-6,
TimeUnit::Months,
BusinessDayConvention::Following,
false,
);
let mut previous: Option<Rate> = None;
for n in 6..=10 {
let maturity = calendar.advance(
issue_date,
n,
TimeUnit::Years,
BusinessDayConvention::Following,
false,
);
let schedule = Schedule::new(
issue_date,
maturity,
Period::new(6, TimeUnit::Months),
calendar.clone(),
BusinessDayConvention::ModifiedFollowing,
BusinessDayConvention::ModifiedFollowing,
DateGeneration::Forward,
false,
Date::null(),
Date::null(),
);
let priced_on = |probability: Handle<dyn DefaultProbabilityTermStructure>| {
let mut cds = CreditDefaultSwap::new(
ProtectionSide::Seller,
10_000.0,
0.0120,
schedule.clone(),
BusinessDayConvention::ModifiedFollowing,
Actual360::new(),
true,
true,
Shared::clone(&settings),
)
.unwrap();
cds.base_mut()
.set_pricing_engine(shared_mut(MidPointCdsEngine::new(
probability,
RECOVERY,
discount_curve.clone(),
None,
Shared::clone(&settings),
)) as SharedMut<dyn PricingEngine>);
cds
};
let mut cds = priced_on(probability_curve.clone());
let npv = cds.npv().unwrap();
let rate = cds
.implied_hazard_rate(
npv,
&discount_curve,
Actual365Fixed::new(),
RECOVERY,
1.0e-8,
PricingModel::Midpoint,
)
.unwrap();
assert!(
(H1..=H2).contains(&rate),
"the {n}Y implied rate {rate} is outside [{H1}, {H2}]"
);
if let Some(previous) = previous {
assert!(
rate >= previous,
"the {n}Y implied rate {rate} falls below the previous {previous}"
);
}
previous = Some(rate);
let flat = Handle::new(shared(FlatHazardRate::new(
today,
Handle::new(shared(SimpleQuote::new(rate)) as Shared<dyn Quote>),
Actual365Fixed::new(),
))
as Shared<dyn DefaultProbabilityTermStructure>);
let reproduced = priced_on(flat).npv().unwrap();
assert!(
(npv - reproduced).abs() < 1.0,
"the {n}Y implied rate reprices to {reproduced}, not {npv}"
);
}
}
fn conventional_spread_fixture(
day_counter: DayCounter,
) -> (
Shared<Settings<Date>>,
Date,
Schedule,
Handle<dyn YieldTermStructure>,
) {
let calendar = Target::new();
let today = Date::new(15, Month::June, 2026);
let settings = shared(Settings::new());
settings.set_evaluation_date(today);
let discount_curve = Handle::new(shared(FlatForward::with_rate(
today,
0.03,
day_counter,
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>);
let maturity = calendar.advance(
today,
8,
TimeUnit::Years,
BusinessDayConvention::Following,
false,
);
let schedule = Schedule::new(
today,
maturity,
Period::new(6, TimeUnit::Months),
calendar,
BusinessDayConvention::ModifiedFollowing,
BusinessDayConvention::ModifiedFollowing,
DateGeneration::Forward,
false,
Date::null(),
Date::null(),
);
(settings, today, schedule, discount_curve)
}
const CONVENTIONAL_RECOVERY: Real = 0.4;
const CONVENTIONAL_RUNNING: Rate = 0.0120;
const CONVENTIONAL_UPFRONT: Rate = 0.05;
const CONVENTIONAL_NOTIONAL: Real = 10_000.0;
fn upfront_quoted(schedule: Schedule, settings: &Shared<Settings<Date>>) -> CreditDefaultSwap {
CreditDefaultSwap::with_upfront(
ProtectionSide::Seller,
CONVENTIONAL_NOTIONAL,
CONVENTIONAL_UPFRONT,
CONVENTIONAL_RUNNING,
schedule,
BusinessDayConvention::ModifiedFollowing,
Actual360::new(),
true,
true,
Shared::clone(settings),
)
.unwrap()
}
fn flat_credit(
reference: Date,
hazard_rate: Rate,
) -> Handle<dyn DefaultProbabilityTermStructure> {
Handle::new(shared(FlatHazardRate::new(
reference,
Handle::new(shared(SimpleQuote::new(hazard_rate)) as Shared<dyn Quote>),
Actual365Fixed::new(),
)) as Shared<dyn DefaultProbabilityTermStructure>)
}
#[test]
fn the_conventional_spread_converts_an_upfront_into_a_running_spread() {
let (settings, today, schedule, discount_curve) =
conventional_spread_fixture(Actual360::new());
let quoted = upfront_quoted(schedule.clone(), &settings);
let hazard_rate = quoted
.implied_hazard_rate(
0.0,
&discount_curve,
Actual365Fixed::new(),
CONVENTIONAL_RECOVERY,
1.0e-9,
PricingModel::Midpoint,
)
.unwrap();
let conventional = quoted
.conventional_spread(
CONVENTIONAL_RECOVERY,
&discount_curve,
Actual365Fixed::new(),
PricingModel::Midpoint,
)
.unwrap();
assert!(
(conventional - CONVENTIONAL_RUNNING).abs() > 1.0e-4,
"the upfront left the conventional spread {conventional} at the running \
{CONVENTIONAL_RUNNING}"
);
let mut converted = CreditDefaultSwap::new(
ProtectionSide::Seller,
CONVENTIONAL_NOTIONAL,
conventional,
schedule,
BusinessDayConvention::ModifiedFollowing,
Actual360::new(),
true,
true,
Shared::clone(&settings),
)
.unwrap();
converted
.base_mut()
.set_pricing_engine(shared_mut(MidPointCdsEngine::new(
flat_credit(today, hazard_rate),
CONVENTIONAL_RECOVERY,
discount_curve,
None,
Shared::clone(&settings),
)) as SharedMut<dyn PricingEngine>);
let npv = converted.npv().unwrap();
assert!(
npv.abs() < 1.0,
"the contract converted to {conventional} prices at {npv}, not nothing"
);
}
#[test]
fn the_conventional_spread_converts_an_upfront_on_the_isda_model() {
let (settings, today, schedule, discount_curve) =
conventional_spread_fixture(Actual365Fixed::new());
let quoted = upfront_quoted(schedule.clone(), &settings);
let hazard_rate = quoted
.implied_hazard_rate(
0.0,
&discount_curve,
Actual365Fixed::new(),
CONVENTIONAL_RECOVERY,
1.0e-9,
PricingModel::Isda,
)
.unwrap();
let conventional = quoted
.conventional_spread(
CONVENTIONAL_RECOVERY,
&discount_curve,
Actual365Fixed::new(),
PricingModel::Isda,
)
.unwrap();
assert!(
(conventional - CONVENTIONAL_RUNNING).abs() > 1.0e-4,
"the upfront left the conventional spread {conventional} at the running \
{CONVENTIONAL_RUNNING}"
);
let mut converted = CreditDefaultSwap::new(
ProtectionSide::Seller,
CONVENTIONAL_NOTIONAL,
conventional,
schedule,
BusinessDayConvention::ModifiedFollowing,
Actual360::new(),
true,
true,
Shared::clone(&settings),
)
.unwrap();
converted.base_mut().set_pricing_engine(shared_mut(
IsdaCdsEngine::new(
flat_credit(today, hazard_rate),
CONVENTIONAL_RECOVERY,
discount_curve,
None,
Shared::clone(&settings),
)
.with_fidelity(
NumericalFix::Taylor,
AccrualBias::HalfDayBias,
ForwardsInCouponPeriod::Piecewise,
),
) as SharedMut<dyn PricingEngine>);
let npv = converted.npv().unwrap();
assert!(
npv.abs() < 1.0,
"the contract converted to {conventional} prices at {npv}, not nothing"
);
}
}