A stochastic short-rate term-structure toolkit from first principles in pure Python + NumPy/SciPy. The Vasicek and CIR models (closed-form affine bond prices, moments, yields), the unifying affine framework that reproduces both by integrating the Riccati ODE, the Vasicek bond-option closed form, and curve/calibration utilities — every formula derived from its definition, no rates library underneath.
54/54 tests verifying algebraic identities — the affine ODE reproducing each closed form, the four limits, put-call parity, the credit-style Feller condition — never a comparison to another library.
$ pytest tests/
=========================== 54 passed in 2.54s ============================
Every identity in this repo was adversarially verified by a multi-agent workflow before a line of code was written — 48 claims were independently checked and 4 had subtle errors corrected (the Monte Carlo bond-price test must be an honest 4σ band — tightening it by adding paths only surfaces the trapezoidal bias floor; the CIR σ→0 limit converges at O(σ²) but hits a ~1e-9 roundoff floor below σ≈1e-3, so the bound is scale-aware with a floor; the calibration "fitted yields match" identity holds only at the global minimum on on-model data, not "regardless of where the optimizer lands"; and the bond-call is not monotone in the bond maturity T_B — that clause was dropped for the sign-definite vega). See "How this was built".
Interest-rate models are where most people reach for QuantLib and never see the
machinery. This repo is the opposite: the bond price is A(τ)·exp(−B(τ)·r)
with B, A from the closed form, and the same price is recovered by
numerically integrating the affine Riccati ODE — so the closed form is proven,
not asserted. Every test pins an algebraic identity (a limit, a round-trip, a
cross-model consistency), not a number copied from a textbook.
It completes the rates pillar of the portfolio: the discounting is
tvm-lab, the deterministic term
structure is fixedalt-lab, and
the CIR process here is exactly the stochastic-intensity engine that
credit-lab left on its roadmap.
| Module | Topic | Headline functions |
|---|---|---|
vasicek.py |
Gaussian OU model | bond_price, B, A, mean, variance, zero_yield, long_yield, mc_discount_factor |
cir.py |
Square-root model | bond_price, gamma, feller_satisfied, expected_rate, variance_rate, sample_exact, mc_bond_price |
affine.py |
Affine framework | AffineParams, vasicek_to_affine, cir_to_affine, solve_ab, affine_bond_price |
curve.py |
Curve / calibration | zero_yield, instantaneous_forward, vasicek_forward, par_yield, calibrate_vasicek |
bondopt.py |
Vasicek ZCB options | option_sigma_p, zcb_call, zcb_put |
from shortrate import vasicek, cir, affine, bondopt
vp = vasicek.VasicekParams(kappa=0.3, theta=0.05, sigma=0.02)
vasicek.bond_price(vp, r0=0.03, tau=4) # 0.859410 (= A·exp(-B·r))
vasicek.long_yield(vp) # 0.047778 (= theta - sigma^2/2kappa^2)
cp = cir.CIRParams(kappa=0.5, theta=0.06, sigma=0.10)
cir.feller_satisfied(cp) # True (2 kappa theta >= sigma^2 -> r > 0)
cir.bond_price(cp, r=0.04, tau=5) # 0.770281
# the central identity: ONE ODE solver reproduces BOTH closed forms
affine.affine_bond_price(affine.cir_to_affine(cp), r=0.04, tau=5) # 0.770281 (rel err ~5e-12)
# a Vasicek bond option, with put-call parity holding to ~1e-17
bondopt.zcb_call(vp, r0=0.03, T_O=1, T_B=4, K=0.85) # 0.03811954 tests, every one an identity the formula must satisfy. Highlights:
- THE central identity:
affine_bond_price(vasicek_to_affine(p), …)and…(cir_to_affine(p), …)reproduce the Vasicek/CIR closed forms by integratingb' = 1 − βb − (ν/2)b²,a' = αb − (δ/2)b²— separatelyb == Bandexp(−a) == A(honest solver tolerance ~1e-8, never 1e-12) - Vasicek/CIR duality:
(κθ, κ, σ², 0)vs(κθ, κ, 0, σ²)—δandνswap (constant-vol vs square-root) b(τ)saturates (a β-sign slip makes it explode);P(·,0) == 1exactly
- Affine price
A·exp(−B·r);zero_yieldreuses the sameB(price/yield consistent to ulps) - The four limits: κ→0 → driftless integrated-BM price; σ→0 → deterministic OU discount; τ→0 →
r0; τ→∞ →θ − σ²/2κ² - Conditional moments (state-independent variance); yield increasing in
r0, long yieldr0-independent - Monte Carlo:
E_Q[exp(−∫r ds)] == P(0,T)(exact Gaussian transition, honest 4σ band)
- Affine price with
γ = √(κ²+2σ²)and the(κ+γ)grouping (the long yield2κθ/(κ+γ)is the cleanest place that grouping is checked) - State-dependent variance (the
r0term — not Vasicek's), cross-checked against the exact noncentral-χ² sampler's empirical moments - Feller
2κθ ≥ σ²⟺ strictly positive paths, transition df ≥ 2 - σ→0 → the deterministic OU discount (O(σ²), scale-aware with a roundoff floor)
y = −ln P/τround-trip; analytic forward vs central-difference (O(h²)); par-yield convention- Calibration: a loose-tolerance parameter round-trip (the Jacobian is ill-conditioned, cond ~1.5e3) and a tight-tolerance fitted-yields identity at the global minimum
- Put-call parity for bond options (call & put computed independently, so it's a real test); no-arbitrage sandwich; vega-monotonicity
PYTHONPATH=. python examples/yield_curves.py # Vasicek vs CIR term structures
PYTHONPATH=. python examples/affine_unification.py # one ODE reproduces both closed forms
PYTHONPATH=. python examples/bond_option_and_mc.py # bond option + parity + MC checkaffine_unification.py output (the closed form IS the ODE solution):
CIR: tau closed form affine ODE rel err
0.5 0.979077103015 0.979077103015 3.8e-14
1 0.956751217294 0.956751217294 1.7e-13
5 0.770281316614 0.770281316610 5.1e-12
bond_option_and_mc.py (parity to a hair, MC inside the band):
put-call parity: call - put = 0.03672898
P(0,T_B) - K P(0,T_O) = 0.03672898 (residual 1.4e-17)
T=5: MC = 0.822766 +/- 0.000146 closed = 0.822763 z = +0.02
This repo was built with an adversarial multi-agent workflow before implementation:
- Design panel — three independent agents proposed the modules and identities from different angles (Vasicek-first, CIR-first, affine-&-testing-first).
- Synthesis — one canonical spec: 48 identities, precise signatures, 18 analytically-derived golden vectors.
- Adversarial verification — one agent per identity tried to refute it.
4 of 48 were corrected before any code: the MC honest-band trap, the CIR
σ→0 roundoff floor, the calibration global-minimum qualifier, and the
non-sign-definite
dCall/dT_B(replaced by vega). - Implementation against the verified spec — overflow-safe
e^(−γτ)forms,_ln_Ain log space,expm1/log1pthroughout. - Adversarial code review — reviewers per dimension (math, numerical, API, tests), findings verified before applying.
- v0.2.0 — Hull-White (time-dependent θ(t)) exact fit to an initial curve
- v0.2.0 alt — CIR bond options (noncentral-χ² / Jamshidian on the CIR tree)
- v0.3.0 — two-factor affine (G2++ / CIR2) and the affine ODE system
- v0.3.0 alt — market-price-of-risk: the P-measure drift and term premia
credit-lab— the CIR process here is its stochastic-intensity engine; same affine machinery, hazard instead of ratefixedalt-lab— deterministic term-structure bootstrapping; this lab adds the stochastic dynamicstvm-lab— the discounting spinemonte-carlo-lab,pde-lab— the simulation/PDE siblings the MC check and affine PDE connect to
- Vasicek, O. (1977). An Equilibrium Characterization of the Term Structure. JFE.
- Cox, J., Ingersoll, J., & Ross, S. (1985). A Theory of the Term Structure of Interest Rates. Econometrica.
- Duffie, D. & Kan, R. (1996). A Yield-Factor Model of Interest Rates. Math. Finance. (Affine framework.)
- Brigo, D. & Mercurio, F. (2006). Interest Rate Models — Theory and Practice. Springer.
MIT.