Credit default swap
This page lists everything you need to book and value a single-name credit default swap: the function's arguments and defaults, how it's priced, and examples you can run. A CDS transfers one issuer's default risk: the protection buyer pays a running coupon, and if the issuer defaults, the seller pays the loss on the notional. What the contract is and how it's quoted is covered in how credit products work.
Quick example
The example reads its rates from the desk sheets; load them once and it runs as shown.
This pins a 2% flat hazard for ACME on the SOFR close, books five years of protection at the standard 100bp coupon, values it, reads its par spread, and strikes a second contract at par.
use chrono::{Datelike, NaiveDate};
use helixio_docs_sheets::{SOFR_CLOSE_DATE, USD_SOFR_CLOSE};
use helixrs::{
AnyInstrument, CollateralSpec, CreditCurveId, CreditDefaultSwap, Currency, DayCountFraction,
FlatHazard, MarketContext, QuoteSheet, Survival, cds_standard,
};
type Outcome<T> = Result<T, Box<dyn std::error::Error>>;
fn date((year, month, day): (i32, u32, u32)) -> NaiveDate {
NaiveDate::from_ymd_opt(year, month, day).expect("a real calendar date")
}
const ACME: &str = "ACME/SeniorUnSec/XR14/USD";
const HAZARD: f64 = 0.02;
const RECOVERY: f64 = 0.4;
/// A standard CDS on ACME: the standard schedule, and the credit under ACME's
/// identity on the template's standard tier and clause.
fn cds(trade: NaiveDate, usd: Currency, notional: f64, coupon: f64) -> AnyInstrument {
AnyInstrument::Cds(CreditDefaultSwap::standard(&cds_standard(), trade, "ACME", usd, 5, notional, coupon, true))
}
fn main() -> Outcome<()> {
let close = date(SOFR_CLOSE_DATE);
let usd: Currency = "USD".parse()?;
// Rates from the desk's sheet; the name's survival curve joins the same market
// under its credit identity — entity, seniority, documentation clause, currency
// — with the recovery its quotes assume.
let sheet = QuoteSheet::from_rows(USD_SOFR_CLOSE.iter().copied())?;
let acme: CreditCurveId = ACME.parse()?;
let survival = Survival::FlatHazard(FlatHazard::new(close, HAZARD, DayCountFraction::Act365Fixed)?);
let market = MarketContext::as_of(close)
.with_recipe_quotes(&CollateralSpec::Currency(usd), &sheet)?
.with_credit_curve(acme.clone(), survival, Some(RECOVERY));
let (_, recovery) = market.credit_curve(&acme)?;
let recovery = recovery.expect("pinned with its recovery");
println!("credit {ACME}");
println!("recovery {recovery:.2}");
// Five years of protection on the standard contract: the 100bp coupon, and a
// maturity rolled to the next 20 December.
let swap = cds(close, usd, 10_000_000.0, 0.0100);
let dates = swap.contract_dates();
println!("effective {}", dates[0].1);
println!("maturity {}", dates[1].1);
// The par spread is the coupon at which the legs balance. On a flat hazard it
// sits near hazard × (1 − recovery); the buyer pays 100bp for protection worth
// more, so the contract is worth something to the buyer.
let npv = swap.npv(&market)?;
let par = swap.par_spread(&market)?;
println!("npv {npv:.2}");
println!("par spread {par:.6}");
println!("hazard × (1 − recovery) {:.6}", HAZARD * (1.0 - RECOVERY));
// Struck at its par spread, the protection and the premium are equal.
let fair = cds(close, usd, 10_000_000.0, par);
println!("at par {:.2}", fair.npv(&market)?.abs());
Ok(())
}import helixio as hlx
from desk_sheets import SOFR_CLOSE_DATE, USD_SOFR_CLOSE
ACME = "ACME/SeniorUnSec/XR14/USD"
HAZARD, RECOVERY = 0.02, 0.4
# Rates from the desk's sheet; the name's survival curve joins the same market
# under its credit identity — entity, seniority, documentation clause, currency
# — with the recovery its quotes assume.
survival = hlx.curves.SurvivalCurve.flat_hazard(SOFR_CLOSE_DATE, HAZARD)
market = (
hlx.MarketContext(SOFR_CLOSE_DATE)
.with_recipe_quotes("USD", USD_SOFR_CLOSE)
.with_credit_curve(ACME, survival, recovery=RECOVERY)
)
_, recovery = market.credit_curve(ACME)
print(f"credit {ACME}")
print(f"recovery {recovery:.2f}")
# Five years of protection on the standard contract: the 100bp coupon, and a
# maturity rolled to the next 20 December.
cds = hlx.products.credit_default_swap(
SOFR_CLOSE_DATE, "ACME", "USD", 5, notional=10_000_000.0, coupon=0.0100
)
dates = dict(cds.contract_dates())
print(f"effective {dates['effective']}")
print(f"maturity {dates['maturity']}")
# The par spread is the coupon at which the legs balance. On a flat hazard it
# sits near hazard × (1 − recovery); the buyer pays 100bp for protection worth
# more, so the contract is worth something to the buyer.
npv = cds.npv(market)
par = cds.par_spread(market)
print(f"npv {npv:.2f}")
print(f"par spread {par:.6f}")
print(f"hazard × (1 − recovery) {HAZARD * (1.0 - RECOVERY):.6f}")
# Struck at its par spread, the protection and the premium are equal.
fair = hlx.products.credit_default_swap(
SOFR_CLOSE_DATE, "ACME", "USD", 5, notional=10_000_000.0, coupon=par
)
print(f"at par {abs(fair.npv(market)):.2f}")credit ACME/SeniorUnSec/XR14/USD
recovery 0.40
effective 2026-10-02
maturity 2031-12-20
npv 84862.35
par spread 0.011907
hazard × (1 − recovery) 0.012000
at par 0.00
The buyer pays 100bp for protection worth about 119bp, so the contract is worth something to the buyer. The par spread sits close to the hazard times the loss on default, and the contract struck at its par spread is worth nothing.
How it's priced
Write for the survival probability, for the recovery, for the discount factor and for the running coupon. Over the schedule with accrual fractions , the two legs per unit notional are
The buyer's value is protection minus premium. The par spread is the coupon that sets it to zero. With a flat hazard , so that , it sits close to .
API
| Argument | Type | Default | Meaning |
|---|---|---|---|
trade_date |
date |
required | the trade date; the contract's dates roll off it |
reference |
str |
required | a bare entity ("ACME") on the template's standard tier and clause, or a full ENTITY/SENIORITY/DOCCLAUSE spelling ("ACME/SubTier2/XR14") |
currency |
str |
required | completes the identity of the curve the contract values on |
tenor_years |
int |
None |
whole years of protection, maturity rolled to the next 20 June or 20 December; give this or maturity |
maturity |
date |
None |
a stated maturity, for an off-cycle contract; give this or tenor_years |
notional |
float |
required | the protected amount |
coupon |
float |
required | the running coupon, as a decimal (0.01 for 100bp) |
buys_protection |
bool |
True |
False books the seller |
upfront |
float |
None |
the upfront the buyer pays at settlement, as a decimal of the notional |
fixed_recovery |
float |
None |
a recovery the contract fixes; None reads the market's |
accrual_on_default |
AccrualOnDefault or str |
"proportional" |
the premium accrued on default: "proportional", "halfway" or "none" |
protection_start |
date |
None |
a forward start: protection from that date, settlement unchanged |
In Rust, CreditDefaultSwap::standard(&cds_standard(), trade, reference, currency, years, notional, coupon, buys_protection) books the same contract, standard_on takes a stated identity and
maturity, and with_protection_start sets a forward start.
| Symbol | Meaning | Rust | Python |
|---|---|---|---|
| survival, flat hazard | Survival::FlatHazard(FlatHazard::new(..)) |
curves.SurvivalCurve.flat_hazard |
|
| pinned | the curve under its identity, with recovery | MarketContext::with_credit_curve(id, curve, Some(r)) |
MarketContext.with_credit_curve(identity, curve, recovery=r) |
| the contract | standard CDS | CreditDefaultSwap::standard |
products.credit_default_swap |
| protection − premium | the buyer's value | AnyInstrument::npv |
Instrument.npv |
| par spread | AnyInstrument::par_spread |
Instrument.par_spread |
|
| the legs | protection, premium, accrual, annuity | CreditDefaultSwap::diagnostics |
Instrument.diagnostics |
Defaults and limits
- The template: every contract books on the standard template: quarterly coupons on the 20th of March, June, September and December, and maturities rolled to the 20th of June or December. The terms are in the credit conventions catalog.
- The curve: a market must hold a survival curve under the contract's full identity, such as
ACME/SeniorUnSec/XR14/USD. A contract on another tier reads another curve, and a market without it refuses the valuation, naming the key it lacks. - Maturity: give
tenor_yearsormaturity, not both; giving both or neither raisesValueError. - Forward start: a
protection_startpast the last coupon raisesValueError. - A weekend maturity: the final coupon is paid on the next business day, and weighted by survival to the day before it is paid.
More examples
The same contract on a subordinated tier, under each rule for the accrual on default, and with a forward start. The subordinated contract is refused until its own curve is pinned; with the same hazard and a lower recovery, its par spread is wider. Paying no accrual is worth about eleven hundred more to the buyer on ten million, and halfway lands within twenty dollars of the standard.
use chrono::NaiveDate;
use helixio_docs_sheets::{SOFR_CLOSE_DATE, USD_SOFR_CLOSE};
use helixrs::{
AccrualOnDefault, AnyInstrument, CollateralSpec, CreditCurveId, CreditDefaultSwap, Currency,
DayCountFraction, FlatHazard, MarketContext, QuoteSheet, Survival, cds_standard,
};
type Outcome<T> = Result<T, Box<dyn std::error::Error>>;
fn date((year, month, day): (i32, u32, u32)) -> NaiveDate {
NaiveDate::from_ymd_opt(year, month, day).expect("a real calendar date")
}
const HAZARD: f64 = 0.02;
fn main() -> Outcome<()> {
let close = date(SOFR_CLOSE_DATE);
let usd: Currency = "USD".parse()?;
let flat = || -> Outcome<Survival> {
Ok(Survival::FlatHazard(FlatHazard::new(close, HAZARD, DayCountFraction::Act365Fixed)?))
};
let market = MarketContext::as_of(close)
.with_recipe_quotes(&CollateralSpec::Currency(usd), &QuoteSheet::from_rows(USD_SOFR_CLOSE.iter().copied())?)?
.with_credit_curve("ACME/SeniorUnSec/XR14/USD".parse::<CreditCurveId>()?, flat()?, Some(0.4));
let template = cds_standard();
let contract = || CreditDefaultSwap::standard(&template, close, "ACME", usd, 5, 10_000_000.0, 0.0100, true);
// A subordinated contract on the same issuer names its tier; the template's
// `reference` reads the full spelling as given. It reads its own curve, so a
// market holding only the senior curve refuses it, naming the key it lacks.
let senior_par = AnyInstrument::Cds(contract()).par_spread(&market)?;
let sub: CreditCurveId = template.reference("ACME/SubTier2/XR14")?.in_currency(usd);
let maturity = AnyInstrument::Cds(contract()).contract_dates()[1].1;
let junior = AnyInstrument::Cds(CreditDefaultSwap::standard_on(
&template, close, sub.clone(), usd, maturity, 10_000_000.0, 0.0100, true,
));
let missing = match junior.npv(&market) {
Ok(_) => "nothing".to_string(),
Err(refused) => refused.to_string(),
};
println!("refused {}", if missing.contains("ACME/SubTier2/XR14/USD") { "yes" } else { "no" });
// Pinned with the same hazard and a lower recovery, it loses more on default.
let with_sub = market.clone().with_credit_curve(sub, flat()?, Some(0.2));
let junior_par = junior.par_spread(&with_sub)?;
println!("senior par {senior_par:.6}");
println!("junior par {junior_par:.6}");
// On default the buyer owes the premium accrued since the last coupon: in
// proportion to the time accrued (the standard), half a period's worth, or
// nothing. Paying none of it, the buyer pays the least premium.
let with_accrual = |rule: AccrualOnDefault| -> Outcome<f64> {
let mut terms = contract();
terms.accrual_on_default = rule;
Ok(AnyInstrument::Cds(terms).npv(&market)?)
};
let proportional = with_accrual(AccrualOnDefault::Proportional)?;
let halfway = with_accrual("halfway".parse()?)?;
let none = with_accrual(AccrualOnDefault::None)?;
println!("proportional {proportional:.2}");
println!("halfway {halfway:.2}");
println!("none {none:.2}");
// Protection from a later date, the trade's settlement unchanged. Starting a
// year out drops a year of protection and of premium; a start past the last
// coupon is refused.
let spot_npv = AnyInstrument::Cds(contract()).npv(&market)?;
let forward_npv = AnyInstrument::Cds(contract().with_protection_start(date((2027, 10, 1)))?).npv(&market)?;
println!("spot start {spot_npv:.2}");
println!("start Oct-2027 {forward_npv:.2}");
let late_refused = contract().with_protection_start(date((2032, 6, 20))).is_err();
println!("start past the last coupon refused {}", if late_refused { "yes" } else { "no" });
Ok(())
}import datetime as dt
import helixio as hlx
from desk_sheets import SOFR_CLOSE_DATE, USD_SOFR_CLOSE
ACME = "ACME/SeniorUnSec/XR14/USD"
HAZARD = 0.02
market = (
hlx.MarketContext(SOFR_CLOSE_DATE)
.with_recipe_quotes("USD", USD_SOFR_CLOSE)
.with_credit_curve(ACME, hlx.curves.SurvivalCurve.flat_hazard(SOFR_CLOSE_DATE, HAZARD), recovery=0.4)
)
def contract(reference="ACME", **terms):
return hlx.products.credit_default_swap(
SOFR_CLOSE_DATE, reference, "USD", 5, notional=10_000_000.0, coupon=0.0100, **terms
)
# A subordinated contract on the same issuer names its tier: the full spelling
# is read as given. It reads its own curve, so a market holding only the senior
# curve refuses it, naming the key it lacks.
senior_par = contract().par_spread(market)
junior = contract("ACME/SubTier2/XR14")
try:
junior.npv(market)
missing = "nothing"
except hlx.HelixError as refused:
missing = str(refused)
print(f"refused {'yes' if 'ACME/SubTier2/XR14/USD' in missing else 'no'}")
# Pinned with the same hazard and a lower recovery, it loses more on default.
with_sub = market.with_credit_curve(
"ACME/SubTier2/XR14/USD", hlx.curves.SurvivalCurve.flat_hazard(SOFR_CLOSE_DATE, HAZARD), recovery=0.2
)
junior_par = junior.par_spread(with_sub)
print(f"senior par {senior_par:.6f}")
print(f"junior par {junior_par:.6f}")
# On default the buyer owes the premium accrued since the last coupon: in
# proportion to the time accrued (the standard), half a period's worth, or
# nothing. Paying none of it, the buyer pays the least premium.
proportional = contract(accrual_on_default=hlx.credit.AccrualOnDefault.Proportional).npv(market)
halfway = contract(accrual_on_default="halfway").npv(market)
none = contract(accrual_on_default=hlx.credit.AccrualOnDefault.NoAccrual).npv(market)
print(f"proportional {proportional:.2f}")
print(f"halfway {halfway:.2f}")
print(f"none {none:.2f}")
# Protection from a later date, the trade's settlement unchanged. Starting a
# year out drops a year of protection and of premium; a start past the last
# coupon is refused.
spot_npv = contract().npv(market)
forward_npv = contract(protection_start=dt.date(2027, 10, 1)).npv(market)
print(f"spot start {spot_npv:.2f}")
print(f"start Oct-2027 {forward_npv:.2f}")
try:
contract(protection_start=dt.date(2032, 6, 20))
late_refused = False
except ValueError:
late_refused = True
print(f"start past the last coupon refused {'yes' if late_refused else 'no'}")refused yes
senior par 0.011907
junior par 0.015875
proportional 84862.35
halfway 84879.16
none 85998.99
spot start 84862.35
start Oct-2027 66516.83
start past the last coupon refused yes
See also
- Source:
crates/helixrs/src/instruments/credit.rs,crates/helixrs/src/statics/credit_identity.rs,crates/helixrs/src/market/pricers.rs - Related pages:
- How credit products work, the ideas behind the contract.
- Survival curves, for the curve it values on.
- Credit calibration, for solving survival from a ladder of par spreads.
- CDS options, for options on this contract.
From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.