Help us double down on what's working, instead of guessing. Takes 5 seconds, totally optional.
Build an IV Rank Scanner with an API: Implied Volatility Screener for Developers
How to build an IV rank scanner using the FlashAlpha API. Scan 6,000+ US stocks for elevated implied volatility, find premium-selling opportunities, and build automated IV alerts. One POST /v1/screener call ranks your whole universe by the iv_rank_30d metric alongside ATM IV, VRP, and gamma regime.
This guide walks through building a complete IV rank scanner from scratch: single-ticker lookups, batch scanning across a watchlist, high-IV alert systems, sector-based screening, and automated daily notifications. Every code sample runs against the live API.
IV Rank vs IV Percentile: Quick Refresher
Before building the scanner, make sure you're clear on the two metrics you'll be scanning for. They answer related but different questions.
IV Rank measures where current implied volatility sits within its 52-week range:
IV Rank = (Current IV - 52-Week Low IV) / (52-Week High IV - 52-Week Low IV) * 100
An IV rank of 80 means current IV is 80% of the way between its annual low and annual high. It tells you how elevated IV is relative to its own extremes.
IV Percentile measures what percentage of trading days over the past year had lower IV than today:
IV Percentile = (Number of days with IV below current IV) / (Total trading days in lookback) * 100
An IV percentile of 87 means IV was lower than today on 87% of trading days in the past year. It tells you how unusual the current IV level is.
Why both matter: A stock that had one spike to 200% IV and otherwise traded at 20% will show a low IV rank even at 40% IV (because the range is huge), but a high IV percentile (because 40% is above the typical level). IV percentile is generally more robust for screening because it's less sensitive to outlier spikes. Most professional scanners use both.
The API Approach
The primary endpoint for IV scanning is POST /v1/screener (Growth plan and above). IV rank is exposed there as the iv_rank_30d metric, so a single structured query ranks your entire universe in one call instead of looping ticker-by-ticker. Each returned row can carry everything you need for IV screening:
iv_rank_30d - 30-day IV rank (0-100), the field you sort and filter on
atm_iv - current at-the-money implied volatility
vrp_assessment / vrp_percentile - whether IV is rich or cheap vs realized vol
Use GET /v1/screener/fields (Free) to list every queryable field and its type. Note that IV rank is a cross-sectional screener metric (iv_rank_30d); it is not a field on /v1/stock/{symbol}/summary. IV percentile is a related concept covered below, but the screener metric you rank on is iv_rank_30d.
For per-symbol volatility detail, GET /v1/volatility/{symbol} (Growth+ plan) provides realized vol across multiple windows (5d-60d), IV-RV spreads with assessments, skew profiles (25-delta risk reversal, butterfly), term structure, and OI concentration data.
To inspect the available universe, GET /v1/tickers (Free) returns all available tickers - currently 6,000+ US equities and ETFs.
from flashalpha import FlashAlpha
fa = FlashAlpha("YOUR_KEY")
rows = fa.screener(
sort=[("iv_rank_30d", "desc")],
select=["symbol", "iv_rank_30d", "atm_iv", "vrp_assessment"],
limit=10,
)
for r in rows:
print(f"{r['symbol']:<6} IV Rank {r['iv_rank_30d']:>3} "
f"ATM IV {r['atm_iv']:.1f}% VRP {r['vrp_assessment']}")
import { FlashAlpha } from 'flashalpha';
const fa = new FlashAlpha("YOUR_KEY");
const rows = await fa.screener({
sort: [{ field: "iv_rank_30d", direction: "desc" }],
select: ["symbol", "iv_rank_30d", "atm_iv"],
limit: 10,
});
rows.forEach(r => console.log(`${r.symbol}: IV Rank ${r.iv_rank_30d}, ATM IV ${r.atm_iv}%`));
using FlashAlpha;
var fa = new FlashAlphaClient("YOUR_KEY");
var rows = await fa.ScreenerAsync(new ScreenerQuery {
Sort = new[] { new Sort("iv_rank_30d", "desc") },
Select = new[] { "symbol", "iv_rank_30d", "atm_iv" },
Limit = 10,
});
foreach (var r in rows)
Console.WriteLine($"{r["symbol"]}: IV Rank {r["iv_rank_30d"]}, ATM IV {r["atm_iv"]}%");
package main
import (
"context"
"fmt"
flashalpha "github.com/FlashAlpha-lab/flashalpha-go"
)
func main() {
fa := flashalpha.NewClient("YOUR_KEY")
rows, _ := fa.Screener(context.Background(), flashalpha.ScreenerQuery{
Sort: []flashalpha.Sort{{Field: "iv_rank_30d", Direction: "desc"}},
Select: []string{"symbol", "iv_rank_30d", "atm_iv"},
Limit: 10,
})
for _, r := range rows {
fmt.Printf("%s: IV Rank %v, ATM IV %v%%\n", r["symbol"], r["iv_rank_30d"], r["atm_iv"])
}
}
import com.flashalpha.FlashAlphaClient;
import com.flashalpha.ScreenerQuery;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
FlashAlphaClient fa = new FlashAlphaClient("YOUR_KEY");
JsonArray rows = fa.screener(new ScreenerQuery()
.sortDesc("iv_rank_30d")
.select("symbol", "iv_rank_30d", "atm_iv")
.limit(10));
for (var el : rows) {
JsonObject r = el.getAsJsonObject();
System.out.println(r.get("symbol").getAsString()
+ ": IV Rank " + r.get("iv_rank_30d").getAsInt()
+ ", ATM IV " + r.get("atm_iv").getAsDouble() + "%");
}
$ pip install flashalpha | npm install flashalpha | dotnet add package FlashAlpha | go get github.com/FlashAlpha-lab/flashalpha-go
Building the Scanner
1. Basic IV Rank Scanner
Restrict the screen to a watchlist and sort by IV rank descending. This is the simplest scanner: find which stocks in your universe currently have the most elevated implied volatility. The screener does the ranking server-side, so the whole watchlist comes back in a single request.
Filter to tickers with IV rank above a threshold. This is the core of any premium-selling screening workflow: find options that are expensive relative to their historical range. A gte filter on iv_rank_30d does the work server-side.
def scan_high_iv(threshold=80, universe=None):
"""Return tickers with IV rank above threshold, ranked desc."""
return fa.screener(
universe={"symbols": universe} if universe else None,
filters={"op": "and", "conditions": [
{"field": "iv_rank_30d", "operator": "gte", "value": threshold},
]},
sort=[("iv_rank_30d", "desc")],
select=["symbol", "iv_rank_30d", "atm_iv", "vrp_assessment", "regime"],
limit=100,
)
# Find stocks with IV rank > 80
high_iv = scan_high_iv(threshold=80)
for a in high_iv:
print(f" {a['symbol']}: IV Rank {a['iv_rank_30d']}, "
f"ATM IV {a['atm_iv']:.1f}%, VRP: {a['vrp_assessment']}")
3. Low IV Scanner: Cheap Options for Long Vol
The opposite play: find tickers where IV is historically low. These are candidates for buying options (straddles, strangles, calendar spreads) when IV is likely to revert higher.
def scan_low_iv(ceiling=20, universe=None):
"""Return tickers with IV rank below ceiling, ranked asc."""
return fa.screener(
universe={"symbols": universe} if universe else None,
filters={"op": "and", "conditions": [
{"field": "iv_rank_30d", "operator": "lte", "value": ceiling},
]},
sort=[("iv_rank_30d", "asc")],
select=["symbol", "iv_rank_30d", "atm_iv", "vrp_assessment"],
limit=100,
)
low_iv = scan_low_iv(ceiling=20)
print(f"\nLow IV candidates ({len(low_iv)} found):")
for c in low_iv:
print(f" {c['symbol']}: IV Rank {c['iv_rank_30d']}, ATM IV {c['atm_iv']:.1f}%")
4. IV Rank + GEX Regime: The Premium-Selling Sweet Spot
High IV alone doesn't mean it's safe to sell premium. Pairing IV rank with the gamma exposure regime adds a crucial layer of context. Positive gamma regime means dealers are long gamma, creating a natural dampening effect on price moves - the ideal environment for premium sellers. Both conditions live on the screener, so you express the whole setup as one filter tree.
Group tickers by sector ETF and scan each group. This surfaces sector-level IV trends: is tech IV elevated while energy IV is depressed? Useful for relative value vol trades and sector rotation strategies.
sectors = {
"Tech": ["AAPL", "MSFT", "GOOGL", "NVDA", "AMD", "CRM", "ADBE"],
"Financials": ["JPM", "GS", "BAC", "WFC", "MS", "C", "BLK"],
"Energy": ["XOM", "CVX", "COP", "SLB", "EOG", "MPC", "OXY"],
"Healthcare": ["JNJ", "UNH", "PFE", "ABBV", "MRK", "LLY", "BMY"],
"Consumer": ["AMZN", "TSLA", "HD", "NKE", "SBUX", "MCD", "TGT"],
"ETFs": ["SPY", "QQQ", "IWM", "XLF", "XLE", "XLK", "XBI"]
}
for sector, tickers in sectors.items():
print(f"\n{'='*50}")
print(f" {sector}")
print(f"{'='*50}")
sector_results = fa.screener(
universe={"symbols": tickers},
sort=[("iv_rank_30d", "desc")],
select=["symbol", "iv_rank_30d", "atm_iv"],
limit=len(tickers),
)
ranks = [r["iv_rank_30d"] for r in sector_results]
avg_rank = sum(ranks) / len(ranks) if ranks else 0
print(f" Sector Avg IV Rank: {avg_rank:.1f}\n")
for r in sector_results:
bar = "#" * int(r["iv_rank_30d"] / 5)
print(f" {r['symbol']:<6} IV Rank: {r['iv_rank_30d']:>5} |{bar}")
6. IV Rank with VRP Context
High IV rank alone doesn't tell you whether IV is rich relative to what the stock actually delivers in realized moves. The VRP (Volatility Risk Premium) context answers this: is the implied volatility actually overpriced vs historical realized vol?
The screener exposes vrp_assessment and vrp_percentile alongside iv_rank_30d, so a high-IV-and-rich-VRP screen is one filter tree. For deeper per-symbol analysis, /v1/vrp/{symbol} (Alpha plan) provides z-scores and percentiles.
def scan_rich_iv(universe=None):
"""Find high IV that is ALSO rich vs realized vol (high VRP percentile)."""
return fa.screener(
universe={"symbols": universe} if universe else None,
# High IV rank AND a rich VRP regime
filters={"op": "and", "conditions": [
{"field": "iv_rank_30d", "operator": "gte", "value": 60},
{"field": "vrp_assessment", "operator": "in",
"value": ["very_high_premium", "healthy_premium"]},
]},
sort=[("iv_rank_30d", "desc")],
select=["symbol", "iv_rank_30d", "atm_iv", "rv_20d",
"vrp_percentile", "vrp_assessment"],
limit=100,
)
rich_iv = scan_rich_iv()
print(f"\nHigh IV + Rich VRP ({len(rich_iv)} found):")
print(f"{'Ticker':<8} {'IV Rank':>8} {'ATM IV':>8} {'RV20':>8} {'Spread':>8} {'VRP':>18}")
print("-" * 62)
for r in rich_iv:
spread = r["atm_iv"] - r["rv_20d"]
print(f"{r['symbol']:<8} {r['iv_rank_30d']:>7} {r['atm_iv']:>7.1f}% "
f"{r['rv_20d']:>7.1f}% {spread:>+7.1f}% {r['vrp_assessment']:>18}")
For Alpha plan users, the deeper VRP endpoint adds z-scores:
Run the scanner on a schedule and push high-IV alerts to Slack or email. Here's a minimal Slack integration:
"""
Daily IV rank alert - run via cron at 9:00 AM ET
Sends high-IV alerts to a Slack webhook
"""
import requests
from flashalpha import FlashAlpha
fa = FlashAlpha("YOUR_KEY")
SLACK_WEBHOOK = "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
THRESHOLD = 75
watchlist = [
"SPY", "QQQ", "IWM", "AAPL", "MSFT", "GOOGL", "AMZN", "TSLA",
"NVDA", "META", "AMD", "NFLX", "COIN", "XBI", "GDX", "TLT"
]
rows = fa.screener(
universe={"symbols": watchlist},
filters={"op": "and", "conditions": [
{"field": "iv_rank_30d", "operator": "gte", "value": THRESHOLD},
]},
sort=[("iv_rank_30d", "desc")],
select=["symbol", "iv_rank_30d", "atm_iv", "vrp_assessment"],
limit=len(watchlist),
)
alerts = [
f"*{r['symbol']}*: IV Rank {r['iv_rank_30d']} | "
f"ATM IV {r['atm_iv']:.1f}% | VRP: {r['vrp_assessment']}"
for r in rows
]
if alerts:
message = {
"text": f":chart_with_upwards_trend: *IV Rank Alert* - "
f"{len(alerts)} tickers above {THRESHOLD}%\n\n"
+ "\n".join(alerts)
}
requests.post(SLACK_WEBHOOK, json=message)
print(f"Sent {len(alerts)} alerts to Slack")
else:
print("No tickers above threshold")
Schedule this with cron (Linux/Mac) or Task Scheduler (Windows):
# Run at 9:00 AM ET every weekday
0 9 * * 1-5 cd /path/to/scanner && python iv_alert.py
For email, swap the Slack webhook for SMTP or a service like SendGrid. The scanning logic stays identical.
AI Agents: Ask Claude for IV Scans
FlashAlpha provides an MCP (Model Context Protocol) server at https://lab.flashalpha.com/mcp. Connect it to Claude, Cursor, Windsurf, or any MCP-compatible AI assistant and query IV data conversationally:
"Which stocks in my watchlist have IV rank above 80?"
"Compare IV rank and IV percentile for TSLA, NVDA, and AMD"
"Find stocks with high IV rank and positive gamma regime"
"Is COIN's IV rich or cheap relative to realized vol?"
The MCP server exposes the same endpoints as the REST API. Your AI agent handles the scanning loop, formatting, and interpretation - you just ask questions in plain English.
If you're considering computing IV rank and IV percentile yourself, here's what's involved:
Raw options chain data - you need every listed option (all strikes, all expirations) to compute ATM IV accurately. For a single liquid stock like AAPL, that can be 5,000+ contracts. Across 6,000 tickers, you're processing millions of contracts daily.
IV computation - implied volatility isn't a field in raw options data. You have to solve for it using BSM or a binomial model, handling dividends, early exercise, and bid/ask spread noise.
52-week history - IV rank requires the 52-week high and low of ATM IV. IV percentile requires storing daily ATM IV for every ticker for the past 252 trading days. That's 252 * 6,000 = 1.5 million daily data points, plus the chain data to compute each one.
Daily updates - the entire pipeline needs to run before market open, every day. Miss a day and your rank/percentile computations are stale.
Infrastructure - a database for historical IV, a scheduler, error handling, data vendor costs for the raw chains, and compute for the IV solver.
The API abstracts all of this. One call, one response, pre-computed and ready.
Pricing
Plan
Price
Requests
IV Scanning Coverage
Free
$0
5 req/day
Stock quotes, GEX/levels, BSM Greeks, IV solver, stock summary, and /v1/screener/fields. Prototype before upgrading.
Basic
from $63/mo
100 req/day
Adds DEX/VEX/CHEX, index symbols, and max pain. The IV-rank screener is Growth+.
Growth
from $239/mo
2,500 req/day
Unlocks POST /v1/screener (iv_rank_30d) - up to 20 symbols per query - plus the /volatility endpoint for detailed RV, skew profiles, term structure.
Alpha
from $1,199/mo
Unlimited
Larger screener queries (~250 symbols) and the /vrp endpoint for VRP z-scores. Zero-cache responses for max freshness.
IV rank measures where current implied volatility sits within its 52-week range: (Current IV - 52w Low) / (52w High - 52w Low) * 100. An IV rank of 80 means current IV is 80% of the way between its annual low and high. It tells you how elevated IV is relative to its own historical extremes.
IV percentile measures the percentage of trading days over the past year that had lower implied volatility than today. An IV percentile of 87 means IV was lower than today on 87% of trading days. It's more robust than IV rank because it's less sensitive to single extreme spikes.
The API covers 6,000+ US equities and ETFs. Use GET /v1/tickers to retrieve the full list. The IV-rank screener (POST /v1/screener) is a Growth-plan feature: Growth queries return up to 20 symbols per call and Alpha up to ~250, and the screen runs in a single request rather than one call per ticker.
IV-rank scanning runs on POST /v1/screener, which requires the Growth plan ($239/mo, 2,500 req/day) - it returns up to 20 ranked symbols per query and adds the /volatility endpoint for deeper analysis. Alpha ($1,199/mo, unlimited) raises the screener to ~250 symbols per query and adds VRP z-scores with zero-cache freshness. The Free and Basic plans cover quotes, GEX/levels, Greeks, and the stock summary, but not the screener.
Building an IV rank scanner doesn't require a historical volatility database, a daily computation pipeline, or millions of raw options contracts. FlashAlpha's POST /v1/screener ranks your universe by the iv_rank_30d metric and returns ATM IV, VRP context, and gamma regime alongside it - one structured request, pre-computed and ready to scan. The screener is a Growth-plan feature (~20 symbols per query, ~250 on Alpha); start there and scale to Alpha when you're ready to screen a larger universe.