Credit default swaps
Module: Shoals.Cds.
This module values a single-name credit default swap from a piecewise-constant
hazard curve, and bootstraps that curve from par spreads. Times are elapsed
years from the valuation date as f32, not calendar dates; derive them with
Shoals.Date.year_fraction under the day count the contract uses. Spreads,
hazards, rates, and recovery are decimals (0.01 is 100 basis points), and
every value is per unit of notional.
Hazard curves
Section titled “Hazard curves”def hazard_curve_from_pillars[n](times: tensor[n, f32], hazards: tensor[n, f32]) -> HazardCurve[n]def hazard_curve_pillars[n](curve: HazardCurve[n]) -> (tensor[n, f32], tensor[n, f32])def cds_survival_from_hazards[n](curve: HazardCurve[n], t: f32) -> f32hazards[i] is the constant default intensity on the interval ending at
times[i]: the first applies from time 0 to times[0], and the last
continues past the final pillar. cds_survival_from_hazards(curve, t) is
exp(-integral of the hazard from 0 to t).
hazard_curve_from_pillars requires strictly increasing times. It fails at
the first duplicate or out-of-order pair, naming its index and both values.
It does not reorder pillars, and it does not check the hazards: a negative
hazard gives a survival probability above one. Pillar times should be
positive; a pillar at or before 0 covers no time. Survival at any t <= 0
is 1. With no pillars the curve has
zero hazard and every survival probability is 1. HazardCurve is opaque, so
the only ways to build one are this constructor and cds_bootstrap_hazards;
read it back with hazard_curve_pillars.
curve = hazard_curve_from_pillars( to_tensor([cast(1.0, f32), cast(3.0, f32), cast(5.0, f32)]), to_tensor([cast(0.01, f32), cast(0.02, f32), cast(0.03, f32)]))q4 = cds_survival_from_hazards(curve, cast(4.0, f32)) -- 0.9231163 = exp(-(0.01 + 0.02 * 2 + 0.03))q7 = cds_survival_from_hazards(curve, cast(7.0, f32)) -- 0.8436648Valuation
Section titled “Valuation”def cds_premium_leg_value[n](spread: f32, t_maturity: f32, n_premiums_per_year: i64, hazards: HazardCurve[n], r: f32) -> f32def cds_protection_leg_value[n](t_maturity: f32, recovery: f32, hazards: HazardCurve[n], r: f32) -> f32def cds_pv[n](spread: f32, t_maturity: f32, n_premiums_per_year: i64, recovery: f32, hazards: HazardCurve[n], r: f32) -> f32Discounting uses exp(-r * t) with one continuously compounded rate r.
- The premium leg pays on the grid
k / n_premiums_per_yearfork = 1 .. trunc(t_maturity * n_premiums_per_year). Each payment isspread * dt * DF(t_k) * Q(t_k), wheredtis the time since the previous payment. There is no accrued premium on default, and a maturity that is not a whole number of periods drops the final stub. - The protection leg uses a monthly grid to
t_maturityand sums(1 - recovery) * DF(midpoint) * (Q(t_{k-1}) - Q(t_k)). cds_pvis protection minus premium: the value to the protection buyer.
The premium leg and cds_pv require n_premiums_per_year >= 1 and fail
with a diagnostic naming the received frequency otherwise. Supply
t_maturity > 0 and 0 <= recovery <= 1; these conditions are not
checked. A recovery above 1 makes the protection leg negative.
prem = cds_premium_leg_value(cast(0.01, f32), cast(5.0, f32), cast(4, i64), curve, cast(0.03, f32)) -- 0.044187766prot = cds_protection_leg_value(cast(5.0, f32), cast(0.4, f32), curve, cast(0.03, f32)) -- 0.057317026pv = cds_pv(cast(0.01, f32), cast(5.0, f32), cast(4, i64), cast(0.4, f32), curve, cast(0.03, f32)) -- 0.01312926Bootstrapping from par spreads
Section titled “Bootstrapping from par spreads”def cds_bootstrap_hazards[n](spreads: tensor[n, f32], tenors: tensor[n, f32], recovery: f32, r: f32, n_premiums_per_year: i64) -> HazardCurve[n]cds_bootstrap_hazards takes par spreads (positive decimals per year) at
strictly increasing positive tenors, with 0 <= recovery < 1 and
n_premiums_per_year >= 1, and
solves one hazard per tenor, in order, holding the earlier hazards fixed, so
that cds_pv at that tenor is zero. It checks tenor ordering before solving
any pillar and fails the same way as hazard_curve_from_pillars, naming
Shoals.Cds.cds_bootstrap_hazards. A nonempty bootstrap also checks the
payment frequency when it values each pillar. Empty spread and tenor
tensors produce an empty curve without reading the frequency.
Each pillar is solved by Brent's method over hazards in [1e-6, 2.0], to a
tolerance of 1e-6 within 100 iterations. A spread whose hazard falls
outside that bracket does not fail: its hazard comes back NaN, and so does
every later pillar that depends on it. Spreads [0.006, 5.0] at tenors
[1, 3] return hazards [0.009949725, NaN]. Check the result for NaN
before using the curve.
boot = cds_bootstrap_hazards( to_tensor([cast(0.006, f32), cast(0.01, f32), cast(0.012, f32)]), to_tensor([cast(1.0, f32), cast(3.0, f32), cast(5.0, f32)]), cast(0.4, f32), cast(0.03, f32), cast(4, i64))-- hazards [0.009949725, 0.0201197, 0.025517497]The module generates no calendar payment schedule, no IMM dates, and no upfront fee; supply those terms through the times and spread you pass.