Esc

    The API is under testing and may change before release.

    Nth-to-default

    This page lists everything you need to book and value an nth-to-default basket: the function's arguments and defaults, the correlation it reads, how it's priced, and an example you can run. An nth-to-default swap buys protection on a small basket that pays only when the n-th name defaults. Why its spread sits where it does 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 three names on flat hazards of 2%, 1.5% and 1%, and a basket correlation of 30% under the key TRIO. It reads each name's single-name par spread, then the first-to-default and second-to-default spreads on the basket.

    Rust
    use chrono::NaiveDate;
    use helixio_docs_sheets::{SOFR_CLOSE_DATE, USD_SOFR_CLOSE};
    use helixrs::{
        AnyInstrument, CollateralSpec, CreditCurveId, CreditDefaultSwap, Currency, DayCountFraction,
        FlatHazard, MarketContext, NthToDefaultSwap, 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 NAMES: [(&str, f64); 3] = [("ACME", 0.020), ("BETA", 0.015), ("GAMMA", 0.010)];
    
    fn main() -> Outcome<()> {
        let close = date(SOFR_CLOSE_DATE);
        let usd: Currency = "USD".parse()?;
        let template = cds_standard();
        // A bare name reads on the template's standard tier and clause.
        let identity = |name: &str| -> Outcome<CreditCurveId> { Ok(template.reference(name)?.in_currency(usd)) };
    
        // Each name's survival curve under its identity, and the basket's correlation
        // under a key the contract reads, so it carries a risk row of its own.
        let sheet = QuoteSheet::from_rows(USD_SOFR_CLOSE.iter().copied())?;
        let mut market = MarketContext::as_of(close).with_recipe_quotes(&CollateralSpec::Currency(usd), &sheet)?;
        for (name, hazard) in NAMES {
            market = market.with_credit_curve(
                identity(name)?,
                Survival::FlatHazard(FlatHazard::new(close, hazard, DayCountFraction::Act365Fixed)?),
                Some(0.4),
            );
        }
        let market = market.with_basket_correlation("TRIO", 0.30)?;
        let mut singles = Vec::new();
        for (name, _) in NAMES {
            let cds = CreditDefaultSwap::standard(&template, close, name, usd, 5, 1.0, 0.01, true);
            singles.push(AnyInstrument::Cds(cds).par_spread(&market)?);
        }
        for ((name, _), spread) in NAMES.iter().zip(&singles) {
            println!("{name:<6} single  {spread:.6}");
        }
    
        let basket = |n: usize| -> Outcome<f64> {
            let entities = NAMES.iter().map(|(name, _)| identity(name)).collect::<Outcome<Vec<_>>>()?;
            let swap = NthToDefaultSwap::standard(&template, close, entities, n, usd, 5, 10_000_000.0, 0.0300, 0.0, true)?
                .on_market_correlation("TRIO");
            Ok(AnyInstrument::NthToDefault(Box::new(swap)).par_spread(&market)?)
        };
        let first = basket(1)?;
        let second = basket(2)?;
        println!("first to default   {first:.6}");
        println!("second to default  {second:.6}");
    
        Ok(())
    }
    Python
    import helixio as hlx
    from desk_sheets import SOFR_CLOSE_DATE, USD_SOFR_CLOSE
    
    NAMES = {"ACME": 0.020, "BETA": 0.015, "GAMMA": 0.010}
    
    # Each name's survival curve under its identity, and the basket's correlation
    # under a key the contract reads, so it carries a risk row of its own.
    market = hlx.MarketContext(SOFR_CLOSE_DATE).with_recipe_quotes("USD", USD_SOFR_CLOSE)
    for name, hazard in NAMES.items():
        market = market.with_credit_curve(
            f"{name}/SeniorUnSec/XR14/USD",
            hlx.curves.SurvivalCurve.flat_hazard(SOFR_CLOSE_DATE, hazard),
            recovery=0.4,
        )
    market = market.with_basket_correlation("TRIO", 0.30)
    singles = [
        hlx.products.credit_default_swap(SOFR_CLOSE_DATE, name, "USD", 5, notional=1.0, coupon=0.01).par_spread(market)
        for name in NAMES
    ]
    for name, spread in zip(NAMES, singles):
        print(f"{name:<6} single  {spread:.6f}")
    
    def basket(n):
        return hlx.products.nth_to_default_swap(
            SOFR_CLOSE_DATE, list(NAMES), n, "USD", 5,
            notional=10_000_000.0, spread=0.0300, correlation_key="TRIO",
        )
    
    first = basket(1).par_spread(market)
    second = basket(2).par_spread(market)
    print(f"first to default   {first:.6f}")
    print(f"second to default  {second:.6f}")
    ACME   single  0.011907
    BETA   single  0.008930
    GAMMA  single  0.005953
    first to default   0.024016
    second to default  0.003617

    The first-to-default spread lies between the widest single name's and the sum of all three, and the second-to-default is far cheaper.

    How it's priced

    Under the one-factor copula of the CDO tranche, each name ii defaults by tt with probability pi(t)p_i(t), and the names are independent given the common factor MM. The probability that fewer than nn have defaulted by tt,

    Pn(t)=EM[ Pr⁡(#{i:τi≤t}<n∣M)],P_n(t) = \mathbb{E}_M\Bigl[\,\Pr\bigl(\#\{i : \tau_i \le t\} < n \mid M\bigr)\Bigr],

    is the basket's survival curve, and the swap's legs are those of a CDS on it.

    API

    Argument Type Default Meaning
    trade_date date required the trade date
    entities list[str] required the names, each a bare entity on the standard tier and clause or a full identity
    n int required which default pays: 1 for first-to-default
    currency str required completes each bare entity's curve identity
    tenor_years int required whole years of protection
    notional float required the protected amount
    spread float required the running spread the buyer pays
    correlation float None a pairwise correlation fixed in the contract, with no risk row
    correlation_key str None the market's basket correlation under that key, with a risk row
    buys_protection bool True False books the seller
    fixed_recovery float None a recovery the contract fixes; None reads each name's
    factor_dof, idiosyncratic_dof float None the double-t copula's degrees of freedom; give both or neither
    copula_dof_key str None a credit index id whose marked degrees of freedom the double-t reads

    A basket correlation is pinned with MarketContext.with_basket_correlation(key, correlation). In Rust the basket is NthToDefaultSwap::standard, or on_market_correlation to read the market's.

    Symbol Meaning Rust Python
    the names' curves each under its identity MarketContext::with_credit_curve MarketContext.with_credit_curve
    ρ\rho in the market basket correlation under a key MarketContext::with_basket_correlation MarketContext.with_basket_correlation
    the basket nth-to-default on nn NthToDefaultSwap::standard products.nth_to_default_swap
    Pn(t)P_n(t) fewer than nn defaults basket_survival_probability credit.basket_survival_probability
    par spread legs equal AnyInstrument::par_spread Instrument.par_spread

    Defaults and limits

    • Correlation: give exactly one of correlation and correlation_key. A correlation in the market carries a risk row of its own; one fixed in the contract doesn't.
    • The names' curves: the market must hold a survival curve for every name in the basket.
    • Terms: the basket follows standard CDS terms; see the credit conventions catalog.

    See also

    • Source: crates/helixrs/src/instruments/credit.rs, crates/helixrs/src/models/credit.rs
    • Related pages:
      • How credit products work, the ideas behind the bounds.
      • Credit default swap, for the single names.
      • CDO tranche, for the same copula on an index pool.

    From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.