DEV Community

tomasz dobrowolski
tomasz dobrowolski

Posted on • Originally published at flashalpha.com

Live Options Screener API — Real-Time Filter, Rank, and Score Across 250 Symbols

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/live 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. No database query at request time. 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.

Why one POST endpoint beats a dozen GETs

The usual pattern with options APIs is "one endpoint per ticker, loop over your watchlist, join in your client." That works fine for 10 symbols. Two problems when you scale:

N round trips. You pay the network cost N times. For a 250-symbol scan, that's 250 HTTP requests.

You do the joining. You call /exposure/summary, /volatility, /vrp, merge them in memory, and only then can you rank.

A POST screener inverts both. The server already has every symbol hydrated in memory. 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.

{
  "filters":  { ... filter tree ... },
  "sort":     [ ... sort specs ... ],
  "select":   [ ... field names ... ],
  "formulas": [ ... custom computed fields ... ],
  "limit":    50,
  "offset":   0
}
Enter fullscreen mode Exit fullscreen mode

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:

Stock (1 per symbol)
 └─ Expiry aggregates (many per symbol)
     └─ Strike aggregates (many per expiry)
         └─ Contracts (2 per strike: C and P)
Enter fullscreen mode Exit fullscreen mode

Each level has its own filterable fields, accessed via a dotted prefix:

  • regime, net_gex, atm_iv, vrp_20d — stock-level (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 means you can express "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.

Filter grammar: AND, OR, leaves

Filters form a recursive tree. A node is either a leaf 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 }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Groups nest up to 3 levels deep, with a max of 20 total leaf conditions per query. Operators: eq, neq, gt, gte, lt, lte, between, in, is_null, is_not_null. String comparisons are case-insensitive.

Cascading filters — the killer feature

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
}
Enter fullscreen mode Exit fullscreen mode

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, and only the call contracts at those strikes with delta ≥ 0.3.

Without cascading you'd pull everything and filter client-side. Cascading shifts the entire winnowing process server-side.

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.

Formulas: your own computed fields

If the field you want isn't in the catalog, define it inline:

{
  "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"]
}
Enter fullscreen mode Exit fullscreen mode

Supported: + - * /, parentheses, unary negation, numeric literals, any numeric field name. Division by zero returns null. You can also use formulas inline in filter conditions:

{ "formula": "atm_iv - rv_20d", "operator": "gt", "value": 6 }
Enter fullscreen mode Exit fullscreen mode

Formulas are Alpha-tier only.

Pre-computed strategy scores

For every symbol, Alpha-tier responses include 0–100 strategy scores refreshed every 5–10 seconds:

  • harvest_score — is it safe to sell premium here, right now?
  • 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

The vrp_regime classifier tags each symbol as harvestable, toxic_short_vol, cheap_convexity, event_only, or surface_distorted — letting you write scans like "only sell premium when harvestable, never when toxic_short_vol."

Five worked examples

1. Negative-gamma alert board

{
  "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"]
}
Enter fullscreen mode Exit fullscreen mode

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"]
}
Enter fullscreen mode Exit fullscreen mode

3. 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": ["*"]
}
Enter fullscreen mode Exit fullscreen mode

4. Macro-conditioned regime scan

{
  "filters": { "op": "and", "conditions": [
    { "field": "regime", "operator": "eq",  "value": "negative_gamma" },
    { "field": "vix",    "operator": "gte", "value": 20 }
  ]},
  "select": ["symbol", "regime", "atm_iv", "vix"]
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

When to use the screener vs per-symbol endpoints

The screener answers "which symbols?" — ranking, filtering, regime detection across the universe. Per-symbol endpoints (GEX, VRP, Narrative) answer "tell me everything about this one" — per-strike data, surface parameters, historical context.

Typical workflow: screener trims ~250 to 5–10 candidates, then you drill into each one with the detail endpoints for position sizing and entry timing.

Getting started

curl -X POST "https://lab.flashalpha.com/v1/screener/live" \
  -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
  }'
Enter fullscreen mode Exit fullscreen mode

Full field reference: Live Screener docs and Field Taxonomy. Copy-paste recipes: Screener Cookbook.

The Live Screener is part of the FlashAlpha Lab API — Growth for a 10-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.

Top comments (0)