pub struct CdsContract {
pub notional: f64,
pub spread_bps: f64,
pub maturity_years: f64,
pub recovery_rate: f64,
pub payment_freq: u32,
}
impl CdsContract {
pub fn new(notional: f64, spread_bps: f64, maturity: f64, recovery: f64) -> Self {
CdsContract { notional, spread_bps, maturity_years: maturity, recovery_rate: recovery, payment_freq: 4 }
}
fn discount(t: f64, risk_free_rate: f64) -> f64 {
(-risk_free_rate * t).exp()
}
fn survival_prob(t: f64, hazard_rate: f64) -> f64 {
(-hazard_rate * t).exp()
}
pub fn protection_leg_pv(&self, hazard_rate: f64, risk_free_rate: f64) -> f64 {
let n_steps = (self.maturity_years * 365.0) as u32;
let dt = self.maturity_years / n_steps as f64;
let mut pv = 0.0;
let mut q_prev = 1.0f64;
for i in 1..=n_steps {
let t = i as f64 * dt;
let q = Self::survival_prob(t, hazard_rate);
let d = Self::discount(t, risk_free_rate);
pv += (1.0 - self.recovery_rate) * (q_prev - q) * d;
q_prev = q;
}
self.notional * pv
}
pub fn premium_leg_pv(&self, hazard_rate: f64, risk_free_rate: f64) -> f64 {
let spread = self.spread_bps / 10000.0;
let dt = 1.0 / self.payment_freq as f64;
let n_periods = (self.maturity_years * self.payment_freq as f64) as u32;
let mut pv = 0.0;
for i in 1..=n_periods {
let t = i as f64 * dt;
let q = Self::survival_prob(t, hazard_rate);
let d = Self::discount(t, risk_free_rate);
pv += spread * dt * q * d;
}
self.notional * pv
}
pub fn par_spread(&self, hazard_rate: f64, risk_free_rate: f64) -> f64 {
let protection = self.protection_leg_pv(hazard_rate, risk_free_rate);
let dt = 1.0 / self.payment_freq as f64;
let n_periods = (self.maturity_years * self.payment_freq as f64) as u32;
let mut risky_annuity = 0.0f64;
for i in 1..=n_periods {
let t = i as f64 * dt;
risky_annuity += dt * Self::survival_prob(t, hazard_rate) * Self::discount(t, risk_free_rate);
}
if risky_annuity < 1e-10 { return 0.0; }
(protection / (self.notional * risky_annuity)) * 10000.0
}
pub fn mtm(&self, hazard_rate: f64, risk_free_rate: f64) -> f64 {
self.protection_leg_pv(hazard_rate, risk_free_rate)
- self.premium_leg_pv(hazard_rate, risk_free_rate)
}
pub fn implied_hazard_rate(&self, market_spread_bps: f64, risk_free_rate: f64) -> f64 {
let target_spread = market_spread_bps;
let mut lo = 0.0001f64;
let mut hi = 0.5f64;
for _ in 0..50 {
let mid = (lo + hi) / 2.0;
let mut cds = CdsContract::new(self.notional, 0.0, self.maturity_years, self.recovery_rate);
cds.payment_freq = self.payment_freq;
let spread = cds.par_spread(mid, risk_free_rate);
if spread < target_spread { lo = mid; } else { hi = mid; }
}
(lo + hi) / 2.0
}
pub fn cs01(&self, hazard_rate: f64, risk_free_rate: f64) -> f64 {
let v_up = {
let mut cds = CdsContract::new(self.notional, self.spread_bps, self.maturity_years, self.recovery_rate);
cds.payment_freq = self.payment_freq;
let h_up = cds.implied_hazard_rate(self.spread_bps * 10000.0 / 10000.0 + 1.0, risk_free_rate);
cds.mtm(h_up, risk_free_rate)
};
let v_down = {
let mut cds = CdsContract::new(self.notional, self.spread_bps, self.maturity_years, self.recovery_rate);
cds.payment_freq = self.payment_freq;
let h_down = cds.implied_hazard_rate((self.spread_bps * 10000.0 / 10000.0 - 1.0).max(0.1), risk_free_rate);
cds.mtm(h_down, risk_free_rate)
};
(v_up - v_down) / 2.0
}
}