use std::any::Any;
use crate::cashflow::Leg;
use crate::cashflows::{FixedRateLeg, SimpleCashFlow};
use crate::errors::QlResult;
use crate::instrument::{Instrument, InstrumentBase, InstrumentResults};
use crate::instruments::claim::{Claim, FaceValueClaim};
use crate::instruments::protection::ProtectionSide;
use crate::interestrate::Compounding;
use crate::pricingengine::{Arguments, GenericEngine, Results};
use crate::settings::Settings;
use crate::shared::{Shared, shared};
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::date::Date;
use crate::time::dategenerationrule::DateGeneration;
use crate::time::daycounter::DayCounter;
use crate::time::frequency::Frequency;
use crate::time::schedule::Schedule;
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 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,
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>;
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> {
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 = 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_payment = shared(SimpleCashFlow::new(0.0, effective_upfront_date)?);
let accrual_rebate = if terms.rebates_accrual {
require!(
trade_date < schedule.date(0),
"a trade date on or after the first accrual date rebates the accrued coupon, \
which is not ported yet (creditdefaultswap.cpp:143-168)"
);
Some(shared(SimpleCashFlow::new(0.0, effective_upfront_date)?))
} else {
None
};
let base = InstrumentBase::new();
settings.register_eval_date_observer(&base.observer());
Ok(CreditDefaultSwap {
base,
settings,
side,
notional,
upfront: None,
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)
}
}
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(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cashflow::CashFlow;
use crate::event::Event;
use crate::patterns::observable::{AsObservable, Observable};
use crate::pricingengine::PricingEngine;
use crate::shared::{SharedMut, shared_mut};
use crate::time::calendars::weekendsonly::WeekendsOnly;
use crate::time::date::Month;
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_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 a_trade_date_on_or_after_the_first_accrual_date_is_refused_while_rebating() {
let first_accrual = ten_year_schedule().date(0);
assert!(
contract(CdsTerms {
trade_date: Some(first_accrual),
..CdsTerms::default()
})
.is_err()
);
let bare = contract(CdsTerms {
trade_date: Some(first_accrual),
rebates_accrual: false,
..CdsTerms::default()
})
.unwrap();
assert_eq!(bare.trade_date(), first_accrual);
}
#[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 {
rebates_accrual: false,
..CdsTerms::default()
})
.unwrap();
assert_eq!(cds.trade_date(), cds.protection_start_date());
assert_eq!(cds.protection_start_date(), schedule.date(0));
assert!(build(CdsTerms::default()).is_err());
}
#[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),
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),
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"
);
}
}