Skip to content

Repository files navigation

Implied Volatility Surface Construction & SVI Calibration

Fit an SPY option chain to an implied volatility surface that is arbitrage-free by theorem, and measure exactly what that guarantee costs you in fit quality.

The interface in use

Python NumPy SciPy Plotly Tests License

Live demo: https://codeebytee.github.io/02-vol-surface-svi/ (enable Pages: Settings → Pages → main /docs)

Run the interface locally: clone the repo and open docs/index.html in any browser. No install, no server, no internet — Plotly is vendored in docs/vendor/. requirements.txt is only needed to re-run the research.

What this does

  • Turns a raw listed option chain into clean implied vols. Nine SPY expiries from 7 days to 1.9 years, forward and discount factor implied from put–call parity rather than assumed, quotes filtered through a seven-stage funnel, then inverted to IV with Brent over a branched rational initial guess.
  • Calibrates two models and puts them head to head. Raw SVI fitted per slice via the Zeliade quasi-explicit scheme (outer Nelder–Mead over (m, σ), inner constrained linear least squares), and SSVI fitted globally with a power-law φ(θ) whose parameter bounds make the no-arbitrage theorems hold by construction.
  • Checks for static arbitrage instead of assuming it away. Butterfly via the Durrleman function g(k) scanned directly on a 241-point grid, plus the sufficient conditions; calendar via total-variance crossing. The page shows the violations in the raw chain and the fit that removes them.

Headline result

Fitting each expiry independently is 3.1× more accurate and completely unusable:

Model Free parameters RMSE (vol points) Butterfly-free Calendar-free
Per-slice raw SVI 45 1.25 no — 3 of 9 slices no — 165/1928 grid cells
Global SSVI 12 3.86 yes yes

Three of the nine independently-fitted slices imply a negative probability density inside the quoted strike range, and the fitted slices cross in total variance in the wings — a calendar spread with a negative price. SSVI gives up 2.6 volatility points of fit and returns a surface you can actually differentiate for local vol or hand to a Monte Carlo. That trade-off is the result, and neither number is the "right" answer; the choice depends on whether you are marking a book or pricing an exotic off the surface.

The raw chain itself contains 649 negative-cost butterflies. This is not free money — the prints in one expiry were not observed at the same instant, and a non-simultaneous price set violates convexity for mechanical reasons. It is the argument for imposing no-arbitrage on the fit rather than inheriting it from the data.

Install and run

pip install -r requirements.txt      # 1. research dependencies (interface needs none)
python scripts/build_frontend.py     # 2. re-runs the whole pipeline, writes docs/data.js
pytest -q                            # 3. 92 tests

build_frontend.py uses the committed chain snapshot in data/, so it is fully offline and reproducible. Pass --refresh to pull a fresh chain from yfinance instead.

Repo map

src/models/     black.py (reflection-stable Black-76), implied_vol.py (Brent + rational guess),
                svi.py (quasi-explicit calibration), ssvi.py (global fit), arbitrage.py
                (Durrleman g, density, calendar), surface.py
src/data/       option_chain.py (yfinance → cache → synthetic fallback), filters.py
                (the seven-stage funnel), forward.py (parity regression)
scripts/        build_frontend.py (regenerates docs/data.js), make_results.py, make_gif.py
docs/           index.html — the whole interface, one file; data.js — generated, never hand-edited
tests/          92 tests including JS/Python parity checks on the ported math
results/        model_comparison.csv, svi_fits.csv, synthetic_recovery.csv, figures/
notebooks/      vol_surface_research.ipynb — the story, importing from src/
config.yaml     every tunable number in the project

New to the topic? Start with PREREQUISITES.md. Already know the finance and want to judge the work? DEEP_DIVE.md.

Design decisions

  • Arbitrage-free by parameterisation, not by post-hoc repair. The SSVI bounds on (ρ, η, γ) are chosen so Gatheral–Jacquier's sufficient conditions hold for every point the optimiser can reach. The alternative — fit freely, then project onto the admissible set — is easier to code and gives no guarantee at all about the region between your knots. The cost of the honest version is a visible 2.6 vol points, reported rather than hidden.
  • The forward is measured, not assumed. Every slice gets its own (F, D) from a put–call parity regression over the near-the-money strikes. Assuming a rate and a dividend yield instead pushes the entire error into skew: a forward that is wrong by 0.3% looks exactly like a smile that is tilted, and you would then calibrate ρ to your own bad guess.
  • The split between Python and JS follows the cost of the math, not convenience. Calibration, inversion and the arbitrage scan are expensive and run in Python; the fitted surface, the smile, g(k) and the implied density are closed-form in the SVI parameters, so they are ported to JS and recompute live on every slider move — including the five raw-SVI sliders in the Slice Explorer, which let you break the surface yourself and watch the density go negative. tests/test_js_parity.py pins the two implementations to each other so the port cannot silently drift.

Honesty notes

All nine expiries in the committed snapshot were built from recent traded prints rather than two-sided quotes, because the free feed publishes a zero book outside regular trading hours. Prints are worse data than quotes — not simultaneous, not necessarily at mid — and they inflate the apparent arbitrage count. Every slice is labelled on the page with the price source it used. The 1.25-vs-3.86 comparison is unaffected since both models see the same quotes, but absolute error would be lower on a live book. SPY options are also American, and the ignored early-exercise premium biases deep put IVs slightly upward; SPX would remove this. Both points, and five more, are in §7 of DEEP_DIVE.md.

License

MIT — see LICENSE.

About

Implied volatility surface from a live SPY chain: robust IV inversion, per-slice SVI vs globally arbitrage-free SSVI, with Durrleman butterfly and calendar checks. Interactive, offline-capable.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages