Esc

    The API is under testing and may change before release.

    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.

    Rust
    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(())
    }
    Python
    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 Q(t)Q(t) for the survival probability, RR for the recovery, D(t)D(t) for the discount factor and cc for the running coupon. Over the schedule t0<t1<⋯<tnt_0 < t_1 < \dots < t_n with accrual fractions αi\alpha_i, the two legs per unit notional are

    premium=c∑iαi D(ti) Q(ti)+accrual on default,protection=(1−R)∫t0tnD(t) (−dQ(t)).\text{premium} = c \sum_i \alpha_i\, D(t_i)\, Q(t_i) + \text{accrual on default}, \qquad \text{protection} = (1 - R) \int_{t_0}^{t_n} D(t)\, \bigl(-\mathrm{d}Q(t)\bigr).

    The buyer's value is protection minus premium. The par spread ss is the coupon that sets it to zero. With a flat hazard λ\lambda, so that Q(t)=e−λtQ(t) = e^{-\lambda t}, it sits close to s≈λ (1−R)s \approx \lambda\,(1 - R).

    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
    Q(t)Q(t) survival, flat hazard Survival::FlatHazard(FlatHazard::new(..)) curves.SurvivalCurve.flat_hazard
    Q,RQ, R 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
    ss 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_years or maturity, not both; giving both or neither raises ValueError.
    • Forward start: a protection_start past the last coupon raises ValueError.
    • 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.

    Rust
    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(())
    }
    Python
    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.