Historical GEX API: Backtest Gamma Exposure Strategies (Minute-Level) | FlashAlpha
gammaexposure · 13 min read

Historical GEX API: Backtest Gamma Exposure Strategies (Minute-Level)

Inspect a timestamped SPY historical GEX response with a downloadable notebook, then build a minute-level research pipeline. Check coverage, input vintages, timezone, costs and lookahead bias before testing a strategy.

T
Tomasz Dobrowolski Quant Engineer
Apr 15, 2026
Updated Oct 14, 2026
13 min read
GammaExposure GEX Backtesting HistoricalData OptionsAPI Quant

If you've ever read a blog post claiming "short straddles when GEX is positive returns 30% a year" and wondered why the claim evaporates when you try to reproduce it, the answer is usually the same: the backtest cheated somewhere. Either it used data that didn't exist at the decision time, it labelled regimes using end-of-day aggregates that look clean but smear across the true intraday flip, or it skipped the execution costs that swamp the signal.

FlashAlpha's Historical Analytics API provides minute-level historical GEX data through the same analytics engine as live. Use the at parameter for decision-time regime labels and the historical VRP endpoint for leak-free percentiles. This article walks through the research pipeline.

Reproduce One Timestamp Before Buying Historical Access

Download the historical GEX notebook and sample pack (ZIP). It includes copies of our published GEX and levels responses, a SHA-256 manifest, expected values, a Python script and an evaluation worksheet. Extract all files together. Run python dashboard.py with Python 3.10 or later, or open evaluation.ipynb in Jupyter from the extracted folder. The offline example needs no key and makes no network requests.

Input or outputSaved example
Requests/v1/exposure/gex/SPY and /v1/exposure/levels/SPY on historical.flashalpha.com
Query and scopeat=2026-03-05T15:30:00; no expiration filter, full returned chain, 439 strike rows
TimezoneEastern wall-clock, America/New_York. On this date: 15:30 EST = 20:30 UTC. Do not append Z to the at request.
Underlying price$679.46 per SPY share
Net GEX−$14,504,525,330.20 of delta notional per 1% underlying move; label negative
Reported levelsGamma flip 687.171655677348; call wall 685; put wall 675, in SPY price units
Engine versionendpoint_version: 2026.08.25. Saved replay, not JSON served on March 5.
Archived input vintageEquity feed 2026-03-05T20:29:48Z; option feed 2026-03-05T20:29:58Z; OI feed 2026-03-04T21:00:00Z

Open the generated dashboard or inspect the full manifest and hashes. The original fixture capture time was not recorded; this pack was assembled on October 14, 2026. archive_as_of records archived input vintages; historical data_as_of live-feed slots are null. These are response fixtures, not the raw chain needed to independently rebuild the pricing engine.

Assumptions: settled OI, the fixed calls-positive/puts-negative structural convention, and the equity multiplier of 100. This does not reveal actual dealer holdings. The saved schema has no flip-certification status, so the notebook reports a historical flip value without manufacturing a certified regime. New replay responses may differ after engine or archive updates; save their retrieval time, version and payloads.

What Alpha Adds to This Example

The free download lets you inspect one saved timestamp. Alpha lets you request your own supported symbols and decision times. With an Alpha key in FLASHALPHA_API_KEY, run:

python dashboard.py --historical --symbol SPY --at 2026-03-05T15:30:00

This makes two authenticated requests and saves both returned payloads next to the HTML. Check per-symbol coverage, then test a normal session and your event date in the historical playground. SPY coverage does not establish SPX coverage. Historical futures replay is not currently supported. Choose Alpha access when the routes, dates and internal-use license fit the study.


Historical GEX Data: Minute-Level Coverage Since 2017

The Historical API mirrors 52 analytics routes with the same paths, fields and units as their live counterparts. The archive covers 200+ symbols, including at least 54 with eight or more years of history and 14 beginning on 2017-01-03. Minute-level quotes and Greeks support historical gamma exposure, historical GEX levels and regime studies; settled open interest and surface parameters have daily vintages. The historical methodology explains those inputs and leak-free percentile construction.

