DEV Community

Cover image for We tested 17,100 option-selling strategies on SPY and QQQ
tomasz dobrowolski
tomasz dobrowolski

Posted on Originally published at flashalpha.com

We tested 17,100 option-selling strategies on SPY and QQQ

We tested 17,100 option-selling strategies on SPY and QQQ from January 2022 to October 2026: 6 structures, 6 deltas, 4 expiries, 6 exits and 15 filters based on dealer gamma exposure (GEX) and the volatility risk premium (VRP). This post covers the data, the method, the main result and the code to run the rule.

Summary

  • For same-day strangles, negative-gamma days were 60% to 63% of SPY trades and held 72% to 74% of the dollar losses. On QQQ they were 54% to 60% of trades and 68% to 76% of losses.
  • Selling only on positive-gamma days, all eight strangle setups made money on both ETFs at midpoint fills.
  • QQQ 0DTE strangles at 0.25 delta: Sharpe +0.35 every day, +1.38 on positive-GEX days only.
  • The strongest same-day rule was positive GEX with spot above a certified gamma flip (p = 0.005 on SPY, 0.006 on QQQ, against random calendars).

Gamma exposure

Market makers hedge the delta of the options they hold. When they are net long gamma, hedging means selling rallies and buying dips, which dampens moves. When they are net short gamma, they hedge with the move, which can amplify it. Net GEX aggregates this across the chain. It does not predict direction, only how far the market is likely to move, which is the risk a short strangle carries. The gamma flip is the spot level where net GEX changes sign.

Data

Every number comes from four FlashAlpha REST endpoints, replayed minute by minute on the Historical API with an at timestamp. Live paths are shown. On the Historical API, the quote endpoints sit under /v1/ (/v1/stockquote, /v1/optionquote).

Step Endpoint Fields used
Regime and flip /v1/exposure/gex/{symbol} net_gex, gamma_flip, gamma_flip_status
Spot at the open /stockquote/{symbol} bid, ask, mid
Contract selection and pricing /optionquote/{symbol} type, expiry, strike, bid, ask, mid, delta, open_interest
VRP filters /v1/vrp/{symbol} vrp.vrp_20d

Example replay call:

GET https://historical.flashalpha.com/v1/exposure/gex/SPY?at=2026-10-22T15:55:00
Enter fullscreen mode Exit fullscreen mode

Trade rules

  • Short strangle, 0.10 or 0.25 delta, 0DTE or 1-week expiry.
  • Contract nearest the target delta, open interest of at least 250, spread of at most 20%.
  • Open at 09:35 ET, close at 15:55 ET, one trade per day.
  • $1 commission per contract on every leg, four fill levels: mid, a quarter of the way to bid/ask, halfway, full bid/ask.
  • Regime read from the previous session's 15:55 ET GEX snapshot, so there is no look-ahead.

Losses on negative-GEX days

Share of trades, of dollar losses and of the worst 5% of trades that fell on negative-GEX days:

ETF Strangle Trades Losses Worst 5% p
QQQ 0DTE, 0.10 delta 54% 71% 66% 0.031
QQQ 0DTE, 0.25 delta 55% 68% 67% 0.049
QQQ 1 week, 0.10 delta 59% 76% 81% 0.002
QQQ 1 week, 0.25 delta 60% 75% 77% 0.013
SPY 0DTE, 0.10 delta 60% 74% 71% 0.027
SPY 0DTE, 0.25 delta 63% 72% 76% 0.032
SPY 1 week, 0.10 delta 62% 73% 78% 0.008
SPY 1 week, 0.25 delta 63% 73% 75% 0.027

p is the share of over a thousand calendars, each the negative-gamma calendar shifted by 63 or more sessions, that caught the losses at least as well. All eight are below 0.05.

