Esc

    The API is under testing and may change before release.

    Survival curves

    This page lists what you need to state a survival curve, the probability that an issuer hasn't defaulted by a date, in three ways: dated probabilities, one hazard rate, or a default density. It also covers how a curve is filed in a market under a full credit identity with its recovery.

    Quick example

    This states a survival curve all three ways, prints each beside its formula, and files one in a market under a credit identity with a 40% recovery. The example reads its quotes from the desk sheets; load them once and it runs as shown.

    Rust
    use std::sync::Arc;
    
    use chrono::NaiveDate;
    use helixio_docs_sheets::{SOFR_CLOSE_DATE, USD_SOFR_CLOSE};
    use helixrs::{
        CollateralSpec, CreditCurveId, DayCountFraction, DensitySurvival, FlatHazard, MarketContext,
        PiecewiseSurvival, QuoteSheet, Survival, SurvivalProbability,
    };
    
    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")
    }
    
    fn main() -> Outcome<()> {
        let reference = date(SOFR_CLOSE_DATE);
        let (three, five) = (date((2028, 10, 2)), date((2031, 10, 1)));
        let years = |d: NaiveDate| (d - reference).num_days() as f64 / 365.0;
        let act365 = DayCountFraction::Act365Fixed;
    
        // Survival of 96% to three years and 90% to five: returned exactly at the
        // pillars, and between them read from the log survival.
        let pillars = Survival::Piecewise(PiecewiseSurvival::new(reference, &[(three, 0.96), (five, 0.90)], act365)?);
        let view = pillars.view::<f64>();
        println!("survival 2030-01-02 {:.10}", view.survival_probability(date((2030, 1, 2)))?);
    
        // One hazard of 2% a year: survival exp(-h t).
        let hazard = Survival::FlatHazard(FlatHazard::new(reference, 0.02, act365)?);
        let at_five = hazard.view::<f64>().survival_probability(five)?;
        println!("survival 5Y       {at_five:.10}");
        println!("exp(-h t)         {:.10}", (-0.02 * years(five)).exp());
    
        // A default density rising from 2% a year today to 3% at five years. Linear
        // density integrates to the average of its ends times the span.
        let density = Survival::Density(DensitySurvival::new(reference, &[(reference, 0.020), (five, 0.030)], act365)?);
        let expected = 1.0 - 0.5 * (0.020 + 0.030) * years(five);
        let survived = density.view::<f64>().survival_probability(five)?;
        println!("survival 5Y       {survived:.10}");
        println!("by the density    {expected:.10}");
    
        // A market files a survival curve under a full credit identity — entity,
        // seniority, documentation clause, currency — and keeps the recovery beside it.
        let credit: CreditCurveId = "ACME/SeniorUnSec/XR14/USD".parse()?;
        let sheet = QuoteSheet::from_rows(USD_SOFR_CLOSE.iter().copied())?;
        let market = MarketContext::as_of(reference)
            .with_recipe_quotes(&CollateralSpec::Currency("USD".parse()?), &sheet)?
            .with_credit_curve(credit.clone(), Arc::new(pillars.clone()), Some(0.40));
        for (role, key) in market.contents() {
            println!("{role:<11} {key}");
        }
        let (curve, recovery) = market.credit_curve(&credit)?;
        let recovery = recovery.expect("stated");
        println!("filed 5Y          {:.10}", curve.view::<f64>().survival_probability(five)?);
        println!("recovery          {recovery:.2}");
    
        Ok(())
    }
    Python
    import datetime as dt
    import math
    
    import helixio as hlx
    from desk_sheets import SOFR_CLOSE_DATE, USD_SOFR_CLOSE
    
    REFERENCE = SOFR_CLOSE_DATE
    THREE, FIVE = dt.date(2028, 10, 2), dt.date(2031, 10, 1)
    
    def years(d: dt.date) -> float:
        return (d - REFERENCE).days / 365.0
    
    # Survival of 96% to three years and 90% to five: returned exactly at the
    # pillars, and between them read from the log survival.
    pillars = hlx.curves.SurvivalCurve(REFERENCE, [(THREE, 0.96), (FIVE, 0.90)])
    print(f"survival 2030-01-02 {pillars.survival(dt.date(2030, 1, 2)):.10f}")
    
    # One hazard of 2% a year: survival exp(-h t).
    hazard = hlx.curves.SurvivalCurve.flat_hazard(REFERENCE, 0.02)
    print(f"survival 5Y       {hazard.survival(FIVE):.10f}")
    print(f"exp(-h t)         {math.exp(-0.02 * years(FIVE)):.10f}")
    
    # A default density rising from 2% a year today to 3% at five years. Linear
    # density integrates to the average of its ends times the span.
    density = hlx.curves.SurvivalCurve.default_density(REFERENCE, [(REFERENCE, 0.020), (FIVE, 0.030)])
    expected = 1.0 - 0.5 * (0.020 + 0.030) * years(FIVE)
    print(f"survival 5Y       {density.survival(FIVE):.10f}")
    print(f"by the density    {expected:.10f}")
    
    # A market files a survival curve under a full credit identity — entity,
    # seniority, documentation clause, currency — and keeps the recovery beside it.
    credit = "ACME/SeniorUnSec/XR14/USD"
    market = (
        hlx.MarketContext(REFERENCE)
        .with_recipe_quotes("USD", USD_SOFR_CLOSE)
        .with_credit_curve(credit, pillars, recovery=0.40)
    )
    for role, key in market.contents():
        print(f"{role:<11} {key}")
    curve, recovery = market.credit_curve(credit)
    print(f"filed 5Y          {curve.survival(FIVE):.10f}")
    print(f"recovery          {recovery:.2f}")
    survival 2030-01-02 0.9344642857
    survival 5Y       0.9047878393
    exp(-h t)         0.9047878393
    survival 5Y       0.8749315068
    by the density    0.8749315068
    discount    USD
    projection  USD-SOFR
    credit      ACME/SeniorUnSec/XR14/USD
    recovery    ACME/SeniorUnSec/XR14/USD
    filed 5Y          0.9000000000
    recovery          0.40

    The market lists the curve and its recovery as two entries under one identity.

    How it's priced

    Write S(t)S(t) for the probability of surviving to tt, with S(t0)=1S(t_0) = 1, and τ\tau on ACT/365.

    • Pillars: dated probabilities S(Ti)S(T_i), returned exactly at the pillars and read between them from ln⁡S\ln S.
    • A constant hazard hh, the instantaneous default rate given survival:
    S(t)=e−h τ(t0,t).S(t) = e^{-h\, \tau(t_0, t)}.
    • A default density f(t)=− dS/dtf(t) = -\,dS/dt, stated as linear between nodes:
    S(t)=1−∫t0tf(u) du,S(t) = 1 - \int_{t_0}^{t} f(u)\, du,

    so for a density linear from f0f_0 at t0t_0 to f1f_1 at TT, S(T)=1−12(f0+f1) τ(t0,T)S(T) = 1 - \tfrac{1}{2}(f_0 + f_1)\, \tau(t_0, T).

    How credit is quoted, and why a curve needs its recovery, is covered in how curves work.

    API

    Symbol Meaning Rust Python
    S(Ti)S(T_i) survival at pillars PiecewiseSurvival::new, wrapped in Survival::Piecewise curves.SurvivalCurve(reference, pillars)
    e−hτe^{-h\tau} a constant hazard FlatHazard::new, wrapped in Survival::FlatHazard curves.SurvivalCurve.flat_hazard
    f(t)f(t) a default density DensitySurvival::new, wrapped in Survival::Density curves.SurvivalCurve.default_density
    S(t)S(t) the survival to a date SurvivalProbability::survival_probability on Survival::view curves.SurvivalCurve.survival
    identity a curve filed with its recovery MarketContext::with_credit_curve with a CreditCurveId, read with MarketContext::credit_curve MarketContext.with_credit_curve, read with MarketContext.credit_curve
    Argument Type Default Meaning
    reference date required the curve's reference date, where survival is 1
    pillars list[tuple[date, float]] required (date, survival probability), for SurvivalCurve(...)
    hazard float required the constant hazard rate, for SurvivalCurve.flat_hazard(...)
    nodes list[tuple[date, float]] required (date, density), the first on reference, for SurvivalCurve.default_density(...)
    day_count str or DayCountFraction "ACT/365.FIXED" the day count time is measured on, for all three

    Defaults and limits

    • Credit identity: a market files a survival curve under the full identity ENTITY/SENIORITY/DOCCLAUSE/CCY, for example ACME/SeniorUnSec/XR14/USD or ACME/SubTier2/XR14/USD, and refuses a bare entity, so two curves on one issuer can't collide. Any entity name works. The seniority is one of SeniorSec, SeniorUnSec, SeniorLossAbsorbingCapacity, SubTier1, SubTier2, SubLowerTier2, SubTier3 and Other, and the documentation clause one of XR14 (the North American standard), MM14 (the European standard), MR14 and CR14. Any other spelling is refused with an error naming the part.
    • Recovery: a curve is filed with its recovery, because a survival curve without one has no units.
    • Solved from quotes: a curve solved from credit default swap quotes is shown in credit calibration.

    See also

    • Source: crates/helixrs/src/curves/survival.rs, crates/helixrs/src/statics/credit_identity.rs
    • Related pages:
      • Credit calibration, for a curve solved from quotes.
      • Spread curves, for default risk stated as a spread.

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