use std::fmt::Display;
use chrono::NaiveDate;
use super::survival_curve::SurvivalCurve;
use crate::calendar::DayCountConvention;
use crate::calendar::Frequency;
use crate::calendar::Schedule;
use crate::cashflows::CurveProvider;
use crate::traits::FloatExt;
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CdsPosition {
#[default]
Buyer,
Seller,
}
impl Display for CdsPosition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Buyer => write!(f, "Protection buyer"),
Self::Seller => write!(f, "Protection seller"),
}
}
}
#[derive(Debug, Clone)]
pub struct CdsValuation<T: FloatExt> {
pub protection_leg_npv: T,
pub premium_leg_coupons_npv: T,
pub premium_leg_accrual_npv: T,
pub premium_leg_npv: T,
pub risky_annuity: T,
pub fair_spread: T,
pub net_npv: T,
pub direction: CdsPosition,
}
#[derive(Debug, Clone)]
pub struct CreditDefaultSwap<T: FloatExt> {
pub direction: CdsPosition,
pub notional: T,
pub spread: T,
pub recovery_rate: T,
pub premium_schedule: Schedule,
pub day_count: DayCountConvention,
pub effective_date: NaiveDate,
pub maturity_date: NaiveDate,
pub accrual_on_default: bool,
pub integration_step_days: i64,
}
impl<T: FloatExt> CreditDefaultSwap<T> {
pub fn new(
direction: CdsPosition,
notional: T,
spread: T,
recovery_rate: T,
premium_schedule: Schedule,
day_count: DayCountConvention,
effective_date: NaiveDate,
maturity_date: NaiveDate,
) -> Self {
assert!(
recovery_rate >= T::zero() && recovery_rate < T::one(),
"recovery_rate must lie in [0, 1)"
);
assert!(
premium_schedule.adjusted_dates.len() >= 2,
"premium schedule must contain at least two dates"
);
assert!(
effective_date < maturity_date,
"effective_date must precede maturity_date"
);
Self {
direction,
notional,
spread,
recovery_rate,
premium_schedule,
day_count,
effective_date,
maturity_date,
accrual_on_default: true,
integration_step_days: 1,
}
}
pub fn vanilla(
direction: CdsPosition,
notional: T,
spread: T,
recovery_rate: T,
effective_date: NaiveDate,
maturity_date: NaiveDate,
frequency: Frequency,
day_count: DayCountConvention,
) -> Self {
let schedule = crate::calendar::ScheduleBuilder::new(effective_date, maturity_date)
.frequency(frequency)
.build();
Self::new(
direction,
notional,
spread,
recovery_rate,
schedule,
day_count,
effective_date,
maturity_date,
)
}
pub fn with_integration_step(mut self, days: i64) -> Self {
assert!(days >= 1, "integration step must be at least 1 day");
self.integration_step_days = days;
self
}
pub fn with_accrual_on_default(mut self, enabled: bool) -> Self {
self.accrual_on_default = enabled;
self
}
pub fn valuation(
&self,
valuation_date: NaiveDate,
discount_day_count: DayCountConvention,
discount: &(impl CurveProvider<T> + ?Sized),
survival: &SurvivalCurve<T>,
) -> CdsValuation<T> {
let discount = discount.discount_curve();
let mut premium_coupons_unit = T::zero();
let mut premium_accrual_unit = T::zero();
let mut protection_unit = T::zero();
for window in self.premium_schedule.adjusted_dates.windows(2) {
let coupon_start = window[0];
let coupon_end = window[1];
if coupon_end <= valuation_date {
continue;
}
let alpha: T = self.day_count.year_fraction(coupon_start, coupon_end);
let payment_tau: T = tau_year_fraction(discount_day_count, valuation_date, coupon_end);
let df_end = discount.discount_factor(payment_tau);
let q_end = survival.survival_probability(tau_year_fraction(
DayCountConvention::Actual365Fixed,
valuation_date,
coupon_end,
));
premium_coupons_unit += alpha * df_end * q_end;
let (accrual_unit, protection_period) = self.integrate_period(
valuation_date,
discount_day_count,
coupon_start.max(self.effective_date).max(valuation_date),
coupon_end.min(self.maturity_date),
discount,
survival,
);
if self.accrual_on_default {
premium_accrual_unit += accrual_unit;
}
protection_unit += protection_period;
}
let premium_leg_coupons_npv = self.notional * self.spread * premium_coupons_unit;
let premium_leg_accrual_npv = self.notional * self.spread * premium_accrual_unit;
let premium_leg_npv = premium_leg_coupons_npv + premium_leg_accrual_npv;
let protection_leg_npv = self.notional * (T::one() - self.recovery_rate) * protection_unit;
let risky_annuity = self.notional * (premium_coupons_unit + premium_accrual_unit);
let fair_spread = if risky_annuity.abs() <= T::min_positive_val() {
T::zero()
} else {
protection_leg_npv / risky_annuity
};
let net_npv = match self.direction {
CdsPosition::Buyer => protection_leg_npv - premium_leg_npv,
CdsPosition::Seller => premium_leg_npv - protection_leg_npv,
};
CdsValuation {
protection_leg_npv,
premium_leg_coupons_npv,
premium_leg_accrual_npv,
premium_leg_npv,
risky_annuity,
fair_spread,
net_npv,
direction: self.direction,
}
}
pub fn npv(
&self,
valuation_date: NaiveDate,
discount_day_count: DayCountConvention,
discount: &(impl CurveProvider<T> + ?Sized),
survival: &SurvivalCurve<T>,
) -> T {
self
.valuation(valuation_date, discount_day_count, discount, survival)
.net_npv
}
pub fn fair_spread(
&self,
valuation_date: NaiveDate,
discount_day_count: DayCountConvention,
discount: &(impl CurveProvider<T> + ?Sized),
survival: &SurvivalCurve<T>,
) -> T {
self
.valuation(valuation_date, discount_day_count, discount, survival)
.fair_spread
}
pub fn risky_annuity(
&self,
valuation_date: NaiveDate,
discount_day_count: DayCountConvention,
discount: &(impl CurveProvider<T> + ?Sized),
survival: &SurvivalCurve<T>,
) -> T {
self
.valuation(valuation_date, discount_day_count, discount, survival)
.risky_annuity
}
fn integrate_period(
&self,
valuation_date: NaiveDate,
discount_day_count: DayCountConvention,
period_start: NaiveDate,
period_end: NaiveDate,
discount: &crate::curves::DiscountCurve<T>,
survival: &SurvivalCurve<T>,
) -> (T, T) {
if period_end <= period_start {
return (T::zero(), T::zero());
}
let total_days = (period_end - period_start).num_days();
if total_days <= 0 {
return (T::zero(), T::zero());
}
let step = self.integration_step_days.min(total_days).max(1);
let mut cursor = period_start;
let mut df_prev = discount.discount_factor(tau_year_fraction(
discount_day_count,
valuation_date,
cursor,
));
let mut q_prev = survival.survival_probability(tau_year_fraction(
DayCountConvention::Actual365Fixed,
valuation_date,
cursor,
));
let mut accrual_unit = T::zero();
let mut protection_unit = T::zero();
let half = T::from_f64_fast(0.5);
while cursor < period_end {
let delta_days = step.min((period_end - cursor).num_days());
let next = cursor + chrono::Duration::days(delta_days);
let df_next =
discount.discount_factor(tau_year_fraction(discount_day_count, valuation_date, next));
let q_next = survival.survival_probability(tau_year_fraction(
DayCountConvention::Actual365Fixed,
valuation_date,
next,
));
let df_mid = half * (df_prev + df_next);
let dq = q_prev - q_next;
let alpha_to_cursor: T = self.day_count.year_fraction(period_start, cursor);
let alpha_slice: T = self.day_count.year_fraction(cursor, next);
let accrual_factor_mid = alpha_to_cursor + half * alpha_slice;
accrual_unit += df_mid * dq * accrual_factor_mid;
protection_unit += df_mid * dq;
cursor = next;
df_prev = df_next;
q_prev = q_next;
}
(accrual_unit, protection_unit)
}
}
fn tau_year_fraction<T: FloatExt>(
dcc: DayCountConvention,
valuation_date: NaiveDate,
target: NaiveDate,
) -> T {
if target <= valuation_date {
T::zero()
} else {
dcc.year_fraction(valuation_date, target)
}
}