Tutorial 1 — single firm, AAPL-style walk-through¶
This tutorial fits the Merton model to an Apple-like firm. We use a
synthetic equity time series (so the docs build is hermetic), but the
exact same code works on a real AAPL series fetched via
Firm.from_yfinance("AAPL") once you pip install "merton[data]".
Set up¶
import numpy as np
import merton
from merton import Firm, MertonModel, fit
print("merton version:", merton.__version__)
merton version: 1.0.2
Build a synthetic AAPL series¶
We simulate one year of daily prices under geometric Brownian motion with a typical large-cap volatility of ~25 % and a small upward drift.
rng = np.random.default_rng(2026)
n_days = 252
sigma_E = 0.25
mu_E = 0.10
dt = 1.0 / 252.0
log_returns = rng.normal((mu_E - 0.5 * sigma_E**2) * dt, sigma_E * np.sqrt(dt), size=n_days)
equity_path = 3.0e12 * np.exp(np.cumsum(log_returns)) # starts ≈ $3T market cap
firm = Firm(
equity=equity_path,
debt_short=20e9,
debt_long=90e9,
rf=0.045,
dividend_yield=0.006,
horizon=1.0,
ticker="AAPL_SYNTH",
)
print("equity range:", f"${equity_path.min()/1e12:.2f}T", "→", f"${equity_path.max()/1e12:.2f}T")
print("default point (KMV):", f"${float(firm.default_point_value())/1e9:.1f}B")
equity range: $2.89T → $4.48T
default point (KMV): $65.0B
Three calibration methods side-by-side¶
snapshot_firm = firm.replace(equity=float(equity_path[-1]), equity_vol=sigma_E)
methods = ["naive", "jmr_iterative", "vassalou_xing"]
print(f"{'method':<18}{'DD':>10}{'PD':>14}{'σ_A':>10}")
print("-" * 52)
for m in methods:
result = fit(snapshot_firm, method=m)
print(f"{m:<18}{result.dd:>10.4f}{result.pd:>14.6e}{result.asset_vol:>10.4f}")
method DD PD σ_A
----------------------------------------------------
naive 17.0505 1.733391e-65 0.2480
jmr_iterative 17.1771 1.971827e-66 0.2465
vassalou_xing 17.1771 1.971827e-66 0.2465
Full Duan MLE on the equity series¶
result = MertonModel(method="duan_mle").fit(firm)
print(result.summary())
MertonResult (AAPL_SYNTH, method=duan_mle)
Distance-to-default : 16.1658
Probability of default : 0.000000
Implied spread (LGD=.6) : 0.00 bps
Asset value : 4,445,481,015,090.43
Asset volatility (σ_A) : 0.2617
Asset drift (μ) : 0.4210
Default point : 65,000,000,000.00
Horizon (years) : 1.0
Solver converged : True (3 iters)
The Duan MLE also returns asymptotic standard errors, which we can turn into confidence intervals on any quantity of interest:
ci = result.confidence_interval(level=0.95, method="asymptotic")
for name, interval in ci.items():
print(f" {name:<14} 95% CI = [{interval.lower:+.4g}, {interval.upper:+.4g}]")
asset_vol 95% CI = [+0.2388, +0.2846]
asset_drift 95% CI = [-0.09305, +0.935]
dd 95% CI = [+14.73, +17.6]
pd 95% CI = [-9.81e-58, +1.069e-57]
Bootstrapped confidence intervals¶
When the calibrator doesn’t return a Hessian (or we want a CI that doesn’t assume MLE asymptotics), the model can run a block bootstrap on the equity series:
bs_result = MertonModel(
method="vassalou_xing",
n_bootstrap=100,
block_length=20,
random_state=42,
).fit(firm)
bs_ci = bs_result.confidence_interval(level=0.95, method="bootstrap")
for name, interval in bs_ci.items():
print(f" {name:<14} bootstrap CI = [{interval.lower:+.4g}, {interval.upper:+.4g}]")
asset_vol bootstrap CI = [+0.2336, +0.2882]
asset_drift bootstrap CI = [-0.1142, +0.9556]
dd bootstrap CI = [+14.62, +18.4]
pd bootstrap CI = [+9.908e-76, +2.516e-48]
PD term structure¶
result.pd_term_structure(horizons=[1/12, 3/12, 6/12, 1, 3, 5])
| horizon_years | dd | pd | |
|---|---|---|---|
| 0 | 0.083333 | 55.942197 | 0.000000e+00 |
| 1 | 0.250000 | 32.304314 | 3.042096e-229 |
| 2 | 0.500000 | 22.849039 | 7.469519e-116 |
| 3 | 1.000000 | 16.165817 | 4.393266e-59 |
| 4 | 3.000000 | 9.354370 | 4.204974e-21 |
| 5 | 5.000000 | 7.262154 | 1.904865e-13 |
Greeks at the calibrated operating point¶
g = result.greeks()
print(f" equity Δ : {g.equity_delta:.6f}")
print(f" equity Vega : {g.equity_vega:.6e}")
print(f" equity Γ : {g.equity_gamma:.6e}")
print(f" ∂PD/∂L : {g.pd_dleverage:.6e}")
print(f" ∂PD/∂σ_A : {g.pd_dvol:.6e}")
print(f" ∂PD/∂r : {g.pd_drate:.6e}")
equity Δ : 0.994018
equity Vega : 4.429977e-47
equity Γ : 8.566808e-72
∂PD/∂L : 4.191535e-68
∂PD/∂σ_A : 4.475663e-56
∂PD/∂r : -2.724498e-57
What changes with real data?¶
Swap the synthetic series for a real AAPL pull:
firm = Firm.from_yfinance("AAPL", lookback="2y") # needs merton[data]
…and re-run any of the cells above. The package interface stays identical.