A minimal version of that test (an illustration, not the study's code):

import numpy as np

def shift_test(pnl, flagged, min_shift=63, n=1000, seed=0):
    '''Share of losses on flagged days vs circularly shifted calendars.'''
    rng = np.random.default_rng(seed)
    losses = np.clip(-pnl, 0, None)
    observed = losses[flagged].sum() / losses.sum()
    shifts = rng.integers(min_shift, len(pnl) - min_shift, size=n)
    null = np.array([losses[np.roll(flagged, s)].sum() / losses.sum() for s in shifts])
    return observed, (null >= observed).mean()
Enter fullscreen mode Exit fullscreen mode

pnl is a float array of daily trade P&L and flagged a boolean array marking negative-GEX days. Circular shifts preserve the clustering of regimes over time, which a plain shuffle would not.

Selling only on positive-GEX days

Sharpe ratio of monthly P&L and maximum drawdown at midpoint fills:

ETF Strangle Sharpe, every day Sharpe, positive GEX Max DD, every day Max DD, positive GEX
QQQ 0DTE, 0.25 delta +0.35 +1.38 −$6,402 −$2,856
QQQ 0DTE, 0.10 delta +0.04 +1.06 −$4,122 −$1,535
QQQ 1 week, 0.25 delta −0.25 +1.14 −$5,687 −$845
QQQ 1 week, 0.10 delta −0.40 +1.02 −$3,712 −$445
SPY 0DTE, 0.25 delta +0.26 +0.54 −$5,907 −$2,276
SPY 0DTE, 0.10 delta +0.20 +0.66 −$3,052 −$1,377
SPY 1 week, 0.25 delta −0.33 +0.25 −$4,724 −$1,218
SPY 1 week, 0.10 delta −0.55 +0.18 −$3,725 −$690

The filter raised the Sharpe ratio and reduced the drawdown in every row. At full bid/ask, selling every day lost money in every setup, and all four filtered SPY setups and two of the four filtered QQQ setups also lost money. QQQ 0DTE at 0.25 delta held +0.72 filtered against −0.47 every day at full spread.

Across all 17,100 strategies

  • Positive GEX with spot above a certified flip beat random calendars on both ETFs for same-day trades.
  • Plain positive GEX passed on QQQ but not on SPY.
  • Negative GEX and high VRP, used alone, did no better than random timing.
  • For positions held to expiry, taking profit at 50% of the credit reduced the maximum drawdown in 179 of 180 designs (median 28%).
  • The highest Sharpe overall was a 0.10-delta, 1-month SPY put sold every day and held to expiry (+4.08), with a $57,739 drawdown while positions were open. GEX and VRP filters lowered it.

The rule in code

from datetime import date
import requests

KEY = 'YOUR_API_KEY'
LAB = 'https://lab.flashalpha.com'
HIST = 'https://historical.flashalpha.com'
H = {'X-Api-Key': KEY}

# 1. Regime and certified flip from the previous close (15:55 ET snapshot)
gex = requests.get(
    f'{HIST}/v1/exposure/gex/SPY',
    params={'at': '2026-10-22T15:55:00'},  # previous session
    headers=H,
).json()

if gex['net_gex'] < 0:
    raise SystemExit('Negative gamma: skip or reduce size')

# 2. Spot at the open
spot = requests.get(f'{LAB}/stockquote/SPY', headers=H).json()['mid']
certified = gex['gamma_flip_status'] == 'available'
strongest = certified and spot > gex['gamma_flip']

# 3. Pick a 0.25-delta same-day put and call from the chain
chain = requests.get(f'{LAB}/optionquote/SPY', headers=H).json()
today = date.today().isoformat()

def pick(kind, target=0.25):
    ok = [q for q in chain
          if q['type'] == kind and q['expiry'] == today
          and q['open_interest'] >= 250 and q['bid'] > 0
          and (q['ask'] - q['bid']) / q['mid'] <= 0.20]
    return min(ok, key=lambda q: abs(abs(q['delta']) - target))

put, call = pick('P'), pick('C')
print('strongest regime' if strongest else 'positive GEX', put['strike'], call['strike'])

# 4. Close at 15:55 ET by reading the same contracts from the chain again
Enter fullscreen mode Exit fullscreen mode

The study reads the previous close through the Historical API. Running live, you can call /v1/exposure/gex/SPY at 15:55 ET and store the result for the next morning.

The gamma_flip_status field

A gamma flip computed from a thin chain can move when a single strike's quote widens. The GEX endpoint therefore returns gamma_flip_status alongside the level. A flip is certified only when it passes three checks: coverage (at least 75% of the open interest between spot and the level carries usable gamma), quote quality (on expiry day, at least 75% of that open interest has two-sided quotes worth more than their spread) and a stress test (every strike's gamma scaled by 0.75 and 1.25 without moving the level). When a check fails, the level is still returned and the status names the failed check. The flip was certified on about 80% of SPY sessions and 68% of QQQ sessions.

Validation

  • Pre-registration. Rules, fills, filters and pass criteria were committed before any SPY result was computed.
  • Out of sample. The idea came from QQQ. SPY was held back and tested once. The 1-week strangle hypothesis passed every criterion on SPY. The 0DTE, iron condor and GEX-plus-VRP versions improved in every case but did not pass.
  • Multiple testing. Allowing for 84 filter tests, there is about a 12% chance the four strongest same-day results are false discoveries.
  • Fills. The pre-registered tests used the halfway fill. Midpoint figures were chosen as headlines afterwards, so every figure is published at all four fill levels.

Limitations

  • Short strangles carry unlimited risk. The filter reduced the worst days without removing them.
  • One period of under five years, two ETFs.
  • Flip-based filters only trade days with a certified flip, so part of their result can come from which days are left out.

Links

Not investment advice.

Top comments (1)

Collapse
 
signumhq profile image
SIGNUM HQ •

Publishing all four fill levels is the right call, since the full bid/ask rows are where most of the filtered SPY results turn negative. On your own limitation that flip-based filters only trade days with a certified flip: a clean way to separate the signal from the day selection is to run the unfiltered strategy on only the certified-flip sessions (about 80% of SPY and 68% of QQQ) and compare that with the positive-GEX rule on the same days. If the unfiltered subset already looks better than every day, part of the lift comes from which days are left out. Did you run that comparison, and if so does the positive-GEX filter still add Sharpe on top of it?