"SPX gamma flip is 7615, dealers are short gamma" is the kind of line that shows up in trading Discords every morning. The data behind it usually sits behind a $50 to $300 a month subscription. Most of it can be computed from one free JSON file, and this post walks through how.

The data: one GET per symbol

Cboe publishes a delayed (about 15 minute) quote for every listed option on a symbol in a single JSON document:

https://cdn.cboe.com/api/global/delayed_quotes/options/SPY.json https://cdn.cboe.com/api/global/delayed_quotes/options/_SPX.json (indexes take an underscore) 

It redirects to cdn-api.cboe.com, needs no key, and returns the underlying's price plus an options array (about 5,000 contracts for TSLA, more for SPX). Each contract looks like this:

{ "option": "TSLA261120P00140000", "bid": 0.0, "ask": 0.01, "iv": 2.164, "open_interest": 13695.0, "volume": 55021.0, "delta": -0.001, "gamma": 0.0, "theta": -0.0238 } 

The contract code is a standard OCC symbol: root, YYMMDD expiry, C or P, and the strike times 1000 in 8 digits.

function parseOcc(code) { const m = /^([A-Z0-9.]+?)(\d{6})([CP])(\d{8})$/.exec(code); if (!m) return null; const [, root, d, t, k] = m; return { root, expiry: `20${d.slice(0,2)}-${d.slice(2,4)}-${d.slice(4,6)}`, call: t === 'C', strike: Number(k) / 1000 }; } 

Two gotchas cost me time:

  1. Adjusted contracts. After corporate actions you get roots like TSLA1 or odd ones like B1SLA with non-standard deliverables. Keep only the plain root (plus SPXW-style weekly roots for indexes), or your open interest totals will be quietly wrong.
  2. Unknown symbols return 403, not 404. A 403 here means "no chain", not "you're blocked".

Max pain

Max pain is the strike where option holders' total intrinsic value at expiry would be smallest. For each candidate settlement price K, add up what every open contract would be worth:

// strikes: [{ strike, callOi, putOi }] for one expiry let maxPain = null, best = Infinity; for (const k of strikes) { let pay = 0; for (const s of strikes) { if (s.strike < k.strike) pay += s.callOi * (k.strike - s.strike); // calls in the money if (s.strike > k.strike) pay += s.putOi * (s.strike - k.strike); // puts in the money } if (pay < best) { best = pay; maxPain = k.strike; } } 

That is O(n²) in strikes, which is fine: even SPX has a few hundred strikes per expiry.

Gamma exposure (GEX)

GEX estimates how much stock dealers have to buy or sell per 1% move to stay hedged. The usual convention assumes customers are long calls and long puts, so dealers are short both. With that, call gamma stabilizes the market and put gamma destabilizes it:

// per contract, in USD per 1% move const gex = gamma * openInterest * 100 * spot * spot * 0.01; net += isCall ? gex : -gex; 

The * 100 is the contract multiplier. spot² * 0.01 turns gamma (delta change per $1) into dollars of delta per 1% move.

Positive net GEX means dealer hedging dampens moves. Negative means it amplifies them.

The gamma flip

The flip is the price where net GEX changes sign. To find it you need gamma at prices other than the current one, so you can't use Cboe's published gamma. Re-price it with Black-Scholes from each contract's implied volatility:

function bsGamma(S, K, T, iv, r = 0.04) { if (!(S > 0 && K > 0 && T > 0 && iv > 0)) return 0; const st = iv * Math.sqrt(T); const d1 = (Math.log(S / K) + (r + 0.5 * iv * iv) * T) / st; return Math.exp(-0.5 * d1 * d1) / Math.sqrt(2 * Math.PI) / (S * st); } // scan -20%..+20% in 0.25% steps, linear-interpolate the zero crossing let prev = null, flip = null; for (let p = -0.2; p <= 0.2001; p += 0.0025) { const S = spot * (1 + p); let net = 0; for (const c of contracts) { const g = bsGamma(S, c.strike, c.T, c.iv); net += (c.call ? 1 : -1) * g * c.oi * 100 * S * S * 0.01; } if (prev && Math.sign(prev.net) !== Math.sign(net)) { const x = prev.S + (S - prev.S) * (0 - prev.net) / (net - prev.net); if (flip == null || Math.abs(x - spot) < Math.abs(flip - spot)) flip = x; } prev = { S, net }; } 

Clamp T to at least half a day, or same-day (0DTE) contracts blow up the gamma.

Call wall and put wall

Group GEX by strike. Within about 25% of the price, the strike with the largest positive call GEX is the call wall (often resistance) and the one with the most negative put GEX is the put wall (often support).

What it says right now

Using Friday's close (spot 754.37) and the three nearest SPY expiries:

SPY net GEX -$887M per 1% regime: negative gamma flip 763.47 spot is 1.2% below it call wall 760, put wall 758 max pain (nearest expiry) 762 

SPX told the same story: net GEX of -$11.7B per 1% and a flip at 7615 with spot at 7562. TSLA was the opposite: positive GEX, spot above a flip at 380.6.

Caveats

  • Open interest updates once a day, overnight, so intraday GEX moves only through price and IV.
  • The "dealers are short everything" assumption is a simplification. Real positioning differs, especially in single stocks with heavy call buying.
  • Quotes are delayed about 15 minutes. That's fine for daily levels, not for scalping.

If you'd rather not run it yourself

I packaged this as an Apify actor (schedule it, export to a sheet or webhook) and as a REST API on RapidAPI with a free tier. The same Cboe chain also powers an earnings expected move endpoint: the at-the-money straddle on the first expiry after each report. This week that's ±11.1% for PANW and ±8.8% for NVDA.

The math above is everything that's in there. If you build your own version, I'd like to hear how your numbers compare.