Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

credit-lab

A credit risk toolkit from first principles in pure Python + NumPy/SciPy. The Merton structural model (equity as a call on the firm's assets), reduced-form hazard/intensity machinery, CDS legs with the par-spread bootstrap, and defaultable bond pricing — every formula derived from its definition, no credit or pricing library underneath.

56/56 tests verifying algebraic identities — three independent debt constructions forced to agree, the credit triangle emerging from annuity cancellation, round-trip bootstraps — never a comparison to another library.

$ pytest tests/
=========================== 56 passed in 0.46s ============================

Every identity in this repo was adversarially verified by a multi-agent workflow before a line of code was written — 39 claims were independently checked and 9 had subtle errors corrected (e.g. the t→0 branch-consistency check must run at t=1e-12, not 1e-8, where the discounting term K·r·t still dwarfs rtol 1e-12; the K→0 debt bound needs a derived half-ulp slack ε·V when debt is built off the balance sheet; a 0<R<1 risky bond is non-monotone in hazard — decreasing only below λ*≈1.05 — while R=1 is globally increasing; the hazard curve's behavior past its last knot is a convention that must be pinned by its own test, not assumed). See "How this was built".

Why this exists

Credit is where option theory and actuarial math meet, and almost nobody implements it honestly — practitioners call a CDS pricer, students memorize "spread ≈ λ(1−R)" without knowing when it's exact. This repo is the opposite: equity really is priced as a Black-Scholes call on assets, the protection leg really is summed period by period, and every test pins an algebraic identity — two or three independent constructions of the same number forced to agree.

The structural side is option algebra (the BS block mirrors monte-carlo-lab's options.py); the discounting spine is tvm-lab; the binomial-tree bond side of fixed income lives in fixedalt-lab. This lab fills the missing pillar: default.

The 5 modules

Module Topic Headline functions
bs.py Black-Scholes building block call_price, put_price, d1, d2, norm_cdf
merton.py Structural model (Merton 1974) equity_value, debt_value, default_probability, distance_to_default, credit_spread, equivalent_hazard, analyze, mc_default_probability
hazard.py Reduced-form survival HazardCurve (piecewise-flat), survival, forward_survival, default_density, expected_loss
cds.py CDS legs + bootstrap rpv01, protection_leg_pv, par_spread, par_spread_flat_continuous, price_cds, bootstrap_hazard_curve
riskybond.py Defaultable bonds risky_bond_price, risky_zcb_price_flat, zcb_credit_spread

API in 30 seconds

from credit import (
    analyze, equity_value, default_probability,        # structural
    HazardCurve, expected_loss,                        # reduced form
    par_spread, bootstrap_hazard_curve,                # CDS
    risky_bond_price, zcb_credit_spread,               # bonds
)

# Merton: a firm with V=100, debt face K=80 due in 1y, asset vol 20%
res = analyze(100, 80, r=0.05, sigma_v=0.2, t=1)
res.equity                  # 24.589  — a BS call on the assets
res.debt                    # 75.411  — = V - E = K*e^(-rT) - put (parity!)
res.default_probability     # 10.28%  — Phi(-d2)
res.credit_spread           # 90.7 bps

# Reduced form: bootstrap a hazard curve from CDS quotes...
curve = bootstrap_hazard_curve(
    tenors=(1.0, 3.0, 5.0), spreads=(0.0060, 0.0125, 0.0210),
    recovery=0.4, r=0.03, freq=4)
curve.survival(5.0)         # P(no default by 5y)

# ...and price a different instrument off the same curve
risky_bond_price(0.06, curve, recovery=0.4, r=0.03, maturity=5.0, freq=4)

# CFA credit framing
expected_loss(pd=0.04, recovery=0.6, ead=10_000_000)   # 160,000 = PD x LGD x EAD

The algebraic identities we actually test

56 tests, every one an identity the formula must satisfy. Highlights:

Black-Scholes block

  • Put-call parity call − put + K·e^(−rT) == S — emerges via Φ(x)+Φ(−x)=1, never built in
  • Gamma symmetry S·φ(d1) == K·e^(−rT)·φ(d2) — catches the ±σ²/2 sign swap parity can't see
  • d1 − d2 == σ√T as a real identity (d2 implemented from its own formula)
  • ATM-forward closed form call == S·(2Φ(σ√T/2)−1) + the Brenner-Subrahmanyam bound

Merton structural

  • Debt three ways: V − call == K·e^(−rT) − put == K·e^(−rT)·Φ(d2) + V·Φ(−d1) — balance sheet, parity route, and survival+recovery decomposition, all independent
  • Homogeneity: everything depends on (V, K) only through leverage (GBM scale invariance)
  • σ→0 trichotomy: PD → 0/1 deterministically, with the knife edge V == K·e^(−rT) landing on 1/2
  • Asset substitution is exact: E(σ)+D(σ) == V for every σ — vol is a pure wealth transfer
  • The distressed counterexample: PD is not monotone in σ for an underwater firm (pinned, so nobody "generalizes" the test)
  • spread ≥ 0 with equality iff PD == 0, at the floating-point level it actually holds
  • equivalent_hazard == −ln(1−PD)/T is a strict upper bound on the spread (Merton debt embeds recovery); the zero-recovery piece maps exactly: e^(−(r+λeq)T) == e^(−rT)(1−PD)
  • One MC band: simulated GBM default frequency lands within 4σ of Φ(−d2)

Hazard / survival

  • Axioms (S(0)=1, non-increasing) + multiplicativity S(t₂) == S(t₁)·S(t₁,t₂) with the forward survival accumulated by its own loop (never the ratio)
  • Knot-refinement invariance: splitting segments without changing λ never changes S
  • PD via −expm1(−H): full relative accuracy at λ=1e-9 where 1−exp loses 8 digits
  • ∫λ(t)S(t)dt == PD(T) by quadrature — calculus the implementation never does

CDS

  • Both legs vs geometric closed forms derived on paper
  • The credit triangle: par_spread_flat_continuous == λ(1−R) EXACTLY, invariant in r and T — coded as the ratio of two closed-form legs so the cancellation is emergent, not an echo
  • The discrete par spread has its own r- and T-invariant closed form (1−R)(e^(λΔ)−1)·freq, converging to the triangle from above at rate (1−R)λ²/(2·freq)
  • Protection telescopes at r=0: (1−R)(1−S(T)) for any curve
  • Bootstrap round trip: λ → spreads → bootstrap → λ at rtol 1e-9 (honestly brentq-limited), plus a single-tenor analytic inverse the root-finder cannot fake

Risky bonds

  • The cleanest identity in credit: zero-coupon, zero-recovery, flat λ ⇒ P == e^(−(r+λ)T) for any payment frequency, so the yield spread IS the hazard rate
  • Bond–CDS decomposition: bond == c·RPV01 + Z(T)S(T) + R·(zero-recovery protection leg) — the bond's standalone loop vs CDS primitives, exact because both share one payment-timing convention
  • R=1 is NOT riskless (timing trichotomy): equality only when recovery and redemption coincide (n=1); for n≥2, r>0 the bond is worth more — face paid early at default is discounted less
  • λ→∞ pins where recovery lands on the grid: P → R·e^(−rΔ) (period end, not 0)
  • End-to-end pipeline: CDS quotes → bootstrap → bond price, cross-checked against the generating curve

Worked examples

PYTHONPATH=. python examples/merton_structure.py    # leverage ladder + asset substitution
PYTHONPATH=. python examples/credit_triangle.py     # the triangle, exact + convergence
PYTHONPATH=. python examples/bootstrap_pipeline.py  # quotes -> hazards -> bond

credit_triangle.py output (the invariance is the point):

Continuous premium: EXACT, invariant in r and T
  r=   0%  T=   1y   s* = 1.2000000000%
  r=   3%  T=   5y   s* = 1.2000000000%
  r=  10%  T=  30y   s* = 1.2000000000%

Discrete premium: converges from ABOVE at O(1/freq)
  freq     par spread    error vs triangle
     1    1.21208040%            1.208e-04
     4    1.20300501%            3.005e-05
   252    1.20004762%            4.762e-07

bootstrap_pipeline.py reprices the input quotes to the basis point:

   t       S(t)    PD(0,t)   repriced spread
   1   0.990062  0.993781%       60.0000 bps
   5   0.832768 16.723195%      210.0000 bps

How this was built

This repo was built with an adversarial multi-agent workflow before implementation:

  1. Design panel — three independent agents proposed the modules and identities from different angles (structural-model-first, intensity/CDS-first, testing-first).
  2. Synthesis — one canonical spec: 39 identities, precise signatures, 16 analytically-derived golden vectors (mpmath, 50 digits).
  3. Adversarial verification — one agent per identity tried to refute it. 9 of 39 were corrected before any code: the t→0 consistency scale, the K→0 half-ulp debt-bound slack, the non-monotone 0<R<1 bond, the hazard-curve extension convention, the bootstrap tolerance honestly tied to brentq's xtol, the corrected Merton golden literals (the originally proposed PD was 1 ulp off the correctly-rounded double).
  4. Implementation against the verified spec — debt routed through the put (the small correction, never the difference of large numbers), spreads via log1p, PD via expm1.
  5. Adversarial code review — reviewers per dimension (math, numerical, API, tests), findings verified before applying.

The result: identities that are right because they were proven right, not because they happened to pass.

What's intentionally NOT here yet

  • v0.2.0 — accrual-on-default premium leg (half-period convention) + upfront/running quoting
  • v0.2.0 alt — KMV-style calibration: solve (V, σ_V) from observed equity value and equity vol
  • v0.3.0 — CDS index (portfolio) pricing; Gaussian copula default correlation
  • v0.3.0 alt — stochastic hazard (CIR intensity) with closed-form survival
  • v0.4.0 — counterparty CVA on the CDS itself (wrong-way risk demo)

Related repos

  • monte-carlo-lab — the BS block here mirrors its options.py; its GBM engine is the MC cross-check
  • tvm-lab — the discounting spine under every leg
  • fixedalt-lab — term structure + binomial-tree bonds; this lab adds default
  • convexity-lab, pde-lab — the option-pricing siblings
  • var-lab, port-lab — market risk and allocation; credit completes the risk triad

References

  • Merton, R. C. (1974). On the Pricing of Corporate Debt: The Risk Structure of Interest Rates. Journal of Finance.
  • O'Kane, D. (2008). Modelling Single-name and Multi-name Credit Derivatives. Wiley.
  • Hull, J. (2018). Options, Futures, and Other Derivatives. Pearson. (Credit risk chapters.)
  • Duffie, D. & Singleton, K. (2003). Credit Risk: Pricing, Measurement, and Management. Princeton.

License

MIT.

About

Credit risk from first principles: Merton structural model (equity = call on assets, debt three independent ways), hazard/intensity survival, CDS legs + bootstrap, defaultable bonds. Every formula identity-tested — the credit triangle emerges, never echoed. No pricing library underneath.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages