Skip to content

Commit b1761e8

Browse files
docs: add 6 comprehensive example files for v7.0.0 modules
- 08_credit_risk.py: Merton model, CDS pricing/bootstrap, portfolio EL/UL/credit VaR, Z-spread - 09_volatility_models.py: 5 OHLCV estimators, GARCH/GJR fit+forecast, HAR-RV, regimes - 10_monte_carlo.py: GBM, Heston, jump-diffusion, CIR, option pricing vs BS benchmark - 11_portfolio_insurance.py: CPPI bull/bear/sensitivity, TIPP ratcheting floor - 12_benchmark_analytics.py: IR/TE/capture, active share, BHB sector attribution - 13_scenario_analysis.py: 9 historical scenarios, custom shocks, reverse stress, corr stress - examples/README.md: updated index and learning path
1 parent e224fc9 commit b1761e8

7 files changed

Lines changed: 1542 additions & 102 deletions

examples/08_credit_risk.py

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
"""
2+
Example 08: Credit Risk Analysis
3+
4+
Demonstrates MeridianAlgo's credit risk module:
5+
- Merton structural model: equity as call option on firm assets
6+
- Default probability term structure
7+
- CDS pricing and hazard rate bootstrapping
8+
- Portfolio expected loss and credit VaR
9+
- Z-spread and DV01 computation
10+
"""
11+
12+
import numpy as np
13+
import pandas as pd
14+
15+
from meridianalgo.credit import (
16+
CreditDefaultSwap,
17+
CreditRiskAnalyzer,
18+
MertonModel,
19+
ZSpreadCalculator,
20+
)
21+
22+
23+
# ---------------------------------------------------------------------------
24+
# 1. Merton Structural Model
25+
# ---------------------------------------------------------------------------
26+
27+
print("=" * 60)
28+
print("1. MERTON STRUCTURAL MODEL")
29+
print("=" * 60)
30+
31+
# A leveraged firm: $500M equity, $800M debt, 35% equity vol
32+
model = MertonModel(
33+
equity_value=500e6,
34+
equity_volatility=0.35,
35+
debt_face_value=800e6,
36+
time_to_maturity=1.0,
37+
risk_free_rate=0.05,
38+
)
39+
result = model.calibrate()
40+
41+
print(f"Input equity value: ${500e6/1e6:.0f}M")
42+
print(f"Input debt face value: ${800e6/1e6:.0f}M")
43+
print(f"Input equity vol: 35.00%")
44+
print()
45+
print(f"Implied asset value: ${result['asset_value']/1e6:.1f}M")
46+
print(f"Implied asset vol: {result['asset_volatility']:.2%}")
47+
print(f"Leverage ratio: {result['leverage_ratio']:.2%}")
48+
print(f"Distance to default: {result['distance_to_default']:.4f} sigma")
49+
print(f"Default probability: {result['default_probability']:.4%}")
50+
print(f"Expected recovery rate: {result['expected_recovery_rate']:.4%}")
51+
52+
# Compare three firms: low, medium, and high leverage
53+
print("\n--- Leverage Comparison ---")
54+
firms = [
55+
("Low leverage", 800e6, 0.25, 200e6),
56+
("Medium leverage", 500e6, 0.35, 800e6),
57+
("High leverage", 200e6, 0.50, 1500e6),
58+
]
59+
for name, equity, ev, debt in firms:
60+
m = MertonModel(equity, ev, debt, time_to_maturity=1.0, risk_free_rate=0.05)
61+
r = m.calibrate()
62+
print(f" {name:20s} DD={r['distance_to_default']:6.3f} PD={r['default_probability']:.4%}")
63+
64+
65+
# ---------------------------------------------------------------------------
66+
# 2. Default Probability Term Structure
67+
# ---------------------------------------------------------------------------
68+
69+
print("\n" + "=" * 60)
70+
print("2. DEFAULT PROBABILITY TERM STRUCTURE")
71+
print("=" * 60)
72+
73+
model_ts = MertonModel(
74+
equity_value=400e6,
75+
equity_volatility=0.40,
76+
debt_face_value=900e6,
77+
time_to_maturity=1.0,
78+
risk_free_rate=0.05,
79+
)
80+
horizons = [0.25, 0.5, 1.0, 2.0, 3.0, 5.0, 7.0, 10.0]
81+
term_struct = model_ts.default_probability_term_structure(horizons)
82+
83+
print(f"{'Horizon':>10} {'PD':>10} {'Annualized':>12}")
84+
for t, pd_val in term_struct.items():
85+
ann_pd = 1 - (1 - pd_val) ** (1 / t) if t > 0 else 0
86+
print(f"{t:>10.2f} {pd_val:>10.4%} {ann_pd:>12.4%}")
87+
88+
89+
# ---------------------------------------------------------------------------
90+
# 3. CDS Pricing
91+
# ---------------------------------------------------------------------------
92+
93+
print("\n" + "=" * 60)
94+
print("3. CDS PRICING")
95+
print("=" * 60)
96+
97+
# Constant hazard rate CDS
98+
cds = CreditDefaultSwap(
99+
hazard_rate=0.02,
100+
recovery_rate=0.40,
101+
risk_free_rate=0.05,
102+
maturity=5.0,
103+
payment_frequency=4,
104+
)
105+
r = cds.price()
106+
107+
print(f"Hazard rate: {cds.hazard_rate:.4f}")
108+
print(f"Recovery rate: {cds.recovery_rate:.2%}")
109+
print(f"Fair spread: {r.fair_spread * 10000:.2f} bps")
110+
print(f"Risky annuity (DV01): {r.risky_annuity:.6f}")
111+
print(f"5yr survival prob: {r.survival_probability:.6f}")
112+
113+
# Spread sensitivity to hazard rate
114+
print("\n--- Spread vs Hazard Rate ---")
115+
print(f"{'Hazard Rate':>14} {'5yr Spread (bps)':>18} {'5yr Survival':>14}")
116+
for h in [0.005, 0.010, 0.020, 0.040, 0.080, 0.150]:
117+
c = CreditDefaultSwap(h, recovery_rate=0.40, maturity=5.0)
118+
r = c.price()
119+
print(f" {h:12.3%} {r.fair_spread * 10000:>16.2f} {r.survival_probability:>14.6f}")
120+
121+
# Recover hazard rate from market spread
122+
print("\n--- Bootstrap Hazard Rate from Market Spread ---")
123+
market_spread = 0.0180
124+
cds_from_spread = CreditDefaultSwap.from_spread(
125+
spread=market_spread,
126+
recovery_rate=0.40,
127+
risk_free_rate=0.05,
128+
maturity=5.0,
129+
)
130+
print(f"Market spread: {market_spread * 10000:.1f} bps")
131+
print(f"Implied hazard: {cds_from_spread.hazard_rate:.6f}")
132+
check = cds_from_spread.price().fair_spread
133+
print(f"Verified spread: {check * 10000:.2f} bps (roundtrip error: {abs(check - market_spread)*1e6:.1f} micro-bps)")
134+
135+
# Bootstrap full hazard curve
136+
print("\n--- CDS Curve Bootstrap ---")
137+
maturities = [1.0, 3.0, 5.0, 7.0, 10.0]
138+
market_spreads = [0.0080, 0.0120, 0.0150, 0.0175, 0.0210]
139+
hazard_curve = CreditDefaultSwap.bootstrap_hazard_curve(
140+
maturities, market_spreads, recovery_rate=0.40, risk_free_rate=0.05
141+
)
142+
print(f"{'Maturity':>10} {'Market Spread':>16} {'Hazard Rate':>14}")
143+
for mat, spread, hazard in zip(maturities, market_spreads, hazard_curve.values):
144+
print(f" {mat:8.1f}y {spread * 10000:>14.1f}bps {hazard:>14.6f}")
145+
146+
147+
# ---------------------------------------------------------------------------
148+
# 4. Portfolio Credit Risk: EL, UL, Credit VaR
149+
# ---------------------------------------------------------------------------
150+
151+
print("\n" + "=" * 60)
152+
print("4. PORTFOLIO CREDIT RISK")
153+
print("=" * 60)
154+
155+
analyzer = CreditRiskAnalyzer()
156+
157+
# Loan portfolio
158+
portfolio = pd.DataFrame({
159+
"obligor": ["Corp A", "Corp B", "Corp C", "Corp D", "Corp E",
160+
"Corp F", "Corp G", "Corp H"],
161+
"rating": ["BBB", "BB", "B", "BBB", "A",
162+
"BB", "CCC", "A"],
163+
"pd": [0.0020, 0.0080, 0.0250, 0.0020, 0.0005,
164+
0.0080, 0.1000, 0.0005],
165+
"lgd": [0.45, 0.50, 0.60, 0.40, 0.35,
166+
0.50, 0.70, 0.35],
167+
"ead": [5e6, 3e6, 1e6, 8e6, 10e6,
168+
2e6, 0.5e6, 15e6],
169+
})
170+
171+
portfolio["el"] = portfolio.apply(
172+
lambda r: analyzer.expected_loss(r["pd"], r["lgd"], r["ead"]), axis=1
173+
)
174+
portfolio["ul"] = portfolio.apply(
175+
lambda r: analyzer.unexpected_loss(r["pd"], r["lgd"], r["ead"]), axis=1
176+
)
177+
portfolio["credit_var_99"] = portfolio.apply(
178+
lambda r: analyzer.credit_var(r["pd"], r["lgd"], r["ead"], confidence=0.999), axis=1
179+
)
180+
181+
print(portfolio[["obligor", "rating", "pd", "lgd", "ead", "el", "credit_var_99"]]
182+
.to_string(index=False, float_format=lambda x: f"{x:,.0f}" if x > 100 else f"{x:.4f}"))
183+
184+
result = analyzer.portfolio_expected_loss(portfolio[["pd", "lgd", "ead"]])
185+
print(f"\nPortfolio Summary:")
186+
print(f" Total EAD: ${portfolio['ead'].sum()/1e6:.1f}M")
187+
print(f" Total Expected Loss: ${result['total_el']/1e3:.1f}K ({result['el_rate']:.3%} of EAD)")
188+
print(f" Total Unexpected Loss: ${result['total_ul']/1e3:.1f}K")
189+
print(f" Herfindahl Index: {result['herfindahl_index']:.4f}")
190+
print(f" Top-10 Concentration: {result['top10_concentration']:.2%}")
191+
192+
193+
# ---------------------------------------------------------------------------
194+
# 5. Z-Spread and DV01
195+
# ---------------------------------------------------------------------------
196+
197+
print("\n" + "=" * 60)
198+
print("5. Z-SPREAD AND DV01")
199+
print("=" * 60)
200+
201+
# 5% coupon bond, 5-year maturity
202+
cash_flows = [50, 50, 50, 50, 1050]
203+
times = [1.0, 2.0, 3.0, 4.0, 5.0]
204+
risk_free_rates = [0.035, 0.038, 0.040, 0.042, 0.044]
205+
206+
calc = ZSpreadCalculator(cash_flows, times, risk_free_rates)
207+
208+
par_price = calc.theoretical_price(0.0)
209+
print(f"Risk-free par price: ${par_price:.4f}")
210+
211+
market_prices = [par_price - 5, par_price - 2, par_price, par_price + 2, par_price + 5]
212+
print(f"\n{'Market Price':>14} {'Z-Spread (bps)':>16} {'DV01':>10}")
213+
for price in market_prices:
214+
z = calc.z_spread(price)
215+
dv01 = calc.dv01(z)
216+
print(f" ${price:10.4f} {z * 10000:>14.2f} {dv01:>10.4f}")
217+
218+
print(f"\nDV01 at z=0: {calc.dv01():.4f} (approx ${calc.dv01() * 1000:,.2f} per $1M notional per bp)")

0 commit comments

Comments
 (0)