Apr – Jun 2026
SPY volatility surface research
Options are quoted in implied volatility, and a volatility model is only useful if it can fit the smile the market shows. I built a Python pipeline that cleans a raw SPY option chain, builds an implied-volatility surface, checks it for static arbitrage, and calibrates two standard models, SABR and Heston, to the same quotes. The question was how well each fits very short-dated SPY options, and where the fits break.

Data and filtering
The study uses one saved yfinance snapshot, retrieved on Sunday 2026-07-12 at 20:17 UTC, so the quotes are from the prior session, and yfinance is not vendor-certified. Of 1,169 raw rows, 957 survive cleaning, and the out-of-the-money surface has 595 quotes across six expiries, all within eight calendar days. Static-arbitrage checks flag 138 violations; they flag them, they do not repair them. The calibration set drops expiries under two trading days and keeps quotes with relative spread at most 0.25 and nonzero volume and open interest: 273 quotes across four expiries, 3 to 8 calendar days out.
An expiry-clock error
Time to expiry was first measured to 00:00 UTC on the expiry date instead of the 16:00 New York close, which shortened every maturity by 20 hours. After the fix on 2026-10-12, with every output regenerated, SABR's RMSE on the shortest expiry (2026-07-13, full surface) fell from 0.364 to 0.039, and much of an earlier diagnosis that ultra-short expiries are unstable turned out to be this clock convention.
Results
Median per-expiry SABR RMSE is 0.0143 in implied volatility on the full 595-quote surface and 0.0060 (about 0.60 vol points) on the 273-quote set. That is the same model fit to two different quote sets, not a model improvement: of the 0.0083 gap, 0.0024 comes from dropping the two shortest expiries and 0.0059 from dropping wide-spread or untraded quotes. The filter was chosen on this same snapshot, and every fit is in-sample.
On the 273 quotes, SABR has lower RMSE than both a global and a per-expiry Heston fit on all four expiries, but the Heston fit is numerically fragile here. Moving from 32 to 96 quadrature nodes shifts its implied volatilities by 0.049 on average, refitting at 96 nodes gives a global RMSE of 0.0676 instead of 0.1082, and at 192 nodes the pricer overflows. SABR stays lower either way, but the size of the gap is not reliable. A synthetic recovery test passed at 0.05 to 0.25 year maturities, which rules out a gross pricer bug there but does not explain the underfit at 3 to 8 days.
Limits
There is one usable snapshot: four later pulls failed quality checks as near-duplicate stale quotes, so nothing here is cross-date or out-of-sample evidence. There are no monthly or longer expiries. The rate is a flat 5%, and American exercise is not modeled. There is no trading signal, hedging or P&L test.
Reproduce it
The saved yfinance snapshot is committed in data/snapshots/, so no download is needed. pytest regenerates some tracked files in place; on another machine floating-point digits can change without changing any conclusion.
python -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
python scripts/generate_stage3_real_snapshot_outputs.py
python scripts/generate_stage4_sabr_outputs.py
python scripts/generate_stage4_5_sabr_robustness_outputs.py
python scripts/generate_stage5_heston_outputs.py
python scripts/generate_stage5_5_heston_improvement_outputs.py
python scripts/generate_cross_date_outputs.py
python scripts/generate_final_report_outputs.py
python -m pytest