Esc

    The API is under testing and may change before release.

    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

    1. Solve the discount curve and file it in a market with with_discount.
    2. 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.
    3. Solve the survival curve with calibration.survival, at the recovery the quotes assume, and pin it under the name's identity with with_credit_curve.
    4. 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" reads ACME/SeniorUnSec/XR14/USD in dollars.
    • 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.