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.
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(())
}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 for the probability of surviving to , with , and on ACT/365.
- Pillars: dated probabilities , returned exactly at the pillars and read between them from .
- A constant hazard , the instantaneous default rate given survival:
- A default density , stated as linear between nodes:
so for a density linear from at to at , .
How credit is quoted, and why a curve needs its recovery, is covered in how curves work.
API
| Symbol | Meaning | Rust | Python |
|---|---|---|---|
| survival at pillars | PiecewiseSurvival::new, wrapped in Survival::Piecewise |
curves.SurvivalCurve(reference, pillars) |
|
| a constant hazard | FlatHazard::new, wrapped in Survival::FlatHazard |
curves.SurvivalCurve.flat_hazard |
|
| a default density | DensitySurvival::new, wrapped in Survival::Density |
curves.SurvivalCurve.default_density |
|
| 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 exampleACME/SeniorUnSec/XR14/USDorACME/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 ofSeniorSec,SeniorUnSec,SeniorLossAbsorbingCapacity,SubTier1,SubTier2,SubLowerTier2,SubTier3andOther, and the documentation clause one ofXR14(the North American standard),MM14(the European standard),MR14andCR14. 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.