Here is a bug I see in client code at least once a month:
# DO NOT do this
gex_now = api.get("/v1/flow/summary/SPY")["live_gex"]
gex_open = api.get("/v1/exposure/gex/SPY")["net_gex"]
intraday_move = gex_now - gex_open # ❌ meaningless
net_gex and live_gex look interchangeable. They are not. They come from two different surfaces of the API, computed against two different open interest numbers, answering two different questions. Subtracting one from the other gives you a quantity that means nothing — not "intraday gamma move", not "change since open", just noise. This post is the canonical explainer of where each number comes from and how to keep them apart.
Two questions, two numbers
Dealer-positioning analytics has exactly one input that matters: how many contracts are open at each strike, and on which side dealers sit. The instant you pick which open interest figure to feed the gamma math, you have implicitly chosen which question you are answering.
-
/v1/exposure/*— computes against settled OI, the morning-broadcast OPRA value. Stable all session by construction. -
/v1/flow/*— computes against effective OI, the settled value plus an intraday simulator estimate of position change driven by today's flow.
A backtest that has to match what was knowable at the open uses the settled surface. A live dashboard that wants to react to today's order flow before tomorrow's OI print uses the flow surface. The mistake is assuming there should be one canonical number.
Settled OI: the morning print that does not move
Settled OI is the official open interest figure computed overnight by the clearing process and broadcast in the morning. It is the same value the OPRA tape reports per contract. The /v1/exposure/* endpoints compute gamma, delta, vanna, and charm exposure against it, so the settled GEX you read at 9:45 AM is the same settled GEX you read at 3:30 PM.
That stability is the feature, not the bug. End-of-day reconciliation, backtest parity, and any analysis that has to agree with the official tape depend on the OI not drifting intraday. When developers say "the morning gamma print looks stale by noon", what they almost always mean is "I expected it to react to today's flow." It doesn't — that's what the flow surface is for.
Effective OI: settled plus an intraday flow estimate
The flow surface starts from the same settled OI and adds an intraday simulator estimate of how many contracts today's flow has opened or closed. The simulator side-classifies trade volume into buys and sells, then applies a confidence weight (currently 0.43) to estimate how many contracts were actually opened versus merely traded between existing holders.
Per contract, the simulator tracks this chain:
| Field | Meaning | Stability |
|---|---|---|
official_oi |
Last OPRA settled OI. Identical to what /v1/exposure/* uses. |
Stable all session |
intraday_oi_delta |
Signed estimate of contracts opened (+) or closed (−) today, from side-classified flow. | Updates intraday |
oi_delta_confidence |
0.43 — fraction of side-classified volume assumed to open new positions vs. reshuffle existing ones. |
Model constant |
simulated_oi |
official_oi + intraday_oi_delta. Unclamped — can go negative if the model overshoots on a heavy-close contract. Diagnostic only. |
Updates intraday |
effective_oi |
max(0, simulated_oi). The analytics-safe input fed into GEX, DEX, and wall computation on the flow surface. |
Updates intraday |
Start from settled, apply a confidence-weighted estimate of today's opening flow to get an unclamped simulated value, then floor at zero so the analytics never see a negative position count. simulated_oi is exposed for diagnostics; effective_oi is what the gamma math actually consumes.
The property that matters: independence
Nothing under /v1/flow/* changes anything under /v1/exposure/*. The OPRA OI number is not modified, overwritten, or "corrected". The settled surface is computed from official_oi and is mathematically untouched by the simulator. You can call the flow endpoints all day and your exposure numbers will not move because of it.
The field names are deliberately disambiguated so you cannot mix them by accident:
Settled (/v1/exposure/*) |
Flow (/v1/flow/*) |
|---|---|
gex |
live_gex |
net_gex |
live_net_gex |
Computed from official_oi
|
Computed from effective_oi
|
If you see live_gex in a payload, it's the flow surface. If you see gex or net_gex, it's the settled surface. There is no shared mutable state between them.
One nuance worth flagging: the flow-side field name is endpoint-specific. /v1/flow/summary/{symbol} returns live_gex (net dealer gamma on effective OI). /v1/flow/gex/{symbol} returns live_net_gex alongside live_net_gex_label, live_gamma_flip, and per-strike call_oi/put_oi on effective OI. Their settled mirrors are gex and net_gex respectively.
The right way to call both
import requests
BASE = "https://lab.flashalpha.com"
HEADERS = {"X-Api-Key": "YOUR_KEY"}
# Settled surface — morning-broadcast OPRA OI, stable all session
settled = requests.get(f"{BASE}/v1/exposure/gex/SPY", headers=HEADERS).json()
# Flow surface — effective OI (settled + simulator delta), updates intraday
flow = requests.get(f"{BASE}/v1/flow/summary/SPY", headers=HEADERS).json()
print(f"Settled net GEX: {settled['net_gex']:,}")
print(f"Live (flow) net GEX: {flow['live_gex']:,}")
print(f"Flow direction: {flow['flow_direction']}")
print(f"Intraday OI delta: {flow['intraday_oi_delta']:+,} contracts")
print(f"GEX pct shift: {flow['flow_gex_pct_shift']}")
Two payloads, two field-name conventions, no cross-contamination. If you want a number that represents "how much today's flow has shifted the dealer regime", use flow_gex_pct_shift from the flow surface — not arithmetic across surfaces.
A representative /v1/flow/summary/SPY response (illustrative, not live market data):
{
"symbol": "SPY",
"as_of": "2026-05-15T16:30:45Z",
"underlying_price": 597.50,
"expiry": "2026-05-15",
"flow_direction": "amplifying",
"intraday_oi_delta": 12450,
"contracts_with_flow": 1842,
"contracts_total": 4586,
"live_gex": 12500000000,
"flow_gex_pct_shift": 0.067
}
The field is live_gex, not gex. The settled /v1/exposure/gex/SPY response for the same instant carries net_gex and is computed entirely from official_oi — unchanged whether or not you ever called the flow endpoint.
The flow_direction enum
/v1/flow/summary and /v1/flow/dealer-risk return a flow_direction classification summarizing what today's flow is doing to the dealer gamma regime:
| Value | Meaning |
|---|---|
no_flow |
Zero per-contract movement on every contract (contracts_with_flow == 0). Distinct from neutral — literally nothing to simulate. |
neutral |
Flow exists, but resulting net GEX shift is under the 5% threshold. Movement present but immaterial. |
amplifying |
Net GEX kept the same sign and grew in magnitude. Today's flow is making dealers more exposed in the existing regime. |
dampening |
Net GEX kept the same sign but shrank in magnitude. Positions are resolving and the regime is weakening. |
regime_flip |
Net GEX changed sign (positive ↔ negative gamma), or a regime was created from a zero baseline. |
The distinction between no_flow and neutral matters in code: no_flow means the simulator had no input at all, neutral means it ran but the effect was below the materiality threshold. Different alerts.
When flow_gex_pct_shift is null
flow_gex_pct_shift expresses the live GEX move as a fraction of the settled GEX. Because it's a ratio against the settled value, there's an edge case where it's mathematically undefined:
-
null— settled GEX is zero but live GEX is nonzero. The simulator created a regime from no baseline, so "percent shift relative to settled" has no denominator. Pairs withflow_direction: "regime_flip". -
0— both settled and live GEX are zero. No regime, no shift. -
A finite number (e.g.
0.067) — both values nonzero, ratio well-defined.
Handle null explicitly. Coercing it to zero will mask exactly the scenario you most want to detect: a fresh regime forming from flow with no prior settled baseline.
Filtering by expiry
Every /v1/flow/* endpoint accepts an optional ?expiry=YYYY-MM-DD parameter. When supplied, the chain is filtered to that single expiry before the simulator and gamma math run, so the returned live_gex reflects only that expiry. Omit it to aggregate across all expirations. An invalid date format returns 400 {"error":"invalid_expiry"}; a valid date with no contracts returns 404.
Mental model
- Two surfaces. Two questions. Two field-name conventions.
-
/v1/exposure/*→ settled OPRA OI →gex,net_gex→ stable all session. -
/v1/flow/*→ effective OI (settled + clamped simulator delta) →live_gex,live_net_gex→ updates intraday. - Independence is the property: flow calls do not move exposure values. Ever.
- Don't subtract across surfaces. Use
flow_gex_pct_shift. - Handle
flow_gex_pct_shift: nullexplicitly — that's a regime built from a zero baseline.
Free tier covers settled GEX and levels with no card if you want to call both and watch them diverge: flashalpha.com/pricing.
Originally published on flashalpha.com.
Top comments (0)