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::types::{Rate, Real};
use crate::{fail, handle::Handle, require};
const BASIS_POINT: Rate = 1.0e-4;
pub struct MidPointCdsEngine {
base: CdsEngine,
probability: Handle<dyn DefaultProbabilityTermStructure>,
recovery_rate: Real,
discount_curve: Handle<dyn YieldTermStructure>,
include_settlement_date_flows: Option<bool>,
settings: Shared<Settings<Date>>,
}
impl MidPointCdsEngine {
pub fn new(
probability: Handle<dyn DefaultProbabilityTermStructure>,
recovery_rate: Real,
discount_curve: Handle<dyn YieldTermStructure>,
include_settlement_date_flows: Option<bool>,
settings: Shared<Settings<Date>>,
) -> MidPointCdsEngine {
let base = CdsEngine::new(CdsArguments::default(), CdsResults::default());
probability.register_observer(&base.observer());
discount_curve.register_observer(&base.observer());
MidPointCdsEngine {
base,
probability,
recovery_rate,
discount_curve,
include_settlement_date_flows,
settings,
}
}
pub fn probability(&self) -> &Handle<dyn DefaultProbabilityTermStructure> {
&self.probability
}
pub fn discount_curve(&self) -> &Handle<dyn YieldTermStructure> {
&self.discount_curve
}
}
impl AsObservable for MidPointCdsEngine {
fn observable(&self) -> &Observable {
self.base.observable()
}
}
impl PricingEngine for MidPointCdsEngine {
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<()> {
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 mid-point 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 default_date = effective_start_date + (end_date - effective_start_date) / 2;
let survival = probability.survival_probability_date(payment_date, false)?;
let default = probability.default_probability_between_dates(
effective_start_date,
end_date,
false,
)?;
coupon_leg_npv +=
survival * coupon.amount()? * discount.discount_date(payment_date, false)?;
if arguments.settles_accrual {
if arguments.pays_at_default_time {
coupon_leg_npv += default
* coupon.accrued_amount(default_date)?
* discount.discount_date(default_date, false)?;
} else {
coupon_leg_npv +=
default * coupon.amount()? * discount.discount_date(payment_date, false)?;
}
}
let claim_amount = claim.amount(&default_date, notional, self.recovery_rate)?;
if arguments.pays_at_default_time {
default_leg_npv +=
default * claim_amount * discount.discount_date(default_date, false)?;
} else {
default_leg_npv +=
default * claim_amount * discount.discount_date(payment_date, false)?;
}
}
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 tests {
use super::*;
use crate::cashflows::SimpleCashFlow;
use crate::instrument::Instrument;
use crate::instruments::{CdsTerms, CreditDefaultSwap};
use crate::interestrate::Compounding;
use crate::shared::{SharedMut, shared, shared_mut};
use crate::termstructures::credit::flathazardrate::FlatHazardRate;
use crate::termstructures::yields::FlatForward;
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::calendars::weekendsonly::WeekendsOnly;
use crate::time::date::{Day, Month, Year};
use crate::time::daycounters::actual360::Actual360;
use crate::time::daycounters::actual365fixed::Actual365Fixed;
use crate::time::frequency::Frequency;
use crate::time::schedule::MakeSchedule;
const NOTIONAL: Real = 10_000_000.0;
const SPREAD: Rate = 0.01;
const RECOVERY: Real = 0.4;
fn date(day: Day, month: Month, year: Year) -> Date {
Date::new(day, month, year)
}
struct Vars {
settings: Shared<Settings<Date>>,
probability: Handle<dyn DefaultProbabilityTermStructure>,
discount: Handle<dyn YieldTermStructure>,
}
impl Vars {
fn new(today: Date) -> Vars {
Vars::with_settlement(today, today)
}
fn with_settlement(today: Date, settlement: Date) -> Vars {
let settings = shared(Settings::new());
settings.set_evaluation_date(today);
let probability = Handle::new(shared(FlatHazardRate::with_rate(
today,
0.02,
Actual365Fixed::new(),
))
as Shared<dyn DefaultProbabilityTermStructure>);
let discount = Handle::new(shared(FlatForward::with_rate(
settlement,
0.03,
Actual365Fixed::new(),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>);
Vars {
settings,
probability,
discount,
}
}
fn engine(&self, include_settlement_date_flows: Option<bool>) -> MidPointCdsEngine {
MidPointCdsEngine::new(
self.probability.clone(),
RECOVERY,
self.discount.clone(),
include_settlement_date_flows,
Shared::clone(&self.settings),
)
}
fn contract(
&self,
side: ProtectionSide,
start: Date,
end: Date,
terms: CdsTerms,
) -> CreditDefaultSwap {
let schedule = MakeSchedule::new()
.from(start)
.to(end)
.with_frequency(Frequency::Semiannual)
.with_calendar(WeekendsOnly::new())
.with_convention(BusinessDayConvention::Following)
.with_termination_date_convention(BusinessDayConvention::Unadjusted)
.backwards()
.build();
CreditDefaultSwap::with_terms(
side,
NOTIONAL,
SPREAD,
schedule,
BusinessDayConvention::Following,
Actual360::new(),
terms,
Shared::clone(&self.settings),
)
.unwrap()
}
fn priced(&self, mut cds: CreditDefaultSwap, include: Option<bool>) -> CreditDefaultSwap {
cds.base_mut().set_pricing_engine(
shared_mut(self.engine(include)) as SharedMut<dyn PricingEngine>
);
cds
}
fn direct(&self, cds: &CreditDefaultSwap) -> MidPointCdsEngine {
let mut engine = self.engine(None);
cds.setup_arguments(engine.base.arguments_mut()).unwrap();
engine.calculate().unwrap();
engine
}
}
fn straddling(vars: &Vars, side: ProtectionSide) -> CreditDefaultSwap {
vars.contract(
side,
date(15, Month::December, 2025),
date(15, Month::December, 2027),
CdsTerms::default(),
)
}
#[test]
fn the_coupon_paying_on_the_settlement_date_rides_on_the_include_flag() {
let vars = Vars::new(date(15, Month::June, 2026));
let excluded = vars
.priced(straddling(&vars, ProtectionSide::Seller), None)
.npv()
.unwrap();
let mut included = vars.priced(straddling(&vars, ProtectionSide::Seller), Some(true));
let amount = included.coupons()[0].amount().unwrap();
let included = included.npv().unwrap();
assert!(amount > 0.0, "the dropped coupon pays nothing to detect");
assert!(
(included - excluded - amount).abs() <= 1.0e-8 * amount,
"including the settlement-date coupon moved the NPV by {} rather than its amount {amount}",
included - excluded
);
}
#[test]
fn the_first_period_accrues_from_the_protection_start() {
let vars = Vars::new(date(15, Month::December, 2025));
let start = date(15, Month::June, 2026);
let end = date(15, Month::June, 2028);
let from_schedule = |protection_start| {
let cds = vars.contract(
ProtectionSide::Seller,
start,
end,
CdsTerms {
protection_start,
..CdsTerms::default()
},
);
let mut cds = vars.priced(cds, None);
cds.npv().unwrap();
cds
};
let mut scheduled = from_schedule(None);
let mut earlier = from_schedule(Some(date(15, Month::December, 2025)));
let (scheduled_leg, earlier_leg) = (
scheduled.default_leg_npv().unwrap(),
earlier.default_leg_npv().unwrap(),
);
assert!(
earlier_leg < scheduled_leg,
"an earlier protection start left the seller's protection leg at {earlier_leg} rather than below {scheduled_leg}"
);
assert!(
earlier.fair_upfront().is_ok(),
"an upfront payment still due should price a fair upfront"
);
}
#[test]
fn the_protection_start_overrides_on_the_position_in_the_leg() {
let vars = Vars::with_settlement(
date(15, Month::December, 2025),
date(15, Month::December, 2026),
);
let priced = |protection_start| {
let cds = vars.contract(
ProtectionSide::Seller,
date(15, Month::June, 2026),
date(15, Month::June, 2028),
CdsTerms {
protection_start,
..CdsTerms::default()
},
);
vars.priced(cds, None).npv().unwrap()
};
let scheduled = priced(None);
assert!(scheduled != 0.0, "a zero contract would make this vacuous");
assert_eq!(scheduled, priced(Some(date(15, Month::December, 2025))));
}
#[test]
fn the_two_sides_value_a_contract_as_exact_opposites() {
let vars = Vars::new(date(15, Month::June, 2026));
let seller = vars
.priced(straddling(&vars, ProtectionSide::Seller), None)
.npv()
.unwrap();
let buyer = vars
.priced(straddling(&vars, ProtectionSide::Buyer), None)
.npv()
.unwrap();
assert!(seller != 0.0, "a zero contract would make this vacuous");
assert_eq!(seller, -buyer);
}
#[test]
fn paying_at_the_period_end_moves_both_legs_the_way_discounting_says() {
let vars = Vars::new(date(15, Month::December, 2025));
let priced = |pays_at_default_time| {
let cds = vars.contract(
ProtectionSide::Seller,
date(15, Month::June, 2026),
date(15, Month::June, 2028),
CdsTerms {
pays_at_default_time,
..CdsTerms::default()
},
);
let mut cds = vars.priced(cds, None);
cds.npv().unwrap();
cds
};
let mut at_default = priced(true);
let mut at_end = priced(false);
assert!(at_default.default_leg_npv().unwrap() < 0.0);
assert!(
at_end.default_leg_npv().unwrap() > at_default.default_leg_npv().unwrap(),
"protection paid at the period end should be worth less, not more"
);
assert!(
at_end.coupon_leg_npv().unwrap() > at_default.coupon_leg_npv().unwrap(),
"a whole coupon settled at the period end should beat the half accrued to the mid-point"
);
}
#[test]
fn a_fully_occurred_leg_prices_to_nothing_and_quotes_neither_fair_level() {
let vars = Vars::new(date(15, Month::December, 2027));
let cds = straddling(&vars, ProtectionSide::Seller);
let engine = vars.direct(&cds);
let results = engine.base.results();
assert_eq!(results.coupon_leg_npv, Some(0.0));
assert_eq!(results.default_leg_npv, Some(0.0));
assert_eq!(results.instrument.value, Some(0.0));
assert_eq!(results.fair_spread, None);
assert_eq!(results.fair_upfront, None);
assert_eq!(results.coupon_leg_bps, Some(0.0));
assert_eq!(results.upfront_bps, None);
}
#[test]
fn a_premium_flow_that_is_not_a_coupon_is_rejected() {
let vars = Vars::new(date(15, Month::June, 2026));
let cds = straddling(&vars, ProtectionSide::Seller);
let mut engine = vars.engine(None);
let arguments = engine.base.arguments_mut();
cds.setup_arguments(arguments).unwrap();
arguments.leg[1] = shared(SimpleCashFlow::new(1.0, date(15, Month::June, 2027)).unwrap())
as Shared<dyn CashFlow>;
assert_eq!(
engine.calculate().unwrap_err().message(),
"premium leg flow #2 is not a coupon"
);
}
#[test]
fn an_empty_curve_handle_is_rejected() {
let vars = Vars::new(date(15, Month::June, 2026));
let mut no_discount = MidPointCdsEngine::new(
vars.probability.clone(),
RECOVERY,
Handle::empty(),
None,
Shared::clone(&vars.settings),
);
assert_eq!(
no_discount.calculate().unwrap_err().message(),
"no discount term structure set"
);
let mut no_probability = MidPointCdsEngine::new(
Handle::empty(),
RECOVERY,
vars.discount.clone(),
None,
Shared::clone(&vars.settings),
);
assert_eq!(
no_probability.calculate().unwrap_err().message(),
"no probability term structure set"
);
}
}
#[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;
use crate::time::calendar::Calendar;
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::frequency::Frequency;
use crate::time::period::Period;
use crate::time::schedule::{MakeSchedule, Schedule};
use crate::time::timeunit::TimeUnit;
const NOTIONAL: Real = 10_000.0;
const RECOVERY: Real = 0.4;
struct CommonVars {
settings: Shared<Settings<Date>>,
calendar: Calendar,
today: Date,
probability: Handle<dyn DefaultProbabilityTermStructure>,
discount: Handle<dyn YieldTermStructure>,
}
impl CommonVars {
fn new() -> CommonVars {
let today = Date::new(9, Month::June, 2006);
let settings = shared(Settings::new());
settings.set_evaluation_date(today);
let calendar = Target::new();
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>);
CommonVars {
settings,
calendar,
today,
probability,
discount,
}
}
fn engine(
&self,
include_settlement_date_flows: Option<bool>,
) -> SharedMut<dyn PricingEngine> {
shared_mut(MidPointCdsEngine::new(
self.probability.clone(),
RECOVERY,
self.discount.clone(),
include_settlement_date_flows,
Shared::clone(&self.settings),
)) as SharedMut<dyn PricingEngine>
}
fn issue_and_maturity(&self) -> (Date, Date) {
let issue_date = self.calendar.advance(
self.today,
-1,
TimeUnit::Years,
BusinessDayConvention::Following,
false,
);
let maturity = self.calendar.advance(
issue_date,
10,
TimeUnit::Years,
BusinessDayConvention::Following,
false,
);
(issue_date, maturity)
}
fn contract(
&self,
spread: Rate,
schedule: Schedule,
convention: BusinessDayConvention,
) -> CreditDefaultSwap {
CreditDefaultSwap::new(
ProtectionSide::Seller,
NOTIONAL,
spread,
schedule,
convention,
Actual360::new(),
true,
true,
Shared::clone(&self.settings),
)
.unwrap()
}
fn upfront_contract(
&self,
upfront: Rate,
spread: Rate,
schedule: Schedule,
convention: BusinessDayConvention,
) -> CreditDefaultSwap {
CreditDefaultSwap::with_upfront(
ProtectionSide::Seller,
NOTIONAL,
upfront,
spread,
schedule,
convention,
Actual360::new(),
true,
true,
Shared::clone(&self.settings),
)
.unwrap()
}
}
#[test]
fn a_ten_year_contract_matches_the_cached_mid_point_value() {
let vars = CommonVars::new();
let (issue_date, maturity) = vars.issue_and_maturity();
let convention = BusinessDayConvention::ModifiedFollowing;
let schedule = Schedule::new(
issue_date,
maturity,
Period::try_from(Frequency::Semiannual).unwrap(),
vars.calendar.clone(),
convention,
convention,
DateGeneration::Forward,
false,
Date::null(),
Date::null(),
);
let mut cds = vars.contract(0.0120, schedule, convention);
cds.base_mut().set_pricing_engine(vars.engine(None));
let npv = cds.npv().unwrap();
let fair_spread = cds.fair_spread().unwrap();
assert!(
(npv - 295.015_339_8).abs() <= 1.0e-7,
"the mid-point engine priced the cached contract at {npv} rather than 295.0153398"
);
assert!(
(fair_spread - 0.007_517_539_081).abs() <= 1.0e-7,
"the mid-point engine quoted a fair spread of {fair_spread} rather than 0.007517539081"
);
}
#[test]
fn repricing_at_the_fair_spread_prices_the_contract_to_nothing() {
let vars = CommonVars::new();
let (issue_date, maturity) = vars.issue_and_maturity();
let convention = BusinessDayConvention::Following;
let schedule = MakeSchedule::new()
.from(issue_date)
.to(maturity)
.with_frequency(Frequency::Quarterly)
.with_calendar(vars.calendar.clone())
.with_termination_date_convention(convention)
.with_rule(DateGeneration::TwentiethIMM)
.build();
let engine = vars.engine(None);
let mut cds = vars.contract(0.001, schedule.clone(), convention);
cds.base_mut().set_pricing_engine(SharedMut::clone(&engine));
let fair_rate = cds.fair_spread().unwrap();
let mut fair_cds = vars.contract(fair_rate, schedule, convention);
fair_cds.base_mut().set_pricing_engine(engine);
let fair_npv = fair_cds.npv().unwrap();
assert!(
fair_rate > 0.0,
"a non-positive fair spread of {fair_rate} would make the round trip vacuous"
);
assert!(
fair_npv.abs() < 1.0e-9,
"the contract rebuilt at its fair spread {fair_rate} priced at {fair_npv} rather than nothing"
);
}
#[test]
fn repricing_at_the_fair_upfront_prices_the_contract_to_nothing() {
let vars = CommonVars::new();
let convention = BusinessDayConvention::Following;
let maturity = vars
.calendar
.advance(vars.today, 10, TimeUnit::Years, convention, false);
let schedule = MakeSchedule::new()
.from(vars.today)
.to(maturity)
.with_frequency(Frequency::Quarterly)
.with_calendar(vars.calendar.clone())
.with_termination_date_convention(convention)
.with_rule(DateGeneration::TwentiethIMM)
.build();
let fixed_rate = 0.05;
for upfront in [0.001, 0.0] {
let engine = vars.engine(Some(true));
let mut cds = vars.upfront_contract(upfront, fixed_rate, schedule.clone(), convention);
assert_eq!(cds.upfront(), Some(upfront));
cds.base_mut().set_pricing_engine(SharedMut::clone(&engine));
let quoted_npv = cds.npv().unwrap();
let fair_upfront = cds.fair_upfront().unwrap();
assert!(
quoted_npv.abs() > 1.0,
"a contract already worth {quoted_npv} would make the round trip vacuous"
);
let mut fair_cds =
vars.upfront_contract(fair_upfront, fixed_rate, schedule.clone(), convention);
fair_cds.base_mut().set_pricing_engine(engine);
let fair_npv = fair_cds.npv().unwrap();
assert!(
fair_npv.abs() < 1.0e-9,
"the contract quoted at {upfront} and rebuilt at its fair upfront {fair_upfront} \
priced at {fair_npv} rather than nothing"
);
}
}
}