Skip to content

option_IndexVolBot on real SPY option data (2026-09-29)

Verdict: on real option prices, IndexVolBot is not distinguishable from zero. - 2019–2026: alpha +0.8%/yr, t 0.45. - Since early 2024 (the second half by sampled days): alpha −1.6%/yr, t −1.01.

The synthetic backtest says t 3.23 over the same window. The halves split the sampled days in two; they are denser after mid-2024, so the split falls around January 2024. That result was an artifact of its pricing model, mainly the call skew. The bot's live rules (OptionIndexVolBot.RULES) are unchanged in this test.

Data

The DoltHub post-no-preference/options backfill in option_quotes covers SPY from 2019-05-10 to 2026-09-25, 1,177 days. It is a daily sample, not a full chain: - ~3 expiries a day, nearest to ~14, ~30 and ~45–65 days; - ~22 strikes each; - Mon/Wed/Fri only until mid-2024.

The sample changes daily. A condor leg the bot opened was quoted again on about 1 day in 20, and its expiry on about 1 day in 4.

A plain replay (onetime_option_replay_backtest.py) therefore marks a held leg at its last recorded quote, which is usually the entry price. That gave t 0.26, a number that measures the gaps rather than the strategy.

Method: a per-day surface

utils/option_surface.py fits each day's vol surface from whatever that day recorded: - IV: solved from the mid with the bots' own r and q, on the out-of-the-money side only. - Moneyness: standardised as ln(K/F) / T^0.35. - Within an expiry: IV is linear in moneyness. - Across expiries: total variance is linear in T. - Spread: the median of the day's quotes nearest in price.

SurfaceReplayMarket (in utils/option_replay.py) quotes any contract the day did not record off that surface. With live_expiries=True, the chain the strategy enters is the one the live bot would see: the first Friday at least 35 days out, on SPY's $1 strike grid.

How accurate the surface is

The test hides one expiry per day, predicts it from the rest, and scores the prediction against the recorded mids (SPY, 197 sampled days, 15k quotes). Figures are median vol points.

Case Delta 5–15 bias Delta 5–15 abs. error Wings <5 delta bias Delta 15–70 bias
Interpolated (30-day held out) +0.15 0.34 −0.31 +0.05 to +0.16
Extrapolated (14-day held out) +0.54 0.88 −1.19 +0.18 to +0.42

The exponent 0.35 was chosen on this test. At 0 (fixed ln(K/F)), near-expiry wings came out 4.8 points too cheap. At 0.5, the 5–15 delta options came out 1.1 points too rich.

AAPL was not used to choose it. It interpolates as well (bias within ±0.5 points out of the money). Extrapolation there runs 1–3 points low, plausibly from earnings bumps in its term structure.

A short wing marked slightly cheap flatters a short-vol book. The real result is therefore not better than shown.

Result: 2019-05-10 → 2026-09-25, live rules, $100k

Pricing Trades Win rate Net P&L Alpha/yr t Beta Max DD
Synthetic (^VIX × 0.85, calibrated skew) 32 100% +$22.5k +2.6% 3.23 0.01 −2.3%
Real surface, live expiries: full 40 88% +$4.6k +0.8% 0.45 0.01 −6.2%
Real surface: H1 (2019-05 → ~2024-01) +3.0% 0.89 0.00 −6.2%
Real surface: H2 (~2024-01 → 2026-09) −1.6% −1.01 0.03 −4.5%
Real surface, sampled expiries (42–65 DTE) 23 87% +$0.8k +0.3% 0.16 0.00 −8.1%

The synthetic book never lost. Every one of its 32 condors reached the 50% take-profit, most within 1–3 weeks.

On real prices, 35 winners averaging +$660 are erased by 5 losers.

Opened Loss What happened
2020-05-13 −$4.6k The May–June 2020 rally, through the short call.
2020-07-27 −$2.7k The August 2020 rally; the condor was still under water at 7 DTE.
2022-07-13 −$3.4k The July–August 2022 bear-market rally, call side.
2022-09-12 −$3.4k The September 2022 selloff, put side.
2025-03-24 −$4.6k The April 2025 tariff crash, put side.

Why the synthetic backtest was wrong: the call skew

The synthetic prices options as ATM × (1 − skew × z), with the put and call skews calibrated on one live chain, 2026-09-25, a calm day. Across 2019–2026, real upside wings are much cheaper than that. The strike the bot picks at 10 delta therefore sits closer to spot.

Median distance of the short strikes from spot at entry:

Short call Short put
Synthetic 9.0% 11.6%
Real surface 7.1% 11.3%

A 2-point-closer short call is what the V-shaped recoveries of 2020 and 2022 ran through. Put placement matched.

