Trading the sign of a public gamma feed
One rule, its entry logic reproduced, run over 63 sessions: 64 trades, −0.062 R each
A widely followed public dealer-gamma feed publishes, for SPX and for the 0DTE scope alone, a single net gamma number: the dealer book's delta change per 1% move, in millions of dollars, built from call/put-signed cumulative volume rather than open interest. An intraday model trades its sign — long when the series crosses above +100,000, short when it crosses below −100,000. That is a clean, falsifiable claim about a public object, so it can be tested rather than argued about.
Two questions hide inside it, and they are not the same question. Does the trade make money? and does the sign carry any information at all? A rule can lose while its signal is informative (bad geometry, bad costs) and a rule can win while its signal is noise (a lucky sample). Both are answered below, separately. The short version: the trade is a coin flip with a cost drag, and the sign is nothing.
The rule, as configured
| element | as it runs |
|---|---|
| series | the feed's 0DTE net-gamma-from-volume value at spot, in $M of dealer delta per 1% move, polled every 5 s |
| trigger | the day's first armed crossing of ±100,000, per side |
| arming | a Schmitt gate at the same level: the value must first be seen at or inside the band, so a feed gap that reappears outside it cannot fire |
| side | long on the + cross, short on the − cross — momentum, not fade |
| cap | one trade per side per day (up to two), and a long and a short may ride at once |
| target | the feed's published positive-gamma peak strike (long) or negative-gamma peak strike (short), taken from the firing sample; skipped if price is already through it |
| stop | half the target distance ⇒ a fixed +2R win / −1R loss ⇒ breakeven win rate 33.3% |
| clock | no new entries after 14:00 ET; flat at 15:55 ET |
| size | 0.2% of account per trade, whatever the stop distance |
The Schmitt gate matters more than it looks: it is why a session can end with no fire on a side at all. The fixed-fraction sizing matters too, and shows up later as the largest single line item in the result. The live record behind the rule was two orders, both long — one of which carried a 0.9-point stop at full dollar risk. There is no live evidence base; that is why the study exists.
Step one: rebuilding the series
The feed publishes no deep history of the series, so it has to be reconstructed from our
own book before anything can be tested — and a test of someone else's signal is only as
good as that reconstruction. Ours is the call/put-signed cumulative-volume book at
dte == 0, evaluated at spot, converted from dollars-per-point into the
published unit:
recon = volume_book_at_spot * spot * 0.01 # $/pt -> $M of dealer delta per 1% move
Eight sessions overlap a live capture of the feed, joined on the exact Eastern-time second — 618 paired observations. "Near band" means the feed's own value sat in [50k, 200k], the region where a crossing is decided.
| session | n | corr | median |err| | median rel err | n near band | median |err| near band | ±100k state agrees |
|---|---|---|---|---|---|---|---|
| 2026-07-22 | 77 | 0.981 | 20,294 | 13.1% | 18 | 26,954 | 0.883 |
| 2026-07-23 | 77 | 0.980 | 4,977 | 8.0% | 26 | 3,566 | 0.961 |
| 2026-07-24 | 78 | 0.992 | 7,902 | 5.1% | 19 | 15,512 | 0.974 |
| 2026-07-27 | 77 | 0.987 | 5,817 | 4.9% | 45 | 4,634 | 0.987 |
| 2026-07-28 | 77 | 0.837 | 7,988 | 14.5% | 17 | 26,880 | 0.935 |
| 2026-07-29 | 77 | 0.998 | 2,178 | 5.5% | 17 | 2,402 | 0.987 |
| 2026-07-30 | 78 | 0.996 | 9,747 | 7.9% | 15 | 10,922 | 1.000 |
| 2026-07-31 | 77 | 0.997 | 5,738 | 3.7% | 10 | 5,900 | 0.987 |
| pooled | 618 | 0.986 | 6,912 | 6.7% | 167 | 7,148 | 0.964 |
Sign agreement is 97.2% overall and 95.4% when the feed's own value is under 200k in absolute terms — the hard cases, near zero, are where it degrades, as it must.
But level agreement is the wrong statistic for a threshold rule. The question is how far a level error moves a crossing in time. Both halves are measurable: the series moves at a median 243 units per second around armed ±100k crossings (IQR 91 to 524, n = 269), and the median error near the band is 7,148. Median error divided by median slope is 29 seconds of timing error at a typical crossing.
The direct test is to run the rule itself on the feed's archived series and on our reconstruction sampled at the feed's own timestamps:
| session | their long | our long | their short | our short | Δ long | Δ short |
|---|---|---|---|---|---|---|
| 07-22 | 11:05:01 | 11:10:03 | 15:10:01 | 14:15:00 | +5 min | −55 min |
| 07-23 | — | — | 11:10:00 | 11:10:00 | — | 0 |
| 07-24 | 11:15:01 | 11:15:01 | 14:20:01 | 14:25:02 | 0 | +5 min |
| 07-27 | 15:00:02 | 15:00:02 | 11:20:01 | 11:20:01 | 0 | 0 |
| 07-28 | 11:20:01 | 11:20:01 | — | — | 0 | — |
| 07-29 | 14:40:00 | 14:40:00 | 15:25:02 | 15:25:02 | 0 | 0 |
| 07-30 | 12:05:02 | 12:05:02 | — | — | 0 | — |
| 07-31 | 12:00:00 | 12:00:00 | — | — | 0 | — |
Fire existence matches exactly — 7 long and 5 short side-days on theirs, 7 and 5 on ours. Timing lands within one five-minute bar on 6 of 7 long and 3 of 5 short side-days. The one bad miss, 55 minutes early, is the honest worst case and is printed rather than averaged away.
The tail was then priced. Residual paths were block-bootstrapped from the eight overlap days, added to the reconstruction, and the first fire re-derived per second — 200 draws across 63 sessions, 12,600 pairs. Fire existence changes on 5.3% of draws. The shift of the first long fire has median 0.3 min, p75 1.9, p95 21.9 — within five minutes on 85% of draws; the short side is median 0.2, p75 1.9, p95 19.0, within five minutes on 84%.
So: roughly 85% of fires land within one bar of where the real feed would have put them, about 5% of side-days gain or lose a fire entirely, and about 15% land more than five minutes off. That widens every confidence interval below. It does not manufacture or erase a mean — and every headline here is a mean over 63 sessions whose interval already spans zero by a wide margin.
The crossings
On the per-second reconstruction the series chatters through the band constantly: a median of 32 armed ±100k crossings per session (mean 34.7, max 90). The one-per-side-per-day cap means only the first two ever matter. A long first fire exists on 57 of 63 days, a short on 48 of 63; 73 of the 105 come before the 14:00 ET cutoff. First long fire: p10 10:40, median 11:31, p90 14:54.
Sampling grain is not a detail. On the five-minute grain a replay layer would use, the same series fires long on 54 of 63 days and short on 41 of 63, a median 3.9 and 4.5 minutes later than the per-second version. Live and replay trade materially different entries.
105 candidate side-days become 64 trades: 32 are killed by the 14:00 cutoff — 30% of all signal — and 9 by degenerate target geometry, where price is already at or through the level when the crossing happens.
The trades
Assumptions, stated in full because one of them is decisive. Prices are the per-second SPX index from put-call parity; results are in points and R, so any venue basis cancels. Fill is spot 5 seconds after the crossing (poll plus submit), with 0, 30 and 60 second variants shown. The target is the firing second's own positive- or negative-gamma peak strike; the stop is half that distance off the fill. Exit is the first second the index crosses target or stop, otherwise the 15:55 force close. Cost is 0.5 index points round trip, spread plus slippage — an assumption, not a measurement, which is why the sensitivity is shown. Median target distance is 9.5 points (p10 3.1, p90 22.7), so the median stop is 4.8 points and the cost assumption is 11% of the median stop. Median hold is 7 minutes, p90 110. Several live guards — day visibility, news, instrument, entry drift — are not modelled, and all of them would only remove trades.
| cut | n | days | net R | R/trade | 95% CI (day-clustered) | t | pts/trade | win rate |
|---|---|---|---|---|---|---|---|---|
| as configured | 64 | 52 | −3.96 | −0.062 | [−0.431, +0.308] | −0.33 | +1.13 | 35.9% |
| long leg | 43 | 43 | +9.85 | +0.229 | [−0.228, +0.686] | +0.98 | +3.28 | 46.5% |
| short leg | 21 | 21 | −13.81 | −0.658 | [−1.140, −0.175] | −2.67 | −3.26 | 14.3% |
A day-block bootstrap over 5,000 draws puts net R in [−26.8, +20.5] with P(net R > 0) = 0.37. Because the stop is half the target, breakeven is a 33.3% win rate; the observed rate is 35.9% with a binomial standard error of 6.0% and a 95% interval of [24.2%, 47.7%]. The rule sits on its own breakeven line with an interval that spans everything either side of it. Resolving a +0.10 R/trade edge at 80% power would need about 1,700 trades — roughly 1,650 sessions, about 6.5 years, at this fire rate.
Sensitivities, read as one object
| variant | n | net R | R/trade | t | win rate |
|---|---|---|---|---|---|
| cost 0.00 pt | 64 | +7.00 | +0.109 | +0.59 | 35.9% |
| cost 0.25 pt | 64 | +1.52 | +0.024 | +0.13 | 35.9% |
| cost 0.50 pt (baseline) | 64 | −3.96 | −0.062 | −0.33 | 35.9% |
| cost 1.00 pt | 64 | −14.93 | −0.233 | −1.19 | 35.9% |
| fill at 0 s | 65 | −5.55 | −0.085 | −0.47 | 36.9% |
| fill at 30 s | 65 | −8.05 | −0.124 | −0.50 | 38.5% |
| fill at 60 s | 65 | +12.85 | +0.198 | +1.07 | 43.1% |
| five-minute grain | 63 | +7.75 | +0.123 | +0.56 | 44.4% |
| no 14:00 cutoff | 89 | −9.67 | −0.109 | −0.70 | 32.6% |
| reversed — fade, not follow | 73 | −17.61 | −0.241 | −2.01 | 15.1% |
The sign of the result flips on a 0.25-point cost assumption, on whether the fill lands at 30 or 60 seconds, and on whether the series is sampled per second or per five minutes. Nothing that behaves like an edge is that fragile. Note also that the reversed rule loses too: with the stop at half the target the structure is negative-carry in both directions, which is a statement about the geometry rather than about the signal.
Where the loss actually comes from
Fixed-fraction sizing means a 0.9-point stop and a 17-point stop risk the same dollars. Observed stop distances run from 0.27 points (p10 1.56, median 4.76) to 16.88.
| subset | n | net R | net points |
|---|---|---|---|
| stop under 2 pt | 9 | −5.24 | −5.8 |
| stop 2 pt or more | 55 | +1.27 | +78.3 |
Nine near-level fires — 14% of trades — account for more than the entire net loss. In points the rule is +72.5 total, +1.13 per trade (t = +0.92); in R it is negative. The gap between those two sentences is the sizing rule, not the signal. R is the unit an operator actually lives in, so the negative number is the true one — but it is worth knowing that the loss is a sizing artefact sitting on top of a signal worth zero.
Controls
Chronological split at 2026-06-25: the first 37 sessions give −0.252 R/trade (t = −1.09, CI [−0.706, +0.202]), the last 26 sessions give +0.199 (t = +0.65, CI [−0.404, +0.802]). Both halves are zero and they disagree on sign.
Threshold placebo — the same machinery with the ±100k level swept:
| fire level | n | net R | R/trade | t |
|---|---|---|---|---|
| 25,000 | 96 | −46.90 | −0.489 | −1.90 |
| 50,000 | 80 | −23.20 | −0.290 | −1.94 |
| 100,000 (as configured) | 64 | −3.96 | −0.062 | −0.33 |
| 200,000 | 41 | −9.37 | −0.228 | −0.93 |
| 300,000 | 33 | −24.80 | −0.752 | −2.42 |
| 500,000 | 8 | −4.38 | −0.547 | −1.04 |
The surface is negative at every threshold, and 100k sits at its least-bad point. That is the signature of a knob resting on a noisy negative surface: ±100k is not doing something its neighbours fail to do, it is where the noise happened to be kindest.
Permutation. Shuffle the crossing second uniformly within each day, keep the same side-days and the same target and stop machinery, 2,000 draws. Reported in points, because net R's variance is dominated by the sub-2-point stops above. Observed net +72.5 pt; permuted mean +258.2 pt (sd 134.1, p05 +46.7, p95 +479.3), giving one-sided p(permuted ≥ observed) = 0.922 and two-sided p = 0.154. In R the same permutation gives an observed median of −1.076 against a permuted −0.660.
Read plainly: a random entry time inside the session, using the same targets and stops, does better than the ±100k crossing. Not significantly worse — but unambiguously not better. The trigger has negative marginal value over a coin toss on the clock, and that is the most damning line in the study.
A different question: does the sign carry anything?
Profitability and informativeness are separate. So, ignoring the trade entirely: a panel of every five minutes from 09:35 to 14:00 ET, 3,402 observations across 63 sessions, forward return in basis points, day-clustered standard errors.
| horizon | sign alone (bps) | t | continuous value (bps) | t | sign + return-to-date (bps) | t |
|---|---|---|---|---|---|---|
| +15 min | −0.013 | −0.03 | +0.010 | +0.04 | +1.141 | +2.09 |
| +30 min | −0.319 | −0.42 | −0.021 | −0.04 | +1.745 | +1.75 |
| +60 min | −0.564 | −0.51 | −0.143 | −0.14 | +3.523 | +2.28 |
| +120 min | −0.321 | −0.16 | +0.413 | +0.20 | +6.288 | +1.98 |
| to 15:55 | +0.661 | +0.15 | +0.136 | +0.04 | +10.079 | +1.97 |
R² of the univariate specifications runs from 0.0000 to 0.0005. The raw sign — which is what the rule trades — is exactly nothing at every horizon, and so is the continuous value.
The third column is the honest complication. The sign of this series correlates +0.729 with the index's return so far on the day, and the continuous value +0.749. Return-to-date mean-reverts (its coefficient is negative, t ≈ −3.4 at every horizon), so once it is partialled out the sign coefficient turns positive at t ≈ 2. That is a textbook suppressor, not a discovery: the sign is a lagging restatement of "the index is up on the day", and the multivariate coefficient measures residual positioning after the mean-reversion has been removed. It is one specification with heavily overlapping windows, and it is not what the rule reads. The rule reads the raw sign, and the raw sign is zero.
The one cell that is not zero
105 first crossings, signed so that positive means the rule's direction was right, day-clustered:
| horizon | n (all) | mean (bps) | t | 95% CI | n (pre-cutoff) | mean | t |
|---|---|---|---|---|---|---|---|
| +15 min | 96 | +0.61 | +0.39 | [−2.45, +3.66] | 73 | −0.10 | −0.07 |
| +30 min | 95 | +0.68 | +0.37 | [−2.94, +4.30] | 73 | +0.38 | +0.19 |
| +60 min | 89 | +2.76 | +1.11 | [−2.09, +7.61] | 73 | +4.62 | +1.77 |
| +120 min | 75 | +7.25 | +2.08 | [+0.41, +14.08] | 73 | +7.26 | +2.03 |
| to 15:55 | 105 | +1.96 | +0.73 | [−3.31, +7.24] | 73 | +1.96 | +0.53 |
Split by side, pre-cutoff, the drift is entirely on one leg:
| side | +30 min | +60 min | to close |
|---|---|---|---|
| long (n = 46) | +4.21 (t = +1.83) | +7.07 (t = +2.63) | +3.86 (t = +0.69) |
| short (n = 27) | −6.15 (t = −1.11) | +0.46 (t = +0.07) | −1.27 (t = −0.13) |
So it got its own robustness pass. The median is +8.03 bps and 72% of observations are positive; dropping the three largest leaves +4.89 bps (t = +1.94); the first half of the sample gives +5.38 (t = +1.21) and the second +8.76 (t = +2.85), so it is not a decaying artefact. A random five-minute anchor over the same 63 sessions drifts +0.68 bps over 60 minutes (t = +0.53), so unconditional market drift explains about a tenth of it. A time-of-day-matched placebo — same clock times, random other session — gives a mean of +0.60 with sd 3.61, putting the observed value at z = +1.79, one-sided p = 0.035. Residualised on return-to-date and the last 30 minutes' return, the intercept is +11.15 bps (t = +2.20), so it is not the suppressor from the panel above.
And yet. It is one of ten correlated cells inspected (two sides × five horizons); Bonferroni at ten cells wants |t| ≈ 2.8 and this is 2.63. It peaks at 60 minutes and decays to +3.86 (t = +0.69) by the close — which is what noise looks like and also what transient microstructure looks like. Most decisively, the shipped geometry cannot convert it: 7 bps over 60 minutes is about 5.2 index points, against a median target of 9.5 and a stop at 4.8, so a drift that size with that dispersion gets stopped out about as often as it pays. The long leg's realised result — +0.229 R/trade, t = +0.98 — is exactly what a real-but-too-small drift looks like inside this structure. And the rule does not trade the long leg alone; it is bolted to a short leg costing −0.658 R/trade.
The verdict
Null, and slightly worse than null. Over 63 sessions the rule produces 64 trades and −0.062 R per trade, CI [−0.431, +0.308], t = −0.33, with a 37% bootstrap probability of being positive. At its configured risk and fire rate that is about −3.2% of account per year with an interval of [−20.7%, +16.8%] — a coin flip with an execution-cost drag. The object it triggers on has no predictive content at any horizon tested. The threshold is inert at every neighbouring value, and a randomly timed entry beats the crossing. Nothing here is a rebuild candidate.
What it does not say
It does not say the feed is wrong. Our reconstruction agrees with it at corr 0.986 and 96.4% ±100k state agreement, which is a statement that the published number is a reasonable rendering of a volume-signed 0DTE book. What is tested is the rule built on that number's sign, not the number's accuracy.
It does not prove there is nothing. 63 sessions cannot resolve a +0.10 R/trade edge — that needs roughly 1,700 trades. The claim is narrower: there is nothing large enough to trade at this size, the threshold is doing no work, and the current structure loses on cost and sizing.
It rests on a reconstruction, not the feed's own history. About 15% of fires are placed more than five minutes from where the real series would have put them and about 5% of side-days gain or lose a fire. That is the one real soft spot, and it widens intervals that already span zero rather than hiding a mean inside them.
The long-side +60-minute drift is a live loose end, not a result. It is one of ten looks, it fails Bonferroni, and it decays by the close. It would take an independent forward sample of at least 40 more crossings reaching |t| > 2.8 to be worth anything — and then as a time-exit study, not as this rule.
Costs are assumed, not measured. 0.5 points round trip is a guess, and the result changes sign at 0.25. The per-second index series has no intra-second extremes, so both target and stop touches are slightly under-counted — near-symmetric, mildly optimistic when the two levels sit close together.
Nothing on this page is investment advice; see the Terms.
Reproduce it
The book underneath all of this is built from licensed data: the ThetaData SPXW
trade and NBBO tick feed (https://thetadata.net), which is a paid subscription.
Rebuilding the book from raw prints is not free. What is free is the output: every
finished session's JSON at https://gex.live/snapshots/YYYY-MM-DD.json — dates
listed at /sessions — publishes the per-minute net gamma of both the
measured book (ngv_meas) and the conventional open-interest book
(ngv_conv), plus a per-second block under sec holding
secs, spot, ngv_volcp (the call/put-signed
cumulative-volume book at 0DTE, in $M per point — the reconstruction basis used here),
cr0_volcp and ps0_volcp (that book's positive- and negative-gamma
peak strikes) and flip_volcp. That is enough to rebuild the series, the
crossings and the trade tape end to end:
import json, urllib.request d = json.load(urllib.request.urlopen("https://gex.live/snapshots/2026-08-11.json")) s = d["sec"] # 23,400 one-second points, 09:30:00 -> 15:59:59 ET ngv = [None if v is None or p is None else v * p * 0.01 for v, p in zip(s["ngv_volcp"], s["spot"])] # $M/pt -> $M per 1% move LEVEL = 100000 # the Schmitt band, same level for arming and firing fire, armed = {}, {"long": False, "short": False} for t, v in zip(s["secs"], ngv): if v is None: continue if v <= LEVEL: armed["long"] = True # seen at/inside the band -> armed if v >= -LEVEL: armed["short"] = True if armed["long"] and v > LEVEL and "long" not in fire: fire["long"] = t if armed["short"] and v < -LEVEL and "short" not in fire: fire["short"] = t print(fire) # the day's first armed crossing, per side # target = s["cr0_volcp"][i] on the long side, s["ps0_volcp"][i] on the short, # read at the firing second i; stop = fill -/+ 0.5 * |target - fill|; # fill = s["spot"][i + 5]; exit = first later second crossing target or stop, # else the 15:55 close. Cost 0.5 index points round trip. Skip a side whose # crossing lands after 14:00 ET, or whose target is already through spot.
Run that over the dates at /sessions and the trade tape follows directly; the sensitivities above are the same loop with the cost, the fill offset and the sampling grain changed. The one thing the public files cannot give you is the feed's own archived series — that anchor pass needed a live capture, and only eight sessions of it exist.
Part of gex.live research. Measured on the free session archive; every session is free to replay.