If you’re building an options trading tool, screening is usually the first thing that gets nasty. You have a universe of tickers, a handful of interesting metrics (IV, GEX, VRP, skew, open interest), and you want to rank them in real time. Doing that yourself means millions of option contracts per day, IV solvers, surface fitting, a scheduler, a database — and then every time a trader says “can you also filter by dealer flow risk?”, you rebuild half of it.
The Live Screener API at POST /v1/screener gives you that entire layer as one request. You send a filter tree, a sort spec, and a list of fields you want back. The server has already pre-computed every metric for ~250 symbols and refreshes them every 5–10 seconds from an in-memory store. There’s no database query at request time and no warm-up lag.
This article explains the data model, the filter grammar, cascading semantics, and formulas — with enough worked examples that you can wire up a screener in an afternoon.
Table of contents
Why one POST endpoint is better than a dozen GETs
If you’ve ever integrated with an options API, the usual pattern is “one endpoint per ticker, loop over your watchlist, join in your client.” That works fine for 10 symbols, but it has two problems:
- N round trips. You pay the network cost N times. For a 250-symbol scan, that’s 250 HTTP requests and whatever the p99 latency of each one happens to be.
- You do the joining. You call
/exposure/summary, /volatility, /vrp, merge them in memory, and only then can you rank. The filter logic lives in your client.
A POST screener inverts both. The server already has every symbol hydrated in memory, so there’s no per-symbol query. And because you send the filter tree as JSON, the server does the join, the filter, the sort, and the pagination. One request in, a ranked page out.
Concretely, a request looks like this:
{
"filters": { ... filter tree ... },
"sort": [ ... sort specs ... ],
"select": [ ... field names ... ],
"formulas": [ ... custom computed fields ... ],
"limit": 50,
"offset": 0
}
Every key is optional. An empty body {} returns every symbol in your plan’s universe with a default field set.
The four-level data model
Every symbol is modelled as a nested tree, and the filter grammar lets you address each level directly.
Stock (1 per symbol)
└─ Expiry aggregates (many per symbol)
└─ Strike aggregates (many per expiry)
└─ Contracts (2 per strike: C and P)
Each level has its own filterable fields, accessed via a dotted prefix:
regime, net_gex, atm_iv, vrp_20d — stock-level scalars (no prefix)
expiries.days_to_expiry, expiries.atm_iv — per-expiry
strikes.call_oi, strikes.net_gex — per-strike inside each expiry
contracts.type, contracts.delta, contracts.oi — individual calls/puts
This is important because it lets you express things like “positive-gamma symbols, only expiries within 14 days, only strikes with call_oi ≥ 2000, only the 30–50 delta call contracts” in a single query. Which brings us to cascading.
Filter grammar: AND, OR, leaves
Filters form a recursive tree. A node is either a leaf (a single condition) or a group (AND / OR with child conditions):
// Leaf
{ "field": "atm_iv", "operator": "gte", "value": 20 }
// Group
{
"op": "and",
"conditions": [
{ "field": "regime", "operator": "eq", "value": "positive_gamma" },
{ "field": "atm_iv", "operator": "gte", "value": 15 }
]
}
Groups nest up to 3 levels deep, with a max of 20 total leaf conditions per query. That’s enough to express basically any reasonable scan, and the ceiling keeps pathological filters from turning the screener into a DDOS surface.
The operators are the ones you’d expect: eq, neq, gt, gte, lt, lte, between (for ranges), in (for lists), is_null, is_not_null. String comparisons are case-insensitive.
Cascading filters and why they matter
Here’s the feature that makes the screener pull its weight: when you combine stock-level + expiry-level + strike-level + contract-level filters inside an AND group, the filter cascades. Non-matching children get trimmed at each level, and symbols with zero survivors are dropped.
{
"filters": {
"op": "and",
"conditions": [
{ "field": "regime", "operator": "eq", "value": "positive_gamma" },
{ "field": "expiries.days_to_expiry", "operator": "lte", "value": 14 },
{ "field": "strikes.call_oi", "operator": "gte", "value": 2000 },
{ "field": "contracts.type", "operator": "eq", "value": "C" },
{ "field": "contracts.delta", "operator": "gte", "value": 0.3 }
]
},
"select": ["*"],
"limit": 20
}
That single query returns:
- Only the symbols where
regime = positive_gamma
- Only their expiries where DTE ≤ 14
- Only the strikes inside those expiries with
call_oi ≥ 2000
- Only the call contracts at those strikes with
delta ≥ 0.3
Without cascading you’d pull every symbol, every expiry, every strike, every contract, and filter in your client. Cascading shifts the entire winnowing process server-side, and the response that comes back is already shaped like the answer.
OR groups behave differently. Nested filters inside an OR use Any() semantics — contracts.delta > 0.3 matches if any contract in the symbol clears the bar. OR does not cascade; the full data comes back for matching symbols.
If the field you want isn’t in the catalog, you can define it inline. Formulas are arithmetic expressions over numeric fields:
{
"formulas": [
{ "alias": "vrp_ratio", "expression": "atm_iv / rv_20d" },
{ "alias": "risk_adj", "expression": "harvest_score / (dealer_flow_risk + 1)" }
],
"sort": [{ "formula": "risk_adj", "direction": "desc" }],
"select": ["symbol", "harvest_score", "dealer_flow_risk", "risk_adj"]
}
Supported: + - * /, parentheses, unary negation, numeric literals, any numeric field name. Max expression length 200 characters. Division by zero returns null, and null inputs propagate through the expression so you never get a surprise 0.
You can also use formulas inline without a formulas array:
{
"filters": {
"op": "and",
"conditions": [
{ "field": "regime", "operator": "eq", "value": "positive_gamma" },
{ "formula": "atm_iv - rv_20d", "operator": "gt", "value": 6 }
]
}
}
Formulas are Alpha-tier only. Growth tier requests with a formulas array throw validation_error.
Pre-computed strategy scores
This is the part that beats writing your own scoring. For every symbol, Alpha-tier responses include a handful of 0–100 strategy scores that are pre-computed every 5–10 seconds:
harvest_score — “is it safe to sell premium here, right now?”. High = rich VRP with friendly dealer regime.
net_harvest_score — harvest score penalized by dealer flow risk.
dealer_flow_risk — estimate of how aggressively dealers will hedge against you.
short_put_spread_score, short_strangle_score, iron_condor_score, calendar_spread_score — strategy-specific attractiveness.
These are the scoring signals that most in-house scanners bolt together manually — rolling z-scores, regime detectors, concentration metrics, skew slopes — and they’re available as fields you can filter and sort on directly.
Alongside the scores, the vrp_regime classifier tags each symbol as harvestable, toxic_short_vol, cheap_convexity, event_only, or surface_distorted — which lets you write scans like “only sell premium when the regime is harvestable, never when it’s toxic_short_vol.”
Five worked examples
1. Negative-gamma alert board
Every morning you want a short list of symbols where dealers are short gamma and already exposed:
{
"filters": { "op": "and", "conditions": [
{ "field": "regime", "operator": "eq", "value": "negative_gamma" },
{ "field": "dealer_flow_risk", "operator": "gte", "value": 50 }
]},
"sort": [{ "field": "dealer_flow_risk", "direction": "desc" }],
"select": ["symbol", "regime", "dealer_flow_risk", "gamma_flip", "net_gex"]
}
2. Harvestable VRP short list
{
"filters": { "op": "and", "conditions": [
{ "field": "regime", "operator": "eq", "value": "positive_gamma" },
{ "field": "vrp_regime", "operator": "eq", "value": "harvestable" },
{ "field": "dealer_flow_risk", "operator": "lte", "value": 40 },
{ "field": "harvest_score", "operator": "gte", "value": 65 }
]},
"sort": [{ "field": "harvest_score", "direction": "desc" }],
"select": ["symbol", "price", "harvest_score", "dealer_flow_risk", "vrp_regime"]
}
3. Macro-conditioned regime scan
“Show me negative-gamma symbols but only when VIX is elevated.”
{
"filters": { "op": "and", "conditions": [
{ "field": "regime", "operator": "eq", "value": "negative_gamma" },
{ "field": "vix", "operator": "gte", "value": 20 }
]},
"select": ["symbol", "regime", "atm_iv", "vix"]
}
4. 0DTE call-seller setup (cascading)
{
"filters": { "op": "and", "conditions": [
{ "field": "expiries.days_to_expiry", "operator": "eq", "value": 0 },
{ "field": "contracts.type", "operator": "eq", "value": "C" },
{ "field": "contracts.delta", "operator": "gte", "value": 0.3 },
{ "field": "contracts.oi", "operator": "gte", "value": 1000 }
]},
"select": ["*"]
}
You get back symbols where, for each symbol, only the 0DTE expiry is included, only the call contracts that clear the delta and OI thresholds are kept. No post-processing needed.
5. Risk-adjusted ranking (formula)
{
"formulas": [
{ "alias": "risk_adj", "expression": "harvest_score / (dealer_flow_risk + 1)" }
],
"filters": { "field": "harvest_score", "operator": "gte", "value": 50 },
"sort": [{ "formula": "risk_adj", "direction": "desc" }],
"select": ["symbol", "price", "harvest_score", "dealer_flow_risk", "risk_adj"],
"limit": 20
}
The +1 keeps the denominator away from zero. This ranks symbols by “how much edge per unit of dealer risk” — a single, defensible ranking axis that combines the scoring and the risk penalty.
When to use the screener (vs the per-symbol endpoints)
The screener isn’t a replacement for the detail endpoints — each serves a different job:
- Screener: “which symbols?” — ranking, filtering, regime detection across the universe.
- Per-symbol endpoints (GEX, VRP, Narrative): “now tell me everything about this one” — per-strike data, surface parameters, narrative, historical context.
A typical workflow is: screener trims your universe of ~250 to 5–10 candidates, then you drill into each one with the detail endpoints for position sizing and entry timing.
Getting started
The screener is live at POST https://lab.flashalpha.com/v1/screener. Pass your API key in the X-Api-Key header:
curl -X POST "https://lab.flashalpha.com/v1/screener" \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"filters": { "field": "regime", "operator": "eq", "value": "negative_gamma" },
"sort": [{ "field": "atm_iv", "direction": "desc" }],
"select": ["symbol", "price", "regime", "atm_iv", "net_gex"],
"limit": 10
}'
For the complete field reference (every level, every tier, every operator), see the Live Screener docs and the Field Taxonomy. For copy-paste recipes, see the Screener Cookbook.
Ready to build with it?
The Live Screener is part of the FlashAlpha Lab API — Growth for a 20-symbol universe, Alpha for ~250 symbols with formulas and strategy scores. Start with the free tier (5 requests/day) and upgrade when you’re ready to scan.
Get an API key →
Compare plans →