The entry signal itself holds up on real data. Real 1-month SPY ATM IV divided by ^VIX has a median of 0.84 over 1,177 days, against the 0.85 the synthetic assumes. It ranges from 0.75 to 0.90 by year. The real gap signal fires in the same episodes as the synthetic one. The strategy fails on what it sells, not on when it sells.

Caveats

  • Fills are model fills. 98% of the quotes the replay needed came off the surface, and the spread is modelled from the day's recorded spreads. What is real is the level, skew and term structure of SPY's vols each day.
  • Fewer days before mid-2024. The replay steps only on sampled days (Mon/Wed/Fri until mid-2024), so exits can be a day or two late.
  • Seven years of history. The recalibration below extends the test to 2000–2026.

Recalibration: the synthetic model fitted to the real chains

A per-day, per-expiry (28–66 DTE) fit of the synthetic model's own smile, iv = ATM × (1 − skew × z) with z = ln(K/S)/(ATM·√T), on 2,384 real SPY day-expiries:

Parameter One-day fit (2026-09-25) Real median, 2019–26 By VIX level
ATM IV / VIX 0.85 0.87 flat: 0.855–0.91
Put skew 0.25 0.28 flat: 0.24–0.29
Call skew 0.08 0.23 0.11 at VIX < 13; 0.29 at VIX 20–30; 0.40 at VIX > 40

The call skew rises with the vol level. It is fitted as 0.284 + 0.246 · ln(ATM / 0.20), capped at 0.40, with the same form in both halves of the sample. The bot sells when IV is rich, usually at VIX 20–35. There, real call wings are priced at about 4× the skew the model assumed.

Which model reproduces the real replay (2019-05 → 2026-09, live rules)

Model t Win rate Net P&L Short call OTM Short put OTM
M0: one-day fit (0.85 / 0.25 / 0.08) 3.23 100% +$22.5k 9.0% 11.6%
M1: real medians, constant (0.87 / 0.28 / 0.23) 1.07 91% +$8.3k 7.7% 12.5%
M2: call skew rising with vol 1.24 88% +$7.7k 6.8% 12.3%
Real surface replay 0.45 88% +$4.6k 7.1% 11.3%

M2 is the synthetic default for SPY now (UNDERLYINGS in onetime_index_vol_backtest.py; --calibration original reproduces the old numbers). It still flatters the strategy a little against real prices: puts 1 point further out, t 1.24 against 0.45.

2000–2026 on the calibrated model

Live rules, 2007–2026 (--decide; the VIX/VIX3M gate needs ^VIX3M, which starts in 2006):

Model / path t (full) H1 H2 Max DD Trades
Original model, decide path 4.13 2.66 3.26 −2.4% 59
Calibrated model, decide path 0.99 0.23 1.29 −9.5% 65
Calibrated model, fast simulator 0.32 −0.03 0.60 −7.7% 59

Walk-forward re-tune on the calibrated model, 2000–2026, split 2013-06-19. The grid has 1,296 parameter sets, including 5-delta short calls, which were added before this run because of the call-skew finding.

Rules 2000–2013 t 2013–2026 t 2013–2026 max DD
Textbook defaults (16-delta, 45 DTE) −0.25 −2.10 −52.7%
#1 of 2000–2013 2.46 0.22 −19.2%
Top-10 consensus (60 DTE, 10% wings, 3-pt gap, no stop) 2.01 0.05 −19.8%
Mean of the top 10 −0.17
Share of the grid positive in 2013–2026 17%

Nothing survives out of sample. No 2000–2013 leader moved the short call out to 5 delta, so pushing the call side further away does not rescue it. The textbook condor is significantly negative in 2013–2026.

Conclusion

The SPY variance premium is real: implied vol exceeds the vol realized over the next 21 days in 66–75% of months. But a 10-delta iron condor that sells it at realistic skew gives it back in rallies and crashes. The earlier evidence for option_IndexVolBot (t 4.13) came from a pricing model that made upside wings too rich. Real prices over 2019–2026, the recalibrated synthetic over 2007–2026, and a walk-forward over 2000–2026 all put it at about zero.

Reproduce

kubectl port-forward -n tradingbots-2025 svc/psql-service 5432:5432 &
POSTGRES_URI=... uv run python scripts/onetime_option_replay_backtest.py --strategy indexvol --end 2026-09-25 --live-expiries
POSTGRES_URI=... uv run python scripts/onetime_option_replay_backtest.py --strategy indexvol --end 2026-09-25 --surface
uv run python scripts/onetime_index_vol_backtest.py --decide                          # calibrated model, live rules 2007+
uv run python scripts/onetime_index_vol_backtest.py --decide --calibration original   # the old numbers
uv run python scripts/onetime_index_vol_backtest.py --tune                            # walk-forward, calibrated