Quant project · derivatives

Implied Volatility Surface Builder

Take a real options chain, work backwards from prices to the volatility the market is pricing at every strike and expiry, fit an arbitrage-aware surface, and see the smile and skew that Black-Scholes assumes away.

1,528of 3,701 SPY contracts survive cleaning
10expiries fitted, as of 2026-10-26
0.16 ptsmean fit error on market quotes
0butterfly / calendar arbitrage violations

Why this matters

Options traders quote volatility, not price. Black-Scholes assumes one volatility for every strike; the market disagrees loudly.

On 2026-10-26 the SPY at-the-money implied vol was about 14.8% at the shortest expiry and 17.1% at the longest, while far out-of-the-money puts at the front of the curve priced in as much as 59%. Index crash risk is what that skew is paying for. The red dashed line on every panel below is the flat volatility Black-Scholes would use.

Volatility smiles by expiry with SVI fit and the flat Black-Scholes volatility

The surface

Interactive. Drag to rotate, scroll to zoom. Black dots are the market quotes; the surface is the fitted SVI. Regions outside the observed strikes are masked rather than extrapolated.

The skew is steepest at short maturities and flattens with time; the term structure of at-the-money vol is upward sloping, with a bump around the first two expiries.

ATM term structure, ATM skew and what the cleaning stage dropped

How it works

Five stages, each with its own module and tests.

1 · Clean, and show your work

Crossed, one-sided, wide-spread, penny and statistically odd quotes are removed. Every dropped row is kept with its reason so nothing disappears silently.

2 · Forward from put-call parity

F = K + (C - P)/DF near the money, so dividends and borrow costs are read from the market instead of assumed.

3 · Invert Black-76

Out-of-the-money options only, solved with vectorised bisection. Quotes violating static no-arbitrage bounds are rejected instead of producing nonsense vols.

4 · Constrained raw SVI

Zeliade's quasi-explicit reduction turns each slice into a small linear least-squares problem, with positivity, Lee wing bounds and calendar spreads as linear constraints, plus a butterfly check and repair.

What the cleaning stage removed

FilterRows
not otm1,113
short dated477
no quote221
low price131
far otm117
wide spread113
iv outlier1

"Not otm" is not data loss: each strike contributes its out-of-the-money leg only, so the in-the-money twin is intentionally skipped.

Per-expiry fit and arbitrage audit

ExpiryDaysQuotesRMSE (vol pts)Max err (vol pts)No butterfly arbNo calendar arb
2026-11-0491040.2771.16yesyes
2026-11-06111270.2841.21yesyes
2026-11-13181460.2301.07yesyes
2026-11-27321370.0990.34yesyes
2026-12-04391290.0820.27yesyes
2026-12-31663140.1230.66yesyes
2027-01-29951800.1081.04yesyes
2027-03-191441100.1030.29yesyes
2027-06-172341250.1180.26yesyes
2027-09-303391560.1330.36yesyes

Validation against a known truth

A fit that matches the market proves little if the market is noisy. So the pipeline is also run on a chain generated from a known arbitrage-free SSVI surface, with bid/ask noise, tick rounding and injected bad quotes.

Recovered surface versus true surface on a synthetic chain

Across every slice, inside the observed strikes, the recovered vols are within 0.18 vol points of the truth (RMS 0.03). This is enforced as a test, not just plotted.

Cross-check against svi-py

An independent implementation, run on exactly the same cleaned quotes and forwards. Fit quality, butterfly and calendar checks are computed by a separate script, not by either library.

Fit error of this project versus svi-py, default and tuned
CalibratorMean RMSE (vol pts)Worst error (vol pts)Slices w/ butterfly arbCalendar violationsCurve gap vs mine (vol pts)
this project0.161.21000.00
svi-py default1.159.806451.04
svi-py default + no-butterfly1.458.370231.27
svi-py tuned0.151.04010.06
svi-py tuned + no-butterfly0.171.02000.11
Reading it honestly. With its default settings svi-py averages 1.15 vol points of error, because it minimises unweighted total-variance error, which neglects the at-the-money region. Switched to its own implied_vol objective with multi-start, it reaches 0.15, essentially level with this project's 0.16, and the fitted curves agree to about a tenth of a vol point. Two independent calibrators converging is the real result. The differences are that this one enforces calendar spreads by construction, and that it is about 15–20× faster than svi-py's tuned configuration (roughly 0.09 s versus 1.5–2 s per slice); svi-py's untuned default is faster still (about 0.01 s) but is the inaccurate one.

Run it

pip install -e ".[dev]"
python -m ivsurface SPY            # live chain from Yahoo Finance
python -m ivsurface --synthetic    # offline, known ground truth
pytest                             # 13 tests

Limitations

SPY options are American-style but priced here with Black-76. Using only out-of-the-money options limits the error, but a European index such as ^SPX is the clean choice.

Free Yahoo data is delayed; this snapshot was taken after the close. The discount rate is a flat constant used only for discounting.

Arbitrage checks cover the observed strike range plus a small margin. Beyond it the surface is extrapolation, and is masked in the 3D view.