Replay runs the current engine over point-in-time archived inputs, rather than returning a log of previously served JSON. Try the route and symbol in the historical playground; the public coverage response at /v1/tickers gives each symbol's dates and gaps. SPX starts later than SPY, as the example below shows.

SPX Historical GEX: Check Coverage Before Choosing Dates

The public historical coverage response reports SPX coverage.first = 2022-01-05, with healthy-day counts and gaps per symbol. Those are coverage bounds, so refresh the response and test the dates your study needs before committing to a window.

SymbolFirst minuteLast sessionHealthy sessions
SPY2017-01-032026-09-152,370
QQQ2017-01-032026-09-152,352
IWM2017-01-032026-09-152,435
SPXW2018-05-162026-09-152,092
SPX2022-01-052026-09-151,175

Snapshot of /v1/tickers on September 17, 2026. A healthy session has every input layer present (quotes, Greeks, settled OI, surface parameters). The last session trails today by the archive's settlement lag.

A first SPX smoke test is GET https://historical.flashalpha.com/v1/exposure/gex/SPX?at=2025-04-07T15:30:00, using an Alpha key in the X-Api-Key header. The timestamp is Eastern wall-clock under the historical API contract. This is a request example, not a captured SPX result. Inspect the returned timestamp, units, expiry scope, provenance and errors before extending the date range. SPY coverage cannot be substituted for SPX coverage; futures history is a separate question.

For today's levels, open SPX gamma exposure. For research, keep the historical response and retrieval date: replay recomputes the analytics from archived point-in-time inputs with the current engine, so it reflects today's methodology rather than a log of past responses.

A Research Example with Inspectable Evidence

The 50-strategy research and downloadable ledgers show how to make a historical options study inspectable. Its monthly ETF sample spans SPY, QQQ and IWM from January 2020 to December 2025, with six structures compared on 133 matched symbol-month windows. The after-cost option scenario buys at ask, sells at bid and charges $0.65 per contract per side. Results use a 100-share entry-value denominator. This is a structure comparison, not a test of the intraday GEX hypothesis below; earnings and the wheel have separate samples.

Use that study's ledger format to record the GEX observation time, entry, exit and costs in your own test. For the intraday example below, use an exchange calendar, exclude early closes from a 15:30-to-16:00 study, log missing sessions and preserve unknown regimes.

What We're Testing

A simple, well-known hypothesis: SPY mean-reverts intraday when dealers are long gamma and trends when dealers are short gamma. Specifically, we'll test whether short-term (30-minute) returns after 15:30 ET differ by dealer regime. If the hypothesis is real, 15:30-to-close returns should be smaller and more mean-reverting during positive-gamma regimes and larger (in either direction) during negative-gamma regimes.

This is a textbook GEX claim. It's also exactly the kind of thing that looks real in a sloppy backtest and dies in a careful one. Let's do it carefully.

The pipeline in five steps
  1. Regime labels - /v1/exposure/summary/SPY?at=<date>T15:30:00, one call per session.
  2. Forward returns - /v1/stockquote/SPY?at=<date>T16:00:00, divided by the 15:30 spot from step 1.
  3. Condition on regime - group returns by positive_gamma / negative_gamma.
  4. Stratify on magnitude - decile net_gex, fit bins on a training window.
  5. Scale to minutes - the same loop at 390 timestamps per session.

Step 1: Pull the Regime Labels

We need, for every trading day in the test window, the dealer regime at 15:30 ET. That's /v1/exposure/summary/{symbol} with at set to 15:30 on each day.

import os
import httpx, pandas as pd
from tqdm import tqdm

API_KEY = os.environ["FLASHALPHA_API_KEY"]
BASE = "https://historical.flashalpha.com"

dates = pd.bdate_range("2022-01-03", "2025-12-31")
rows = []

