Mark a CDS curve from par spreads
You'll turn a name's quoted par spreads into a survival curve, pin it in a market, and price a trade on it, checking that every quote prices back to its spread.
Before you start
- helixio installed: see installing.
- The name's par spreads by tenor, and the recovery they assume (40% is the usual senior unsecured assumption).
- A discount curve in the trade's currency. This guide solves one from five SOFR swap rates; if your market already holds one, start at step 2.
Steps
- Solve the discount curve and file it in a market with
with_discount. - Book one standard CDS per quote with
products.credit_default_swap, struck at its par spread, and pair each with its maturity and its spread. - Solve the survival curve with
calibration.survival, at the recovery the quotes assume, and pin it under the name's identity withwith_credit_curve. - Price your trade on the market, and check that every quote prices back to its spread.
The comments in the program match these steps.
Rust
use chrono::NaiveDate;
use helixrs::{
AnyInstrument, CollateralSpec, CreditCurveId, CreditDefaultSwap, Currency, DayCountFraction,
FixingStore, FxRates, Interpolation, MarketContext, Period, Rung, Survival, Tolerance, calibrate,
calibrate_survival, cds_standard, par_rungs,
};
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 RECOVERY: f64 = 0.4;
fn main() -> Outcome<()> {
let close = date((2026, 10, 1));
let usd: Currency = "USD".parse()?;
let acme: CreditCurveId = "ACME/SeniorUnSec/XR14/USD".parse()?;
let (fixings, fx) = (FixingStore::new(), FxRates::default());
// Step 1: a SOFR discount curve from five swap quotes, filed in a market.
let tenors = ["1Y", "2Y", "5Y", "10Y", "30Y"].iter().map(|t| t.parse::<Period>()).collect::<Result<Vec<_>, _>>()?;
let rates = [0.0438, 0.0447, 0.0462, 0.0475, 0.0470];
let swaps = par_rungs("USD-SOFR-OIS", close, &tenors, &rates, 1.0)?;
let sofr = calibrate(close, &swaps, &fixings, &fx, Tolerance::default(), Interpolation::LogLinear)?;
let market = MarketContext::as_of(close)
.with_discount(CollateralSpec::Currency(usd), sofr.clone())
.with_projection("USD-SOFR", sofr);
// Step 2: one standard CDS per par-spread quote, struck at its spread.
let template = cds_standard();
let mut ladder = Vec::new();
for (years, spread) in [(3, 0.0070), (5, 0.0095), (7, 0.0110), (10, 0.0125)] {
let cds = AnyInstrument::Cds(CreditDefaultSwap::standard(&template, close, "ACME", usd, years, 1.0, spread, true));
let maturity = cds.contract_dates()[1].1;
ladder.push(Rung::par(maturity, cds, spread));
}
// Step 3: solve the survival curve and pin it under the name's identity, with
// the recovery the quotes assume.
let (discount, _) = market.curve(&"discount(USD)".parse()?).expect("the USD curve");
let solved = calibrate_survival(
close, &discount, &ladder, Some(RECOVERY), &fixings, &fx, Tolerance::default(), DayCountFraction::Act365Fixed,
)?;
let market = market.with_credit_curve(acme, Survival::Isda(solved.curve), Some(RECOVERY));
// Step 4: price your trade, five years at the standard 100bp coupon, and check
// that every quote prices back to its spread.
let trade = AnyInstrument::Cds(CreditDefaultSwap::standard(&template, close, "ACME", usd, 5, 10_000_000.0, 0.0100, true));
let npv = trade.npv(&market)?;
let par = trade.par_spread(&market)?;
let mut worst: f64 = 0.0;
for rung in &ladder {
worst = worst.max((rung.instrument.par_spread(&market)? - rung.quote).abs());
}
println!("npv {npv:.2}");
println!("par spread {par:.6}");
println!("largest quote miss {:.6} bp", worst * 1e4);
Ok(())
}Python
import datetime as dt
import helixio as hlx
CLOSE = dt.date(2026, 10, 1)
ACME = "ACME/SeniorUnSec/XR14/USD"
RECOVERY = 0.4
# Step 1: a SOFR discount curve from five swap quotes, filed in a market.
swaps = [("1Y", 0.0438), ("2Y", 0.0447), ("5Y", 0.0462), ("10Y", 0.0475), ("30Y", 0.0470)]
sofr = hlx.calibration.curve(CLOSE, list(hlx.calibration.par_rungs("USD-SOFR-OIS", CLOSE, swaps))).curve()
market = hlx.MarketContext(CLOSE).with_discount("USD", sofr).with_projection("USD-SOFR", sofr)
# Step 2: one standard CDS per par-spread quote, struck at its spread.
quotes = [(3, 0.0070), (5, 0.0095), (7, 0.0110), (10, 0.0125)]
ladder = []
for years, spread in quotes:
cds = hlx.products.credit_default_swap(CLOSE, "ACME", "USD", years, notional=1.0, coupon=spread)
ladder.append((dict(cds.contract_dates())["maturity"], cds, spread))
# Step 3: solve the survival curve and pin it under the name's identity, with
# the recovery the quotes assume.
solved = hlx.calibration.survival(CLOSE, market.curve("discount(USD)"), ladder, recovery=RECOVERY)
market = market.with_credit_curve(ACME, solved.curve(), recovery=RECOVERY)
# Step 4: price your trade, five years at the standard 100bp coupon, and check
# that every quote prices back to its spread.
trade = hlx.products.credit_default_swap(CLOSE, "ACME", "USD", 5, notional=10_000_000.0, coupon=0.0100)
npv = trade.npv(market)
par = trade.par_spread(market)
worst = max(abs(cds.par_spread(market) - spread) for _, cds, spread in ladder)
print(f"npv {npv:.2f}")
print(f"par spread {par:.6f}")
print(f"largest quote miss {worst * 1e4:.6f} bp")npv -22671.65
par spread 0.009500
largest quote miss 0.000000 bp
Check it worked
- Every quote prices back to its spread; the largest miss is zero to the printed precision.
- A trade on a quoted tenor has that tenor's quote as its par spread. Here the five-year trade pays a 100bp coupon for protection the market prices at 95bp, so it has a negative value to the buyer.
- If the market refuses the trade, check that the curve is pinned under the identity the trade
reads: a bare
"ACME"readsACME/SeniorUnSec/XR14/USDin dollars.
Related
- Credit calibration, the reference for the solve and its two forms.
- Credit default swap, the reference for the contract.
- How credit products work, for why the recovery is pinned with the curve.
From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.