A two-factor additive Gaussian (G2++) interest-rate model from first principles in pure Python + NumPy/SciPy. The two correlated Ornstein–Uhlenbeck factors, the convexity functional and exact curve fit, zero-coupon bond options, caps/floors, and — the centrepiece — a European swaption priced three independent ways that agree. Every formula is derived from the SDEs; no rates library underneath.
67/67 tests verifying algebraic identities — the exact curve fit, the forward-bond martingale that pins the measure change, put-call and payer-receiver parities, the one-factor (Hull–White) collapse, and the flagship 1-D == 2-D == Monte-Carlo swaption agreement — never a comparison to QuantLib or any library.
$ pytest tests/
=========================== 67 passed in 21.0s ============================
Every identity in this repo was adversarially verified by a multi-agent workflow before a line of code was written — 60 claims were independently checked and 10 had errors corrected (the
B(z,τ)→τlimit needs an exactz=0branch, not a finite-ztest pretending to hit 1e-12; the bond-option vega is sign-definite only in the totalΣ, not inσ/ηindividually — withρ<0the cross term can flipdΣ²/dσnegative; cap/floor and swaption strike monotonicity is non-strict deep OTM where prices underflow; theVsmall-aseries is mandatory — the naïve closed form returns exactly 0.0 ata=1e-6; and every Monte-Carlo identity is an honest 4σ band, never 1e-12). A second adversarial code review then surfaced 16 findings (3 medium, 4 low, 9 nit), all verified by running the code and applied — including a V cross-block that cancelled to a negative variance at tiny mean-reversion speeds, and a swaption quadrature box that silently truncated deep-OTM payoffs. See "How this was built".
Two-factor rate models are where the textbook closed forms stop being one-liners
and most people reach for a library. This repo is the opposite. The short rate is
r = x + y + φ(t) with x, y correlated Gaussian factors, and:
- the bond price
P(t,T) = P^M(0,T)/P^M(0,t) · exp(A)reprices today's curve exactly (P(0,T) == P^M(0,T)to the last bit) — the defining property of the model, tested directly; - the riskiest piece of the whole model — the change to the
T-forward measure — is pinned by an identity, the forward-bond martingaleE^{Q^T}[P(T,S)] = P^M(0,S)/P^M(0,T), so a sign slip inμ_x/μ_yfails a test; - and the swaption, which has no Jamshidian decomposition in two factors, is priced by the Brigo–Mercurio 1-D semi-analytic integral and proven by an independent 2-D quadrature and a full path Monte Carlo.
It extends the rates pillar of the portfolio one factor further: the v0.3.0
roadmap item that shortrate-lab
left open ("two-factor affine / G2++") realised.
| Module | Topic | Headline functions |
|---|---|---|
factors.py |
the Gaussian factor layer | G2Params, B, cond_var_x/y, cond_cov_xy, chol2x2, V, phi, qt_moments |
curve.py |
market curve P^M(0,·) |
ZeroCurve, FlatCurve, QuadraticForwardCurve, NelsonSiegelCurve |
bond.py |
curve-consistent bond | bond_A, bond_price, short_rate, ahat |
options.py |
ZCB options | option_Sigma, option_variance, zcb_call, zcb_put |
capfloor.py |
caps/floors + swap | caplet, floorlet, cap, floor, forward_swap_rate, swap_value |
swaption.py |
the flagship | swaption_bm, swaption_quad2d, swaption_mc, exercise_boundary |
simulate.py |
exact-OU Monte Carlo | simulate_factors, mc_discount_factor, mc_bond_price |
from g2pp import factors, curve, bond, options, swaption
p = factors.G2Params(a=0.5, b=0.1, sigma=0.01, eta=0.008, rho=-0.7)
crv = curve.QuadraticForwardCurve(c0=0.03, c1=0.005) # f^M(0,T) = 3% + 0.5%·T
bond.bond_price(p, crv, 0.0, 5.0, 0.0, 0.0) # 0.808560316 == P^M(0,5) exactly
options.zcb_call(p, crv, 0.0, 1.0, 3.0, K=0.9) # 0.022391993 (put-call parity to 6e-17)
# the flagship: a 5y->2y payer swaption, priced two analytic ways + Monte Carlo
T, pay, acc, K = 5.0, [5.5, 6.0, 6.5, 7.0], [0.5]*4, 0.04
swaption.swaption_bm(p, crv, T, pay, acc, K) # 0.0314287103 (Brigo-Mercurio 1-D)
swaption.swaption_quad2d(p, crv, T, pay, acc, K) # 0.0314287103 (brute 2-D; rel 1e-13)67 tests, every one an identity the formula must satisfy. Highlights:
P(0,T) == P^M(0,T)exactly (diff0.0) on flat and non-flat curves — the model fits today's curve by construction (A(0,T)=0)- the forward-bond martingale
E^{Q^T}[P(T,S)] == P^M(0,S)/P^M(0,T)(rel1e-16) — the closed-form lognormal expectation that pins the sign of everyQ^Tdrift; any flip inμ_x/μ_ybreaks it Σ²recomputed two independent ways (the variance-of-log-forward-bond decomposition and the verbatim1/a³Brigo–Mercurio form)
swaption_bm == swaption_quad2dto~2e-13— the 1-D semi-analytic reduction equals a brute 2-D adaptive quadrature with no shared price algorithm, so their agreement proves the reduction (there is no Jamshidian in two factors); holds even deep OTM (price~1e-40) via a tail-aware boxswaption_mcwithin a 4σ band of the analytic price (full risk-neutral paths, exact OU steps)- payer − receiver == forward swap value (model-free, rel
1e-12)
- put-call parity for ZCB options (call & put from independent
N(·)forms, residual6e-17); cap − floor == swap; par rate zeroes the swap - HW1F collapse: with
η=ρ=0they- and cross-blocks ofVandΣ²are literally0.0, so the model reduces term-by-term to one-factor Hull–White - the four numerical limits:
B(z,τ)→τ(exactz=0branch), theVsmall-speed Taylor series (self- and cross-blocks),Σ→0intrinsic, theχ-freechol2x2at|ρ|=1 - exact-OU Monte Carlo:
E_Q[e^{-∫r}] == P^M(0,T)and the tower property, honest 4σ bands across multiple seeds
PYTHONPATH=. python examples/yield_and_curvefit.py # P(0,T)=P^M(0,T), phi, the 2-factor correlation
PYTHONPATH=. python examples/bond_option_and_caps.py # ZCB option parity + cap/floor = swap
PYTHONPATH=. python examples/swaption_three_routes.py # 1-D vs 2-D vs Monte Carlo (FLAGSHIP)swaption_three_routes.py (three methods, one number):
5y->2y payer swaption, semiannual, fixed 4%
route price vs BM
Brigo-Mercurio 1-D 0.0314287103 --
brute 2-D quadrature 0.0314287103 1.3e-14 (rel)
path Monte Carlo 0.0314037683 z = -1.10 (+/-2.3e-05)
payer - receiver = +0.03129165
forward swap = +0.03129165 (model-free; residual 1.3e-16)
The portfolio's full adversarial multi-agent workflow, before and after implementation:
- Design panel — three independent agents proposed the modules and identities from different angles (factors-&-curve-first, bonds-options-caps, swaption-&-testing-first).
- Synthesis — one canonical spec: 60 identities, precise signatures, golden vectors derived from the math.
- Adversarial verification — one agent per identity tried to refute it,
checking numerically against Monte Carlo / quadrature. 10 of 60 were
corrected before any code (tolerance honesty, the vega sign qualification,
the
BandVlimit branches, the deep-OTM monotonicity caveats). - Implementation against the verified spec —
expm1/log1pthroughout, dedicated Taylor branches for the cancellation-proneVblocks, the bond price in log space, the swaption boundary by a bracketed root-find. - Adversarial code review — reviewers per dimension (math / numerical / API /
tests), 16 findings all verified by running the code and applied: the
Vcross-block needed its own Taylor branch (it cancelled to a negative variance at tiny speeds — the self-blocks were guarded, the cross-block was not); theswaption_quad2d±8σ box silently truncated deep-OTM exercise regions (made tail-aware);dblquad's absolute floor replaced by pure relative control; plus API/test-honesty polish.
- v0.2.0 — calibration to a cap/swaption vol surface (the analytic prices are all here; what's missing is the optimiser and a market-data adapter)
- v0.2.0 alt — the
x/y↔ principal-components reparametrisation and theP-measure drift / term premia - v0.3.0 — Bermudan swaptions (the 2-D exercise region is already the hard part; this adds backward induction on a lattice/regression)
- v0.3.0 alt — CIR2 / multi-factor affine via the Riccati ODE system (the
shortrate-labaffine spine, lifted to two factors)
shortrate-lab— the one-factor parent (Vasicek/CIR + the affine framework); G2++ is its v0.3.0 roadmap item, and theη=ρ=0collapse here reproduces its Hull–White limitcredit-lab— the same Gaussian machinery drives a hazard-rate intensity instead of a short ratetvm-lab,fixedalt-lab— the discounting spine and the deterministic term structure this adds dynamics tomonte-carlo-lab— the simulation siblings the exact-OU MC cross-checks connect to
- Brigo, D. & Mercurio, F. (2006). Interest Rate Models — Theory and Practice, ch. 4 (the G2++ model). Springer.
- Hull, J. & White, A. (1994). Numerical Procedures for Implementing Term Structure Models II: Two-Factor Models. J. Derivatives.
- Nelson, C. & Siegel, A. (1987). Parsimonious Modeling of Yield Curves. J. Business. (The smooth test curve.)
MIT.