Skip to content

Repository files navigation

Heston Model — Fourier Pricing & Market Calibration

Calibrating Heston to a single expiry produces five confident numbers the data does not contain. This shows exactly how badly, and what fixes it.

The interface

Live demo: https://codeebytee.github.io/03-heston-fourier-calibration/ (enable Pages: Settings → Pages → main /docs)

Run the interface locally: clone the repo and open docs/index.html — double-click it. No server, no install, no internet connection. requirements.txt is only needed to rerun the research.

Python NumPy SciPy Plotly Tests License


Headline result

Fitting Heston to one 14-day SPY smile, 54 of 60 random restarts land within 2% of the best loss — a gap far inside any quote's bid–ask spread. Among those 54 indistinguishable fits:

κ θ κθ σ_v ρ
one expiry 0.31 – 11.38 (37×) 0.005 – 0.299 (60×) 441× 1.42× all railed at −0.98
six expiries 1.76 – 2.68 (1.5×) 0.058 – 0.072 (1.2×) 1.2× 1.04× −0.7356 – −0.7283

Parameter sets whose mean-reversion speed differs by a factor of 37 reproduce the same 161 market quotes to within 0.5 vol points of each other. A desk hedging vega with a κ re-fit each morning from the front month is hedging noise. The term structure — not more optimiser effort, not cleaner data — is what buys identification.

What this does

  • Prices European options under Heston three independent ways: COS (Fang & Oosterlee), Carr–Madan FFT, and Andersen QE Monte Carlo. All in the branch-correct "little Heston trap" formulation. The three agree to 8.9e-4.
  • Calibrates to a live SPY chain (2,126 listed contracts → 901 usable) with differential evolution then L-BFGS-B on a vega-weighted objective, and reports the Feller condition rather than clamping it.
  • Measures how much to trust the answer — 60-restart identifiability study, 2-D loss surfaces, and per-maturity error decomposition showing where the model genuinely fails.

Key numbers

Calibrated to SPY as of 2026-08-07 (901 quotes, 6 expiries, 11.7 s):

κ = 2.049   θ = 0.0662   σ_v = 0.980   ρ = −0.730   v₀ = 0.0203
RMSE 2.03 vol points   |   Feller ratio 0.282  →  VIOLATED

Fit quality is entirely a maturity story: 0.44 vol points at 327 days, 2.94 at 14 days with the wings off by 10.5. Heston cannot make a two-week smile steep enough at any admissible parameters — ρ rails at the −0.98 bound and still misses. That is the diffusive-model failure that motivates jumps, measured rather than asserted.

Install and run

pip install -r requirements.txt
python scripts/make_results.py      # full study, ~4 min → results/
python scripts/build_frontend.py    # regenerates docs/data.js

Everything the page shows comes from docs/data.js; the HTML is never hand-edited with numbers in it. python -m pytest runs 46 tests in ~80 s.

Repo map

docs/index.html        the whole interface, single file, opens from file://
docs/data.js           generated by scripts/build_frontend.py — never hand-written
src/models/heston_cf.py    characteristic function (g₂ / little-trap-safe form)
src/models/cos.py          COS pricer + the cached-grid CosPricer (6.8× faster)
src/models/carr_madan.py   FFT pricer + moment-explosion-safe damping
src/models/simulation.py   Andersen QE Monte Carlo
src/models/calibration.py  objective, DE→L-BFGS-B, multi-start study, loss surfaces
src/data/                  chain fetch, quote filters, parity forward
scripts/make_results.py    runs the study → results/heston_results.json + CSVs
tests/                     46 tests; every test name is a claim
notebooks/                 one generated research notebook, no stored outputs

New to this? Start with PREREQUISITES.md — no finance assumed. Then the app, then DEEP_DIVE.md for the equations, the numerical traps, and the validation evidence.

Design decisions

  • COS runs the inner loop; the FFT exists to disagree with it. Carr–Madan prices a whole ladder per FFT but on its own grid, so real strikes need interpolation and α has to be retuned per maturity against a moment-explosion boundary. COS prices the exact strikes asked for and converges geometrically — 0.17 ms vs 0.68 ms vs 9.9 s for Monte Carlo. Keeping all three is what turns "my pricer agrees with itself" into actual validation.

  • The truncation grid is pinned across the parameter box. In cos_price the COS interval depends on the parameters through the cumulants, so the payoff coefficients are rebuilt every call — ~90% of the runtime. CosPricer fixes the interval once, as the union over the 32 corners of the calibration box, so it is never narrower than a per-call range would have been. Measured: 12.1 ms → 1.8 ms per objective evaluation. That is the difference between this study taking 4 minutes and 30.

  • Report the Feller violation instead of enforcing it. Constraining 2κθ ≥ σ_v² would refuse to fit the skew the market actually shows. The fitted ratio is 0.28, so the variance genuinely visits zero — 13.8% of simulated (path, step) pairs sit at zero — which is why simulation uses Andersen's QE rather than Euler with a truncation patch.

  • Everything expensive is precomputed; everything cheap runs live. The COS pricer is ported to JavaScript, so dragging a slider re-prices the whole strike ladder from the characteristic function rather than interpolating a lookup table. It matches the Python library to 1e-13. Calibration output, the identifiability study and the loss surfaces are precomputed and labelled as such.

Data

SPY option chain, yfinance, snapshotted 2026-08-07 03:26 UTC and committed to data/chain_snapshot.json so the repo reproduces with no network. If the fetch fails, the code falls back to a deterministic synthetic chain generated from known Heston parameters — which doubles as the calibrator's ground-truth test. No API keys anywhere.

License

MIT — see LICENSE.

About

Heston Fourier pricing (COS + Carr-Madan FFT) and market calibration to a live SPY chain, with a 60-restart study showing the parameters are not identified from a single expiry

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages