VRP Trading in Practice: From Signal to Execution | FlashAlpha

VRP Trading in Practice: From Signal to Execution

A step-by-step playbook for turning VRP signals into live trades. Covers the complete workflow: daily screening, regime classification, structure selection, entry timing, position management, and exit rules. Every step uses live API data from the FlashAlpha VRP and GEX endpoints. Alpha plan.

T
Tomasz Dobrowolski Quant Engineer
Apr 1, 2026
39 min read
VRP VolatilitySelling PremiumSelling OptionsTrading Alpha Workflow GEX

Why Most VRP Traders Fail

They know the theory. IV overestimates realized vol. Sell premium, collect the spread. The edge is real and well-documented across decades of academic research.

But knowing the edge exists and harvesting it consistently are different problems. Most VRP traders fail not because the premium is not there, but because they lack a repeatable process for converting the statistical edge into live trades.

Variance Risk Premium $$ \text{VRP} = \sigma_{\text{implied}}^2 - \mathbb{E}[\sigma_{\text{realized}}^2] $$

The VRP is persistently positive because investors systematically overpay for downside protection. But "persistently positive" does not mean "always profitable on any given trade." The premium varies by regime, by symbol, by term, and by the microstructure of dealer hedging flows. A systematic workflow filters for the conditions where the premium is harvestable - and stays out when it is not.

This article is that workflow. Every step maps to a specific API call. By the end, you will have a system you can run every morning in under 5 minutes.

The Daily Workflow: 5 Steps, 5 Minutes

Here is the full workflow, compressed into a daily routine. Each step is expanded in detail below.

1
Screen
/v1/vrp
2
Classify
GEX regime
3
Structure
strategy_scores
4
Entry
/v1/exposure
5
Manage
exit rules

Total: 2 API calls per symbol (VRP + levels). Unlimited calls on the Alpha plan means you can screen your entire universe every morning without throttling.

Step 1: Screen for VRP Signals

The first question every morning: where is the premium?

The /v1/vrp/{symbol} endpoint returns a complete VRP dashboard for any optionable symbol. The key fields for screening:

  • z_score: How many standard deviations the current VRP sits above the 252-day mean. Above +1.0 = elevated. Above +1.5 = rich.
  • percentile: Where today's VRP sits in the trailing year distribution. Above 75th = interesting. Above 90th = rare opportunity.
  • vrp_5d through vrp_30d: The spread between ATM IV and realized vol across multiple lookback windows. 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)
    pct    = vrp.get("percentile", 0)
    spread = vrp.get("vrp_20d", 0)

    if z >= 1.0 and spread > 2.0:
        signals.append({
            "symbol": sym, "z_score": z, "percentile": pct,
            "vrp_20d": spread, "atm_iv": vrp.get("atm_iv", 0),
            "rv_20d": vrp.get("rv_20d", 0), "_raw": d,   # keep full response for Step 2
        })

signals.sort(key=lambda x: x["z_score"], reverse=True)
const API_KEY = "YOUR_API_KEY";
const BASE    = "https://lab.flashalpha.com";
const HEADERS = { "X-Api-Key": API_KEY };

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

const signals = [];

for (const sym of WATCHLIST) {
    const res = await fetch(`${BASE}/v1/vrp/${sym}`, { headers: HEADERS });
    if (!res.ok) continue;
    const d = await res.json();
    const vrp = d.vrp ?? {};

    if (vrp.z_score >= 1.0 && vrp.vrp_20d > 2.0) {
        signals.push({
            symbol: sym, z_score: vrp.z_score, percentile: vrp.percentile,
            vrp_20d: vrp.vrp_20d, atm_iv: vrp.atm_iv, rv_20d: vrp.rv_20d, _raw: d,
        });
    }
}

signals.sort((a, b) => b.z_score - a.z_score);
curl -s "https://lab.flashalpha.com/v1/vrp/SPY" \
  -H "X-Api-Key: YOUR_API_KEY" | jq '{
    symbol: .symbol,
    z_score: .vrp.z_score,
    percentile: .vrp.percentile,
    vrp_20d: .vrp.vrp_20d,
    atm_iv: .vrp.atm_iv,
    rv_20d: .vrp.rv_20d
  }'
$ pip install requests  |  $ npm install node-fetch
Screening filter: z-score ≥ 1.0 AND vrp_20d > 2.0 vol points. This is deliberately conservative. You want symbols where the premium is both statistically elevated (z-score) and practically meaningful (raw spread). A z-score of +2.0 on a 1-point spread is not worth the transaction costs.

Step 2: Classify the Gamma Regime

A high VRP signal without regime context is incomplete. Step 2 determines whether the market microstructure supports your trade.

The /v1/vrp/{symbol} response already includes GEX-conditioned data - no extra API call needed. The key fields from the same response:

  • gex_conditioned.regime: positive_gamma or negative_gamma
  • gex_conditioned.harvest_score: 0 to 1.0 - how favorable the current environment is for premium harvesting
  • regime.gamma_flip: The exact price level where dealer gamma flips sign
  • dealer_flow_risk: 0 to 100 - how much risk dealer flows pose to short vol positions
# No extra API call  - use the data already fetched in Step 1
for s in signals:
    d = s["_raw"]

    gex_cond = d.get("gex_conditioned", {})
    regime   = d.get("regime", {})

    s["gamma_regime"]  = regime.get("gamma", "unknown")
    s["gamma_flip"]    = regime.get("gamma_flip", 0)
    s["harvest_score"] = gex_cond.get("harvest_score", 0)
    s["dealer_risk"]   = d.get("dealer_flow_risk", 0)
    s["spot"]          = d.get("underlying_price", 0)
    s["dist_to_flip"]  = ((s["spot"] - s["gamma_flip"]) / s["spot"] * 100) if s["spot"] > 0 else 0

    # Classify into 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_mult"] = "A", 1.75
    elif not pos_gamma and high_vrp:
        s["cell"], s["size_mult"] = "B", 0.50
    elif pos_gamma and not high_vrp:
        s["cell"], s["size_mult"] = "C", 0.50
    else:
        s["cell"], s["size_mult"] = "D", 0.0

# Remove Cell D  - no edge
signals = [s for s in signals if s["cell"] != "D"]

The 4-Cell GEX-VRP Matrix

This matrix is covered in depth in GEX-Conditioned VRP. Here is the decision summary:

Cell A - Premium Paradise
Positive gamma + high VRP
Size: 1.5–2x · Any structure · Best risk-adjusted returns
Cell B - Tempting Trap
Negative gamma + high VRP
Size: 0.5x · Defined risk only · Wider strikes
Cell C - Grind It Out
Positive gamma + low VRP
Size: 0.5x · Calendars, tight condors · Thin edge
Cell D - Stay Home
Negative gamma + low VRP
Size: 0x · No trade · Expected value is negative

See the VRP z-score and gamma regime for any symbol right now

The VRP dashboard, GEX conditioning, and strategy scores are Alpha-exclusive analytics.

View SPY Live

Step 3: Select the Optimal Structure

The VRP endpoint includes a strategy_scores object that scores five common premium-selling structures on a 0–100 scale based on current conditions - VRP level, skew shape, term structure slope, and gamma regime:

for s in signals:
    d = s["_raw"]

    scores      = d.get("strategy_scores", {})
    directional = d.get("directional", {})

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

    # Best structure = highest score (respecting Cell B defined-risk constraint)
    if s["cell"] == "B":
        eligible = {k: v for k, v in scores.items() if k in ("iron_condor", "jade_lizard", "calendar_spread")}
    else:
        eligible = scores

    s["best_structure"] = max(eligible, key=eligible.get) if eligible else "none"
    s["best_score"]     = eligible.get(s["best_structure"], 0)
Strategy Scores - SPY (Cell A)

short_straddle ████████████████████████████████████████ 78
short_strangle ██████████████████████████████████████████ 82
iron_condor ██████████████████████████████████████████████ 85
calendar_spread ██████████████████████████████████ 61
jade_lizard █████████████████████████████████████████ 74

Best: iron_condor (85)   Put VRP: +7.35   Call VRP: +4.21

Directional VRP Determines Structure Bias

The directional object decomposes VRP into put-side and call-side components. This is critical - most of the time, the premium is asymmetric:

Directional Decomposition $$ \text{VRP}_{\text{put}} = \sigma_{\text{IV}}^{\text{25}\Delta\text{P}} - \sigma_{\text{RV}}^{\text{down}} \qquad \text{VRP}_{\text{call}} = \sigma_{\text{IV}}^{\text{25}\Delta\text{C}} - \sigma_{\text{RV}}^{\text{up}} $$
  • Put VRP >> Call VRP: Premium is concentrated in downside protection. Sell put-heavy structures (put spreads, jade lizards). Symmetric iron condors leave edge on the table.
  • Call VRP ≈ Put VRP: Premium is balanced. Straddles and symmetric strangles are appropriate.
  • Call VRP >> Put VRP: Rare, but happens around earnings or squeeze candidates. Call spreads or call-heavy ratio writes.
Do not default to iron condors. Most premium sellers reflexively trade condors. But the directional VRP data shows that 70–80% of the time, the premium is asymmetric. A symmetric condor in an asymmetric market collects less premium than a targeted put spread - while taking risk on the wing that has no edge.

Step 4: Set Entry Levels Using Dealer Positioning

You know what to trade and which structure. Now: where and when to enter.

The /v1/exposure/levels/{symbol} endpoint returns the key dealer positioning levels that act as mechanical support and resistance:

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["call_wall"]  = lvl.get("call_wall", 0)
    s["put_wall"]   = lvl.get("put_wall", 0)
    s["max_gamma"]  = lvl.get("max_positive_gamma", 0)
    s["oi_magnet"]  = lvl.get("highest_oi_strike", 0)

Level-Based Entry Rules

LevelDefinitionHow to Use It
Put wallHighest put gamma concentrationPlace short put strikes here. Dealers buy at this level - your short put has a mechanical backstop.
Call wallHighest call gamma concentrationPlace short call strikes here. Dealer selling caps upside - your short call has a structural ceiling.
Max gammaStrongest dampening strikeThe "magnet" - price gravitates here in positive gamma. Ideal straddle center.
OI magnetHighest open interest strikeExpiration pin target. Most relevant for weekly and 0DTE structures.
Gamma flipRegime boundaryEnter only when spot is above. Distance from spot to flip = your margin of safety.

Entry Timing Tiers

Ideal
z ≥ 1.5, spot at max gamma, >2% above flip
Acceptable
z ≥ 1.0, spot above flip, score >75
Wait
Spot within 0.5% of flip. Regime could shift.

Step 5: Define Management Rules and Exits

Every trade needs four exit rules defined before entry. No exceptions.

Rule 1: Profit Target - 50% of Max Credit

Close at 50% of premium received. Academic research consistently shows that closing at 50% captures the bulk of the edge while avoiding the gamma risk that concentrates in the final days before expiration.

Why 50% Is Optimal $$ \text{E}[\text{P\&L}_{50\%}] \approx 0.85 \times \text{E}[\text{P\&L}_{\text{exp}}] \qquad \text{Var}[\text{P\&L}_{50\%}] \approx 0.40 \times \text{Var}[\text{P\&L}_{\text{exp}}] $$

You capture approximately 85% of the expected profit while reducing variance by 60%. The Sharpe ratio of the 50%-close strategy significantly exceeds the hold-to-expiration strategy.

Rule 2: Regime Change - Gamma Flip Breach

If spot crosses below the gamma flip, your regime has changed from supportive to hostile. This is a non-negotiable adjustment trigger:

  • Cell A → Cell B transition: Reduce to half size. Add long protection to define risk. Or close entirely.
  • Cell B → Cell D transition: Close immediately. The trade has no remaining edge.

Rule 3: Time Stop - 21 DTE for 45-DTE Entries

If neither the profit target nor the regime trigger has been hit by 21 DTE remaining, close the position. Gamma risk accelerates in the final 3 weeks, and the remaining theta is not worth the path-dependent risk.

Rule 4: VRP Inversion - z-Score Below -0.5

If the VRP z-score drops below -0.5 while in a trade, realized vol is exceeding implied vol. The edge has flipped against you. Close immediately.

# Exit rule monitoring
for s in signals:
    d = s["_raw"]

    z    = d.get("vrp", {}).get("z_score", 0)
    flip = d.get("regime", {}).get("gamma_flip", 0)
    spot = d.get("underlying_price", 0)
    dist = ((spot - flip) / spot * 100) if spot > 0 else 0

    if z < -0.5:
        alert = "EXIT  - VRP inversion (z < -0.5)"
    elif dist < 0:
        alert = "EXIT  - spot below gamma flip"
    elif dist < 0.5:
        alert = "WARNING  - approaching gamma flip"
    else:
        alert = "OK  - regime stable"

    print(f"{s['symbol']:<6} z={z:+.2f}  flip=${flip:.2f}  dist={dist:+.1f}%  -> {alert}")

The Complete Morning Script

Here is the full workflow as a single, runnable script. It screens your watchlist, classifies regimes, selects structures, maps dealer levels, and outputs a trade plan:

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"]

def get_vrp(symbol):
    r = requests.get(f"{BASE}/v1/vrp/{symbol}", headers=HEADERS)
    return r.json() if r.status_code == 200 else None

def get_levels(symbol):
    r = requests.get(f"{BASE}/v1/exposure/levels/{symbol}", headers=HEADERS)
    if r.status_code != 200:
        return {}
    data = r.json()
    return data.get("levels", data)

def classify(regime, z):
    pos  = regime == "positive_gamma"
    high = z >= 1.0
    if pos and high:     return "A", "PREMIUM PARADISE", 1.75
    if not pos and high: return "B", "TEMPTING TRAP", 0.50
    if pos and not high: return "C", "GRIND", 0.50
    return "D", "NO TRADE", 0.0

print("=" * 70)
print("  VRP MORNING SCAN")
print("=" * 70)

for sym in WATCHLIST:
    vrp_data = get_vrp(sym)
    if not vrp_data:
        continue

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

    if z < 0.5:
        continue

    regime_data = vrp_data.get("regime", {})
    gamma       = regime_data.get("gamma", "unknown")
    flip        = regime_data.get("gamma_flip", 0)
    spot        = vrp_data.get("underlying_price", 0)

    cell, label, size = classify(gamma, z)

    if cell == "D":
        print(f"\n  {sym}: Cell D  - NO TRADE (z={z:+.2f}, {gamma})")
        continue

    scores      = vrp_data.get("strategy_scores", {})
    directional = vrp_data.get("directional", {})
    gex_cond    = vrp_data.get("gex_conditioned", {})
    levels      = get_levels(sym)

    eligible = {k: v for k, v in scores.items()
                if k in ("iron_condor", "jade_lizard", "calendar_spread")} if cell == "B" else scores
    best = max(eligible, key=eligible.get) if eligible else "none"

    dist_flip = ((spot - flip) / spot * 100) if spot > 0 else 0

    print(f"\n{'─' * 70}")
    print(f"  {sym}  - Cell {cell}: {label}")
    print(f"{'─' * 70}")
    print(f"  VRP z-score:    {z:+.2f}  ({vrp.get('percentile', 0)}th percentile)")
    print(f"  ATM IV:         {vrp.get('atm_iv', 0):.1f}%   RV20d: {vrp.get('rv_20d', 0):.1f}%   Spread: {spread:+.1f}")
    print(f"  Put VRP:        {directional.get('downside_vrp', 0):+.1f}   Call VRP: {directional.get('upside_vrp', 0):+.1f}")
    print(f"  Gamma regime:   {gamma}   Flip: ${flip:.2f}   Distance: {dist_flip:+.1f}%")
    print(f"  Harvest score:  {gex_cond.get('harvest_score', 0):.2f}   Dealer risk: {vrp_data.get('dealer_flow_risk', 0)}")
    print(f"  Best structure: {best} (score: {eligible.get(best, 0)})")
    print(f"  Size:           {size:.2f}x standard")
    print(f"  Levels:  put_wall=${levels.get('put_wall', 0):.2f}  call_wall=${levels.get('call_wall', 0):.2f}  "
          f"max_gamma=${levels.get('max_positive_gamma', 0):.2f}  oi_magnet=${levels.get('highest_oi_strike', 0):.2f}")

print(f"\n{'=' * 70}")
print("  SCAN COMPLETE")
print(f"{'=' * 70}")
$ python vrp_scan.py
======================================================================
  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%
  Harvest score: 0.82 Dealer risk: 15
  Best structure: iron_condor (score: 85)
  Size: 1.75x standard
  Levels: put_wall=$570.00 call_wall=$590.00 max_gamma=$575.00 oi_magnet=$575.00

  NVDA - Cell B: TEMPTING TRAP (z=+1.62, negative_gamma)
  ⚠ Defined risk only - 0.50x size

Real-World Example: SPY Cell A Trade Construction

Applying the full workflow to the SPY scan result above:

Market State
Spot$580.51
ATM IV18.45%
RV 20d13.04%
VRP z-score+1.34
Gamma regimepositive
Gamma flip$572.50
Trade Plan
CellA - Premium Paradise
StructurePut-skewed iron condor
Short put$570 (put wall)
Short call$590 (call wall)
Wings$565 / $595 ($5 wide)
DTE30–45 days
Size1.5x (Cell A)

Management Plan

  • Profit target: Close at 50% of credit received
  • Regime trigger: If SPY drops below $572.50 (gamma flip), cut to half size or close
  • Time stop: Close at 21 DTE if neither profit nor regime trigger hit
  • VRP inversion: Close if z-score drops below -0.5

Position Sizing With the Kelly Criterion

The workflow so far uses heuristic sizing (1.75x, 0.5x, etc.). For a more rigorous approach, the Kelly criterion optimizes size based on expected return and variance:

Kelly Fraction $$ f^* = \frac{\mu}{\sigma^2} = \frac{\text{E}[R_{\text{trade}}]}{\text{Var}[R_{\text{trade}}]} $$

In practice, most practitioners use fractional Kelly (typically \(\frac{1}{4}\) to \(\frac{1}{2}\) Kelly) to account for estimation error. The VRP z-score serves as a proxy for \(\mu\) (higher z = higher expected return), and the dealer_flow_risk score serves as a proxy for variance risk. A practical sizing formula:

def kelly_size(z_score, dealer_risk, base_contracts=10, kelly_fraction=0.25):
    """
    Simplified Kelly-inspired sizing using VRP z-score and dealer risk.

    z_score:       VRP z-score (higher = more premium)
    dealer_risk:   0-100 (higher = more adverse dealer flows)
    base_contracts: standard position size in contracts
    kelly_fraction: fraction of full Kelly (0.25 = quarter Kelly)
    """
    # Expected edge scales with z-score
    mu = max(z_score - 0.5, 0) / 3.0  # normalize to ~0-1 range

    # Variance proxy: dealer risk inverted (low risk = low variance)
    sigma_sq = 0.2 + (dealer_risk / 100) * 0.8  # range 0.2 to 1.0

    # Kelly fraction
    f_star = mu / sigma_sq if sigma_sq > 0 else 0
    f_adj  = f_star * kelly_fraction

    # Clamp to 0-2x standard size
    size_mult = max(0, min(f_adj * 2, 2.0))

    return round(base_contracts * size_mult)

Term VRP: Selecting Optimal DTE

The VRP endpoint includes a term_vrp array that shows the premium across multiple expirations. This determines the optimal DTE for your trade:

term_vrp = vrp_data.get("term_vrp", [])

print(f"{'DTE':>5} {'IV':>7} {'RV':>7} {'VRP':>7} {'Edge/Day':>9}")
print("-" * 40)
for t in term_vrp:
    edge_per_day = t["vrp"] / t["dte"] if t["dte"] > 0 else 0
    print(f"{t['dte']:>4}d {t['iv']:>6.1f}% {t['rv']:>6.1f}% {t['vrp']:>+6.1f} {edge_per_day:>8.3f}")

# Select DTE with highest edge/day (theta efficiency)
best_dte = max(term_vrp, key=lambda t: t["vrp"] / t["dte"] if t["dte"] > 0 else 0)
print(f"\nOptimal DTE: {best_dte['dte']} days (VRP: {best_dte['vrp']:+.1f}, edge/day: {best_dte['vrp']/best_dte['dte']:.3f})")
Edge per day measures theta efficiency - how much VRP you capture per day of exposure. Shorter DTE typically has higher edge/day but more gamma risk. The sweet spot for most premium sellers is 30–45 DTE, where edge/day is still meaningful but gamma has not yet accelerated.

Adapting the Workflow by Cell

ParameterCell ACell BCell C
Size1.5–2x0.5x0.5x
StructureAny - straddles, strangles, naked putsDefined risk only - condors, spreadsCalendars, tight condors
DTE30–45 days14–21 days (shorter exposure)30–45 days
Profit target50% of credit30% of credit (take money and run)50% of credit
Strike placementAt put/call walls1.5x further OTM than Cell A10–15 delta
Expected outcomeHighest win rate, smallest drawdownsModerate win rate, large tail riskModest win rate, thin edge

Common Mistakes

MistakeWhy It HurtsThe Fix
Selling based on VRP alone Ignoring gamma regime leads to Cell B blowups Always check gex_conditioned.regime before entering
Defaulting to iron condors Symmetric structure in asymmetric market wastes edge Use directional VRP to match structure to premium location
Holding to expiration Gamma risk concentrates in final 2 weeks Close at 50% profit or 21 DTE - whichever comes first
Ignoring the gamma flip Regime change invalidates your thesis Monitor distance to flip daily. Adjust on breach.
Same size every trade Cell A and Cell B have very different risk profiles Size by cell or use Kelly criterion with dealer_flow_risk
Ignoring term VRP Suboptimal DTE selection leaves edge on the table Use term_vrp to select the expiration with highest edge/day
α Alpha Plan - Built for This Workflow

The VRP endpoint (/v1/vrp), strategy scores, directional VRP decomposition, term VRP, GEX-conditioned regime data, and dealer flow risk scoring are all Alpha-exclusive. Combined with unlimited API requests, Alpha gives you everything this workflow needs - no throttling, no missing data points, no compromises.

$1,199/month billed annually. 20% discount vs. monthly.

View Alpha Plan

Related Reading

Live Market Pulse

Get tick-by-tick visibility into market shifts with full-chain analytics streaming in real time.

Intelligent Screening

Screen millions of option pairs per second using your custom EV rules, filters, and setups.

Execution-Ready

Instantly send structured orders to Interactive Brokers right from your scan results.

Join the Community

Discord

Engage in real time conversations with us!

Twitter / X

Follow us for real-time updates and insights!

GitHub

Explore our open-source SDK, examples, and analytics resources!