with httpx.Client(headers={"X-Api-Key": API_KEY}, timeout=30) as c:
    for d in tqdm(dates):
        ts = d.strftime("%Y-%m-%dT15:30:00")
        r = c.get(f"{BASE}/v1/exposure/summary/SPY", params={"at": ts})
        if r.status_code in (401, 403, 429):
            r.raise_for_status()  # Stop on auth, entitlement or quota errors.
        if r.status_code != 200:
            print("Missing observation:", ts, r.status_code)
            continue
        j = r.json()
        rows.append({
            "date": d,
            "spot_1530": j["underlying_price"],
            "net_gex": j["exposures"]["net_gex"],
            "net_dex": j["exposures"]["net_dex"],
            "regime": j["regime"],
            "gamma_flip": j["gamma_flip"],
            "zero_dte_pct": j.get("zero_dte", {}).get("pct_of_total_gex", 0),
        })

gex = pd.DataFrame(rows).set_index("date")

The date range contains roughly 1,000 sessions. The weekday grid above also includes exchange holidays; replace it with a trading calendar and log failed or missing requests instead of silently discarding them in a production study. Cache valid responses with their query parameters and provenance. Runtime depends on response time, rate limits and retries.

Leak check: the settled net_gex calculation combines the official OI available at the session open with the point-in-time chain and Greeks for 15:30 ET. The morning OI load is fixed during settled replay; intraday OI estimates belong to the separate flow simulator. Official OI uses the documented effective T+2 settlement basis, so it should not be described as necessarily yesterday's positions. See the input and computation methodology.


Step 2: Pull the Forward Returns

For each full session, we need the 15:30-to-16:00 ET return. The example reuses the summary's 15:30 underlying price and makes one stock-quote call at 16:00: two requests per date including the summary. Validate that both prices use a compatible convention.

close_rows = []
with httpx.Client(headers={"X-Api-Key": API_KEY}, timeout=30) as c:
    for d in tqdm(dates):
        r = c.get(f"{BASE}/v1/stockquote/SPY", params={"at": d.strftime("%Y-%m-%dT16:00:00")})
        if r.status_code in (401, 403, 429):
            r.raise_for_status()
        if r.status_code == 200:
            close_rows.append({"date": d, "spot_close": r.json()["mid"]})

        else:
            print("Missing closing quote:", d, r.status_code)

closes = pd.DataFrame(close_rows).set_index("date")
df = gex.join(closes, how="inner").dropna()
df["ret_30min"] = df["spot_close"] / df["spot_1530"] - 1

Leak check: the forward return uses 16:00 ET data to measure performance of a signal generated at 15:30 ET. There's a 30-minute gap, which is the signal window. No feature crosses the signal-to-outcome boundary.


Step 3: Condition on Regime

by_regime = df.groupby("regime")["ret_30min"].agg(
    n="count",
    mean_bps=lambda x: x.mean() * 10000,
    std_bps=lambda x: x.std() * 10000,
    abs_mean_bps=lambda x: x.abs().mean() * 10000,
)
print(by_regime)

The interesting column is abs_mean_bps - the average absolute move. If the mean-reversion hypothesis is right, it should be smaller in positive_gamma than in negative_gamma. If the directional hypothesis is right (short puts in positive gamma), mean_bps should differ too.

Compare abs_mean_bps and sample counts across regimes, then repeat on held-out dates. Smaller absolute moves indicate volatility compression; test mean reversion separately against the preceding move and include execution costs before turning the result into a trading rule.


Step 4: Stratify on GEX Magnitude

The regime label is a coarse cut. The underlying net_gex is continuous. For a descriptive in-sample comparison, bucket by deciles. Full-sample bins use future observations and must not become trading features; fit bins on a prior training window for an out-of-sample test:

df["gex_decile"] = pd.qcut(df["net_gex"], 10, labels=False)
by_decile = df.groupby("gex_decile").agg(
    gex_mean=("net_gex", "mean"),
    n=("ret_30min", "count"),
    abs_move_bps=("ret_30min", lambda x: x.abs().mean() * 10000),
)
print(by_decile)

What you want to see is a monotone curve - low GEX deciles should show larger absolute moves, high GEX deciles smaller. If the curve is flat or non-monotone, the regime signal isn't really tracking what you think it's tracking and the rest of the strategy won't work.


Step 5: Scale Up - Every Minute, Every Day

Once the daily-grid version works, sample 09:30 through 15:59 ET on full sessions: 390 observations. Including both 09:30 and 16:00 would be 391. Across 1,000 sessions that is 390,000 calls per endpoint, before forward-outcome requests, errors and retries. Short sessions need their own grid; requests remain subject to throughput limits.

