← back to finance II Credit Risk MIT OCW 18.S096 + 15.450 (CC BY-NC-SA 4.0)
Credit risk is the possibility that a borrower fails to pay. Quantifying it means modeling when default happens, how much you lose, and how the market prices that uncertainty into bond spreads.
time firm value V(t) T D V(T)>D V(T)<D Equity = max(V(T)−D, 0) Merton model: default checked at maturity T, not mid-path. Default probability and hazard rates The hazard rate λ(t) is the default intensity at time t — the probability of default over a short interval [t, t+dt], conditional on survival to t, is approximately λ(t)dt. If the hazard rate is constant, the survival probability decays exponentially: S(t) = e^(-λt). The cumulative default probability is 1 − S(t). Higher hazard rate means the bond issuer is more likely to default in any given instant.
Scheme โ
(define (survival-prob hazard t)
(exp (* (- hazard) t)))
(define (default-prob hazard t)
(- 1 (survival-prob hazard t)))
(define lambda-ig 0.005 )
(define lambda-hy 0.03 )
(display "--- Investment Grade (lambda=0.5%) ---" ) (newline)
(display "1yr survival: " ) (display (exact->inexact (survival-prob lambda-ig 1 ))) (newline)
(display "5yr survival: " ) (display (exact->inexact (survival-prob lambda-ig 5 ))) (newline)
(display "10yr PD: " ) (display (exact->inexact (default-prob lambda-ig 10 ))) (newline)
(newline)
(display "--- High Yield (lambda=3%) ---" ) (newline)
(display "1yr survival: " ) (display (exact->inexact (survival-prob lambda-hy 1 ))) (newline)
(display "5yr survival: " ) (display (exact->inexact (survival-prob lambda-hy 5 ))) (newline)
(display "10yr PD: " ) (display (exact->inexact (default-prob lambda-hy 10 ))) (newline)
(display "Expected default time (IG): " )
(display (/ 1 lambda-ig)) (display " years" ) (newline)
(display "Expected default time (HY): " )
(display (exact->inexact (/ 1 lambda-hy))) (display " years" )
Python โ
import math
def survival(lam, t): return math .exp(-lam * t)
def default_prob(lam, t): return 1 - survival(lam, t)
lam_ig, lam_hy = 0.005 , 0.03
print ("--- Investment Grade (lambda=0.5%) ---" )
print (f"1yr survival: {survival(lam_ig, 1):.6f}" )
print (f"5yr survival: {survival(lam_ig, 5):.6f}" )
print (f"10yr PD: {default_prob(lam_ig, 10):.6f}" )
print ("\n--- High Yield (lambda=3%) ---" )
print (f"1yr survival: {survival(lam_hy, 1):.6f}" )
print (f"5yr survival: {survival(lam_hy, 5):.6f}" )
print (f"10yr PD: {default_prob(lam_hy, 10):.6f}" )
print (f"Expected default time (IG): {1/lam_ig:.0f} years" )
print (f"Expected default time (HY): {1/lam_hy:.1f} years" )
Merton structural model Merton treats equity as a call option on the firm's assets with strike equal to the face value of debt D. If assets V exceed D at maturity, equity holders keep V − D. Otherwise, default: equity is worthless and debt holders recover V. This connects credit risk to option pricing—Black-Scholes gives you the default probability as N(-d₂).
Scheme โ
(define (norm-cdf x)
(/ 1 (+ 1 (exp (* -1.7 x)))))
(define (merton-model V D r sigma T)
(let* ((d1 (/ (+ (log (/ V D)) (* (+ r (/ (* sigma sigma) 2 )) T))
(* sigma (sqrt T))))
(d2 (- d1 (* sigma (sqrt T))))
(equity (- (* V (norm-cdf d1))
(* D (exp (* (- r) T)) (norm-cdf d2))))
(pd (norm-cdf (- d2))))
(list d1 d2 equity pd)))
(define result1 (merton-model 100 60 0.05 0.25 1 ))
(display "--- Healthy firm (V=100, D=60) ---" ) (newline)
(display "d1: " ) (display (exact->inexact (car result1))) (newline)
(display "d2: " ) (display (exact->inexact (cadr result1))) (newline)
(display "Equity value: " ) (display (exact->inexact (caddr result1))) (newline)
(display "Default prob: " ) (display (exact->inexact (cadddr result1))) (newline)
(newline)
(define result2 (merton-model 100 90 0.05 0.40 1 ))
(display "--- Distressed firm (V=100, D=90, vol=40%) ---" ) (newline)
(display "d1: " ) (display (exact->inexact (car result2))) (newline)
(display "d2: " ) (display (exact->inexact (cadr result2))) (newline)
(display "Equity value: " ) (display (exact->inexact (caddr result2))) (newline)
(display "Default prob: " ) (display (exact->inexact (cadddr result2)))
Python โ
import math
def norm_cdf(x):
return 1 / (1 + math .exp(-1.7 * x))
def merton(V, D, r, sigma, T):
d1 = (math .log(V/D) + (r + sigma**2 /2 )*T) / (sigma*math .sqrt(T))
d2 = d1 - sigma*math .sqrt(T)
equity = V*norm_cdf(d1) - D*math .exp(-r*T)*norm_cdf(d2)
pd = norm_cdf(-d2)
return d1, d2, equity, pd
d1, d2, eq, pd = merton(100 , 60 , 0.05 , 0.25 , 1 )
print ("--- Healthy firm (V=100, D=60) ---" )
print (f"d1: {d1:.4f}, d2: {d2:.4f}" )
print (f"Equity value: {eq:.2f}" )
print (f"Default prob: {pd:.4f}" )
d1, d2, eq, pd = merton(100 , 90 , 0.05 , 0.40 , 1 )
print ("\n--- Distressed firm (V=100, D=90, vol=40%) ---" )
print (f"d1: {d1:.4f}, d2: {d2:.4f}" )
print (f"Equity value: {eq:.2f}" )
print (f"Default prob: {pd:.4f}" )
Credit spreads The credit spread is the yield difference between a risky bond and a risk-free bond of the same maturity. It compensates for expected loss (default probability times loss given default) plus a risk premium. Under constant hazard rate λ and recovery rate R, the spread approximates λ(1 − R).
Scheme โ
(define (credit-spread hazard recovery)
(* hazard (- 1 recovery)))
(define (risky-bond-price rf spread T)
(exp (* (- (+ rf spread)) T)))
(define rf 0.04 )
(define s-ig (credit-spread 0.005 0.40 ))
(display "IG spread: " ) (display (* s-ig 10000 ))
(display " bps" ) (newline)
(define s-hy (credit-spread 0.03 0.35 ))
(display "HY spread: " ) (display (* s-hy 10000 ))
(display " bps" ) (newline)
(define p-riskfree (exp (* (- rf) 5 )))
(define p-ig (risky-bond-price rf s-ig 5 ))
(define p-hy (risky-bond-price rf s-hy 5 ))
(newline)
(display "5yr risk-free bond: " ) (display (exact->inexact p-riskfree)) (newline)
(display "5yr IG bond: " ) (display (exact->inexact p-ig)) (newline)
(display "5yr HY bond: " ) (display (exact->inexact p-hy)) (newline)
(display "IG discount: " ) (display (exact->inexact (- p-riskfree p-ig))) (newline)
(display "HY discount: " ) (display (exact->inexact (- p-riskfree p-hy)))
Python โ
import math
def credit_spread(lam, recovery):
return lam * (1 - recovery)
def risky_bond_price(rf, spread, T):
return math .exp(-(rf + spread) * T)
rf = 0.04
s_ig = credit_spread(0.005 , 0.40 )
s_hy = credit_spread(0.03 , 0.35 )
print (f"IG spread: {s_ig*10000:.0f} bps" )
print (f"HY spread: {s_hy*10000:.0f} bps" )
p_rf = math .exp(-rf * 5 )
p_ig = risky_bond_price(rf, s_ig, 5 )
p_hy = risky_bond_price(rf, s_hy, 5 )
print (f"\n5yr risk-free bond: {p_rf:.6f}" )
print (f"5yr IG bond: {p_ig:.6f}" )
print (f"5yr HY bond: {p_hy:.6f}" )
print (f"IG discount: {p_rf - p_ig:.6f}" )
print (f"HY discount: {p_rf - p_hy:.6f}" )
Ratings transitions and migration matrices Credit rating agencies assign letter grades (AAA, AA, ..., D). A transition matrix gives the probability of moving from one rating to another over a period. The matrix is row-stochastic (rows sum to 1). Multi-year transitions come from raising the matrix to a power. The absorbing state D (default) only accumulates—you never leave it.
Scheme โ
(define A-row (list 0.90 0.08 0.02 ))
(define B-row (list 0.05 0.85 0.10 ))
(define D-row (list 0.00 0.00 1.00 ))
(define (dot v1 v2)
(apply + (map * v1 v2)))
(define (mat-vec-mul mat vec)
(map (lambda (row) (dot row vec)) mat))
(define (col mat j)
(map (lambda (row) (list-ref row j)) mat))
(define (mat-mul A B)
(map (lambda (row-a)
(map (lambda (j) (dot row-a (col B j)))
(list 0 1 2 )))
A))
(define M (list A-row B-row D-row))
(define M2 (mat-mul M M))
(display "1-year transition matrix:" ) (newline)
(for-each (lambda (row)
(for-each (lambda (x) (display (exact->inexact x)) (display " " )) row)
(newline)) M)
(newline)
(display "2-year transition matrix:" ) (newline)
(for-each (lambda (row)
(for-each (lambda (x) (display (exact->inexact x)) (display " " )) row)
(newline)) M2)
(define (mat-pow M n)
(if (<= n 1 ) M (mat-mul M (mat-pow M (- n 1 )))))
(define M5 (mat-pow M 5 ))
(newline) (display "5-year cumulative default probs:" ) (newline)
(display "From A: " ) (display (exact->inexact (list-ref (car M5) 2 ))) (newline)
(display "From B: " ) (display (exact->inexact (list-ref (cadr M5) 2 )))
Python โ
M = [
[0.90 , 0.08 , 0.02 ],
[0.05 , 0.85 , 0.10 ],
[0.00 , 0.00 , 1.00 ],
]
def mat_mul(A, B):
n = len (A)
return [[sum (A[i][k]*B[k][j] for k in range (n))
for j in range (n)] for i in range (n)]
def mat_pow(M, n):
result = M
for _ in range (n - 1 ):
result = mat_mul(result, M)
return result
print ("1-year transition matrix:" )
for row in M: print (" " .join(f"{x:.4f}" for x in row))
M2 = mat_mul(M, M)
print ("\n2-year transition matrix:" )
for row in M2: print (" " .join(f"{x:.4f}" for x in row))
M5 = mat_pow(M, 5 )
print ("\n5-year cumulative default probs:" )
print (f"From A: {M5[0][2]:.6f}" )
print (f"From B: {M5[1][2]:.6f}" )
Expected loss and CVA Expected loss = PD × LGD × EAD (probability of default times loss given default times exposure at default). The credit valuation adjustment (CVA) discounts this across all future periods, weighting by the probability of defaulting in each interval. CVA is the market price of counterparty credit risk.
Scheme โ
(define (expected-loss pd lgd ead) (* pd lgd ead))
(define (cva hazard recovery rf ead years)
(define lgd (- 1 recovery))
(define dt 1 )
(define (loop t acc)
(if (> t years) acc
(let* ((surv-prev (exp (* (- hazard) (- t 1 ))))
(surv-now (exp (* (- hazard) t)))
(marginal-pd (- surv-prev surv-now))
(discount (exp (* (- rf) (- t 0.5 ))))
(term (* discount marginal-pd lgd ead)))
(loop (+ t 1 ) (+ acc term)))))
(loop 1 0 ))
(define ead 1000000 )
(define hazard 0.02 )
(define recovery 0.40 )
(define rf 0.04 )
(define pd-1yr (- 1 (exp (- hazard))))
(display "1-year PD: " ) (display (exact->inexact pd-1yr)) (newline)
(display "1-year expected loss: $" )
(display (exact->inexact (expected-loss pd-1yr (- 1 recovery) ead)))
(newline)
(define cva-5 (cva hazard recovery rf ead 5 ))
(display "5-year CVA: $" ) (display (exact->inexact cva-5)) (newline)
(define cva-10 (cva hazard recovery rf ead 10 ))
(display "10-year CVA: $" ) (display (exact->inexact cva-10)) (newline)
(display "CVA/EAD (10yr): " )
(display (exact->inexact (* 100 (/ cva-10 ead)))) (display "%" )
Python โ
import math
def expected_loss(pd, lgd, ead): return pd * lgd * ead
def cva(hazard, recovery, rf, ead, years):
lgd = 1 - recovery
total = 0
for t in range (1 , years + 1 ):
surv_prev = math .exp(-hazard * (t-1 ))
surv_now = math .exp(-hazard * t)
marginal_pd = surv_prev - surv_now
discount = math .exp(-rf * (t - 0.5 ))
total += discount * marginal_pd * lgd * ead
return total
ead, hazard, recovery, rf = 1_000_000 , 0.02 , 0.40 , 0.04
pd_1yr = 1 - math .exp(-hazard)
print (f"1-year PD: {pd_1yr:.6f}" )
print (f"1-year expected loss: ${expected_loss(pd_1yr, 1-recovery, ead):,.2f}" )
cva_5 = cva(hazard, recovery, rf, ead, 5 )
cva_10 = cva(hazard, recovery, rf, ead, 10 )
print (f"5-year CVA: ${cva_5:,.2f}" )
print (f"10-year CVA: ${cva_10:,.2f}" )
print (f"CVA/EAD (10yr): {100*cva_10/ead:.4f}%" )
Neighbors