DEV Community

tomasz dobrowolski
tomasz dobrowolski

Posted on • Originally published at flashalpha.com

I Built a 5-Minute VRP Trading Scanner in Python — Here's the Code

Options implied volatility overestimates realized vol. This is the variance risk premium (VRP) — and it's one of the most persistent edges in financial markets.

The problem: most people who know this still lose money selling premium. Not because the edge isn't there, but because they lack a repeatable process for deciding when, what, and how much to trade.

I built a 5-step daily workflow that answers all three questions using two API calls per symbol. It runs in under 5 minutes. Here's the full code.

The Stack

  • Data: FlashAlpha API — pre-computed VRP analytics, GEX regime data, dealer positioning levels across 4,000+ US equities/ETFs
  • Language: Python (also works in JS/cURL)
  • Dependencies: requests (that's it)

Step 1: Screen for VRP Signals

The /v1/vrp/{symbol} endpoint returns a full VRP dashboard per symbol. The two fields that matter for screening:

  • z_score — how many standard deviations above the 252-day mean. Above +1.0 = elevated.
  • vrp_20d — raw IV minus realized vol over 20 days. Positive = premium exists.
import requests

API_KEY = "YOUR_API_KEY"
BASE    = "https://lab.flashalpha.com"
HEADERS = {"X-Api-Key": API_KEY}

WATCHLIST = ["SPY", "QQQ", "IWM", "AAPL", "TSLA", 
             "NVDA", "AMZN", "META", "MSFT", "AMD"]

signals = []

for sym in WATCHLIST:
    r = requests.get(f"{BASE}/v1/vrp/{sym}", headers=HEADERS)
    if r.status_code != 200:
        continue
    d = r.json()
    vrp = d.get("vrp", {})

    z      = vrp.get("z_score", 0)
    spread = vrp.get("vrp_20d", 0)

    if z >= 1.0 and spread > 2.0:
        signals.append({
            "symbol": sym, 
            "z_score": z,
            "vrp_20d": spread,
            "atm_iv": vrp.get("atm_iv", 0),
            "rv_20d": vrp.get("rv_20d", 0),
            "_raw": d,
        })

signals.sort(key=lambda x: x["z_score"], reverse=True)
print(f"Found {len(signals)} symbols with actionable VRP")
Enter fullscreen mode Exit fullscreen mode

Filter logic: z-score ≥ 1.0 AND vrp_20d > 2.0 vol points. Conservative on purpose. A z-score of +2.0 on a 1-point spread isn't worth the transaction costs.

Step 2: Classify the Gamma Regime

A high VRP signal without regime context is incomplete. The same API response includes GEX-conditioned data — no extra call needed.

The key insight: dealer gamma positioning determines whether your short vol trade gets dampened (positive gamma = dealers buy your dips) or amplified (negative gamma = dealers sell into your drawdown).

for s in signals:
    d = s["_raw"]
    regime = d.get("regime", {})

    s["gamma_regime"] = regime.get("gamma", "unknown")
    s["gamma_flip"]   = regime.get("gamma_flip", 0)
    s["spot"]         = d.get("underlying_price", 0)

    # The 4-cell GEX-VRP matrix
    pos_gamma = s["gamma_regime"] == "positive_gamma"
    high_vrp  = s["z_score"] >= 1.0

    if pos_gamma and high_vrp:
        s["cell"], s["size"] = "A", 1.75   # Premium Paradise
    elif not pos_gamma and high_vrp:
        s["cell"], s["size"] = "B", 0.50   # Tempting Trap
    elif pos_gamma and not high_vrp:
        s["cell"], s["size"] = "C", 0.50   # Grind It Out
    else:
        s["cell"], s["size"] = "D", 0.0    # No Trade

signals = [s for s in signals if s["cell"] != "D"]
Enter fullscreen mode Exit fullscreen mode

This is the core decision matrix:

High VRP (z ≥ 1.0) Low VRP
+Gamma Cell A — full size, any structure Cell C — half size, tight structures
-Gamma Cell B — half size, defined risk only Cell D — no trade

Cell A is where the magic happens: dealers dampen moves (suppressing realized vol) while implied vol is elevated. Your short premium position benefits on both sides.

Cell B is the trap that blows up accounts. Premium looks rich, but dealers are amplifying moves against you.

Step 3: Pick the Structure

The VRP response includes strategy_scores — five common premium structures scored 0–100 based on current VRP, skew, term structure, and regime:

for s in signals:
    scores = s["_raw"].get("strategy_scores", {})
    directional = s["_raw"].get("directional", {})

    s["put_vrp"]  = directional.get("downside_vrp", 0)
    s["call_vrp"] = directional.get("upside_vrp", 0)

    # Cell B = defined risk only
    if s["cell"] == "B":
        eligible = {k: v for k, v in scores.items() 
                    if k in ("iron_condor", "jade_lizard")}
    else:
        eligible = scores

    s["best"] = max(eligible, key=eligible.get) if eligible else "none"
Enter fullscreen mode Exit fullscreen mode

Pro tip: The directional object decomposes VRP into put-side vs. call-side. 70–80% of the time, premium is asymmetric. Defaulting to symmetric iron condors leaves edge on the table. If put VRP is 7.4 and call VRP is 4.2, you want put-heavy structures.

Step 4: Set Entry Levels from Dealer Positioning

Now the second API call — /v1/exposure/levels/{symbol} returns key dealer positioning levels:

for s in signals:
    r = requests.get(
        f"{BASE}/v1/exposure/levels/{s['symbol']}", 
        headers=HEADERS
    )
    if r.status_code != 200:
        continue
    lvl = r.json().get("levels", r.json())

    s["put_wall"]  = lvl.get("put_wall", 0)
    s["call_wall"] = lvl.get("call_wall", 0)
    s["max_gamma"] = lvl.get("max_positive_gamma", 0)
Enter fullscreen mode Exit fullscreen mode
  • Put wall → place short put strikes here (dealers buy at this level = mechanical backstop)
  • Call wall → place short call strikes here (dealers sell at this level = structural ceiling)
  • Max gamma → price magnet, ideal straddle center
  • Gamma flip → the line in the sand. Above = dampened. Below = amplified.

Step 5: Exit Rules (Define Before Entry)

Four non-negotiable rules:

  1. 50% profit target. Captures ~85% of expected P&L with ~40% of the variance. The Sharpe improvement is significant.
  2. Gamma flip breach. If spot drops below the flip, your Cell A trade is now Cell B. Cut half or close.
  3. 21 DTE time stop. Gamma accelerates in the final 3 weeks. The remaining theta isn't worth the path risk.
  4. VRP inversion. If z-score drops below -0.5, realized vol is beating implied. Close immediately.
for s in signals:
    vrp = s["_raw"].get("vrp", {})
    z    = vrp.get("z_score", 0)
    flip = s["gamma_flip"]
    spot = s["spot"]
    dist = ((spot - flip) / spot * 100) if spot > 0 else 0

    if z < -0.5:
        status = "EXIT — VRP inversion"
    elif dist < 0:
        status = "EXIT — below gamma flip"
    elif dist < 0.5:
        status = "WARNING — near flip"
    else:
        status = "OK"

    print(f"{s['symbol']:<6} z={z:+.2f} dist={dist:+.1f}% → {status}")
Enter fullscreen mode Exit fullscreen mode

The Complete Morning Script

All 5 steps in one runnable file → outputs a trade plan with symbol, cell classification, structure, strikes, and sizing. Full script is in the original article.

Sample output:

══════════════════════════════════════════════════════════════════
  VRP MORNING SCAN
══════════════════════════════════════════════════════════════════

──────────────────────────────────────────────────────────────────
  SPY — Cell A: PREMIUM PARADISE
──────────────────────────────────────────────────────────────────
  VRP z-score:    +1.34  (82nd percentile)
  ATM IV:         18.5%   RV20d: 13.0%   Spread: +5.4
  Put VRP:        +7.4   Call VRP: +4.2
  Gamma regime:   positive_gamma   Flip: $572.50   Distance: +1.4%
  Best structure: iron_condor (score: 85)
  Size:           1.75x standard
  Levels: put_wall=$570  call_wall=$590  max_gamma=$575
Enter fullscreen mode Exit fullscreen mode

What You Need

The VRP endpoint, strategy scores, directional VRP, GEX-conditioned regime data, and term VRP are on the Alpha plan. Per-strike GEX and key levels are free.

If you're building systematic premium-selling workflows, Alpha is where the actionable analytics live — VRP z-scores, harvest scores, strategy scoring, vanna outlook, dealer flow risk, and unlimited API calls to run this scanner across your entire universe every morning without throttling.

Links:

Top comments (0)