The minute-level grid lets you answer strictly-better questions:

  • What happens in the 15 minutes after a regime flip? Filter df to rows where regime[t] != regime[t-1], compute forward returns over varying windows.
  • Do moves compress as spot approaches the gamma flip? Compute spot_to_flip_pct and stratify.
  • Is 0DTE GEX a stronger signal intraday than all-expiry GEX? The zero_dte block in the summary gives you both; run the same test on each.

These are the questions the dealer-positioning literature claims to answer but rarely tests at the resolution of the claim. The Historical API makes them runnable.


Three Traps to Check Before Trusting the Study

TrapWhat goes wrongWhat this setup does
Rolling-percentile leakageA percentile at time t is computed with data from after t.Uses the historical VRP endpoint, whose percentiles are date-bounded.
End-of-day smearingClose-of-day GEX labels a 10:30 entry that could not have seen it.Labels every bar with the regime as it stood at that minute.
SurvivorshipThe universe is today's survivors, not what was tradable then.Fixed SPY study; single names need dated universe membership.

1. Rolling-Percentile Leakage

If your backtest uses VRP percentile or GEX percentile as a feature, make sure the percentile at time t is computed only from data strictly before t. FlashAlpha's VRP endpoint does this by default; custom percentiles you compute yourself need the same treatment. Full write-up on this →

2. End-of-Day Smearing

Net GEX can flip intraday. A backtest that uses close-of-day GEX to label a 10:30 ET entry is measuring a feature that didn't exist yet. The Historical API's minute resolution removes the excuse - you can label each bar with the regime as it stood at that bar's open.

3. Survivorship in Symbol Selection

A fixed SPY study avoids selecting a basket of today's surviving stocks, but its results remain specific to that instrument and sample. For single names, reconstruct the investable universe as it existed at each decision date, including delistings. The current /v1/tickers coverage list identifies available data; it is not a historical universe-membership database.


Going Beyond Regime Labels

Everything above uses the scalar regime and net GEX as the signal. The full exposure/summary response is richer - it includes net DEX (directional dealer positioning), net VEX (vanna - how positioning shifts with vol), net CHEX (charm - time-decay flow), and the interpretation text. For ML workflows, you'd pull the whole response and let the model sort out which fields matter:

def features_at(client, symbol, at_ts):
    sr = client.get(f"{BASE}/v1/exposure/summary/{symbol}", params={"at": at_ts})
    vr = client.get(f"{BASE}/v1/vrp/{symbol}", params={"at": at_ts})
    sr.raise_for_status()
    vr.raise_for_status()
    s, v = sr.json(), vr.json()
    flip = s.get("gamma_flip")
    return {
        "spot": s["underlying_price"],
        "net_gex": s["exposures"]["net_gex"],
        "net_dex": s["exposures"]["net_dex"],
        "net_vex": s["exposures"]["net_vex"],
        "net_chex": s["exposures"]["net_chex"],
        "gamma_flip": s["gamma_flip"],
        "spot_to_flip_pct": None if flip is None else (s["underlying_price"] - flip) / s["underlying_price"],
        "zero_dte_pct": s.get("zero_dte", {}).get("pct_of_total_gex", 0),
        "vrp_20d": v["vrp"]["vrp_20d"],
        "vrp_z": v["vrp"]["z_score"],
        "vrp_pct": v["vrp"]["percentile"],
        "atm_iv": v["vrp"]["atm_iv"],
        "hv_20": v["vrp"]["rv_20d"],
    }
FeatureEndpointWhat it captures
net_gex, gamma_flip, spot_to_flip_pctexposure/summaryRegime sign, the level where it flips, and how far spot sits from it.
net_dexexposure/summaryDirectional dealer positioning.
net_vex, net_chexexposure/summaryHow positioning shifts with volatility (vanna) and time decay (charm).
zero_dte_pctexposure/summaryShare of gamma sitting in same-day expiries.
vrp_20d, vrp_z, vrp_pctvrpImplied-minus-realized premium and its date-bounded z-score and percentile.
atm_iv, hv_20vrpThe two legs of that premium.

That's about 15 features per timestamp with two API calls. For an LLM/transformer model, you'd pull the full nested responses and flatten lazily. For XGBoost or similar tabular models, the above is already in the right shape.


What You Actually Pay For

The cost of this backtest is a function of how many timestamps you pull. Alpha has no daily request cap, so the budget is wall time and discipline about caching, not quota.

Resolution (4 years, one symbol)Calls per endpointSummary + closing quote
One 15:30 snapshot per session~1,000~2,000
Every minute, 9:30 to 16:00~390,000~780,000

Cache each (symbol, at, endpoint) response to parquet once; a portfolio-sized study then re-reads locally instead of re-requesting.

The alternative - building this dataset yourself - requires maintaining ThetaData subscription, options parquets, BSM pipeline, SVI fitter, QuestDB or similar columnar store, minute-level partitioning, holidays/half-days handling, coverage reports. FlashAlpha's Historical API is the bought-not-built version of that stack.

Frequently Asked Questions

Query minute-level historical exposure at each decision timestamp, join later prices to measure outcomes, and report results by regime. Use an exchange calendar, log exclusions and missing inputs, and keep full-sample analysis separate from an out-of-sample trading test.

SPX historical GEX starts on 2022-01-05; SPY, QQQ and IWM run from 2017-01-03. The coverage response on the historical host lists each symbol's dates, healthy days and gaps, so refresh it and test the route and minute you need before choosing a study window.

At roughly 1,000 sessions over four years, one daily summary and one closing quote require about 2,000 requests per symbol, before retries. Sampling 390 minutes across those sessions needs 390,000 requests per endpoint. Runtime depends on latency and throughput limits.

Replay recomputes the analytics from archived point-in-time inputs with the current engine, so it reflects today's methodology rather than a log of past responses. Preserve query parameters, retrieval time, input vintages and available provenance fields.

Rolling-percentile leakage uses future observations in a feature; end-of-day smearing uses closing GEX to label an earlier entry; survivorship bias selects only stocks still trading today. Use the historical VRP endpoint for leak-free percentiles, minute-level GEX for intraday labels, and historical universe membership for single-stock studies.

Yes. Historical exposure/gex, exposure/summary and exposure/levels accept the at timestamp for minute-level queries within each symbol's coverage. Sample once daily for a daily study or use finer timestamps for regime transitions. Across roughly 1,000 full sessions, 390 observations per session require 390,000 requests per endpoint; observe throughput limits and cache the responses.

Use the Historical API exposure/gex route for historical gamma exposure, exposure/levels for historical GEX levels, and the VRP route for leak-free VRP percentiles and z-scores. These are separate outputs; the GEX response itself does not supply VRP percentiles. Alpha includes supported historical routes across 200+ symbols, with at least 54 starting eight or more years ago and 14 beginning on 2017-01-03.


Related Articles

Historical API · Alpha tier · from $1,199/mo billed annually
Replay supported decision times across 52 analytics routes
Same response shape as live, leak-free percentiles, 80+ billion minute-level option rows and a trade tape of 4.6+ billion prints, back to 2017.
View pricing →
Full coverage map at /v1/tickers.

Upgrade to Alpha API Spec

A correct GEX backtest needs minute-level regime labels, leak-free percentiles and features computable at decision time. FlashAlpha's Historical API supplies historical gamma exposure and levels through the live analytics engine, with point-in-time inputs and date-bounded VRP statistics. Cache the responses, extend the code above, and test the dealer-positioning hypothesis against held-out sessions with explicit costs and exclusions.

Live Market Pulse

Get fast visibility into market shifts with full-chain analytics over low-latency REST and MCP polling.

Intelligent Screening

Screen millions of option pairs per second using your custom EV rules, filters, and setups.

Export-Ready

Export structured signals to your own execution stack or broker integration - FlashAlpha delivers the analytics, you keep control of order routing.

Join the Community

Discord

Engage in real time conversations with us!

Twitter / X

Follow us for real-time updates and insights!

GitHub

Explore our open-source SDK, examples, and analytics resources!