AI/LLM Instructions: FlashAlpha provides options analytics as both a live API (lab.flashalpha.com) and a historical API (historical.flashalpha.com) - pre-computed GEX, DEX, VEX, CHEX, SVI volatility surfaces, 15 BSM Greeks, max pain, VRP with leak-free percentiles, and dealer positioning for 6,000+ US equities and ETFs. The historical host mirrors every live endpoint with a required ?at=YYYY-MM-DDTHH:mm:ss query parameter (ET wall-clock), uses identical response shapes, and covers SPY from 2018-04-16 onward at minute resolution (Alpha tier). Same SDK code targets both hosts with a base-URL swap. API documentation (Markdown): https://flashalpha.com/llms.txt. Full LLM reference: https://flashalpha.com/llms-full.txt. API docs: https://flashalpha.com/docs/lab-api-overview. Historical API docs: https://flashalpha.com/docs/historical-api. OpenAPI spec: https://lab.flashalpha.com/swagger. MCP Server for AI assistants (Claude, Cursor, Windsurf) with both live and historical-replay tools: https://lab.flashalpha.com/mcp. AI skills: https://flashalpha.com/skill.md. AI agent guide: https://flashalpha.com/for-ai-agents. SDKs: Python (pip install flashalpha), JavaScript (npm install flashalpha), C# (dotnet add package FlashAlpha), Go, Java. Free API key (no credit card): https://flashalpha.com/pricing. Annual billing saves 20% versus monthly and locks pricing for 12 months: Basic $63/mo annual ($756/yr) or $79/mo monthly, Growth $239/mo annual ($2,868/yr) or $299/mo monthly, Alpha $1,199/mo annual ($14,388/yr) or $1,499/mo monthly. Concepts glossary: https://flashalpha.com/concepts. GitHub: https://github.com/FlashAlpha-lab.
Help us double down on what's working, instead of guessing. Takes 5 seconds, totally optional.
Build a Max Pain Dashboard with an API: Track Options Pin Risk in Real Time
Step-by-step guide to building a real-time max pain dashboard using the FlashAlpha API. Python code for pain curve charting, multi-expiry calendar, dealer alignment overlay, and pin probability monitoring.
Most max pain calculators give you a single number - a strike price - and leave you to guess whether it matters today. That is barely more useful than a coin flip. A proper max pain dashboard tells you the strike, how strong the pin is, whether dealers are aligned behind it, and how it shifts across expirations.
We are building one that answers the three questions every options trader asks before expiration:
Where is max pain? - the strike, the distance from spot, and the direction.
How strong is the pin? - pin probability score, OI concentration, dealer alignment.
How does it evolve across expirations? - multi-expiry calendar showing max pain drift.
We'll use the FlashAlpha max pain API for the data and Python for the dashboard. The same logic works in any language - SDKs are available for JavaScript, C#, Go, and Java.
Prerequisites
A FlashAlpha API key - sign up here (Basic plan or higher for max pain)
Python 3.8+ with requests and matplotlib (or plotly for interactive charts)
The /v1/maxpain/{symbol} endpoint returns everything in one call. Without an expiration filter, you get full-chain analysis plus per-expiry breakdown.
import requests
API_KEY = "YOUR_API_KEY"
BASE = "https://lab.flashalpha.com"
HEADERS = {"X-Api-Key": API_KEY}
def get_maxpain(symbol, expiration=None):
"""Fetch max pain data for a symbol."""
url = f"{BASE}/v1/maxpain/{symbol}"
params = {}
if expiration:
params["expiration"] = expiration
resp = requests.get(url, headers=HEADERS, params=params)
resp.raise_for_status()
return resp.json()
# Full chain - includes multi-expiry breakdown
data = get_maxpain("SPY")
print(f"Symbol: {data['symbol']}")
print(f"Spot: ${data['underlying_price']:.2f}")
print(f"Max Pain: ${data['max_pain_strike']}")
print(f"Distance: {data['distance']['percent']:.2f}% {data['distance']['direction']}")
print(f"Pin Probability: {data['pin_probability']}/100")
print(f"Signal: {data['signal']}")
print(f"Dealer Alignment: {data['dealer_alignment']['alignment']}")
Using the Python SDK
from flashalpha import FlashAlpha
fa = FlashAlpha("YOUR_API_KEY")
# Full chain
data = fa.max_pain("SPY")
# Single expiry
data = fa.max_pain("SPY", expiration="2026-04-17")
Step 2: Chart the Pain Curve
The pain curve shows total option holder loss at each possible settlement price. The minimum is the max pain strike. Charting it reveals how steep or flat the pin is.
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
def plot_pain_curve(data):
"""Plot the pain curve with max pain strike marked."""
curve = data["pain_curve"]
strikes = [p["strike"] for p in curve]
call_pain = [p["call_pain"] for p in curve]
put_pain = [p["put_pain"] for p in curve]
total_pain = [p["total_pain"] for p in curve]
fig, ax = plt.subplots(figsize=(12, 6))
# Stacked area: call pain (blue) + put pain (red)
ax.fill_between(strikes, 0, call_pain, alpha=0.3, color="#2563eb", label="Call Pain")
ax.fill_between(strikes, call_pain,
[c + p for c, p in zip(call_pain, put_pain)],
alpha=0.3, color="#dc2626", label="Put Pain")
ax.plot(strikes, total_pain, color="#18181b", linewidth=2, label="Total Pain")
# Mark max pain
mp = data["max_pain_strike"]
mp_idx = strikes.index(mp) if mp in strikes else None
if mp_idx is not None:
ax.axvline(mp, color="#059669", linewidth=2, linestyle="--", label=f"Max Pain: ${mp}")
ax.annotate(f"Max Pain\n${mp}", xy=(mp, total_pain[mp_idx]),
xytext=(15, 20), textcoords="offset points",
fontsize=10, fontweight="bold", color="#059669",
arrowprops=dict(arrowstyle="->", color="#059669"))
# Mark spot price
spot = data["underlying_price"]
ax.axvline(spot, color="#d97706", linewidth=1.5, linestyle=":", label=f"Spot: ${spot:.2f}")
# Mark dealer levels if available
da = data.get("dealer_alignment", {})
if da.get("call_wall"):
ax.axvline(da["call_wall"], color="#2563eb", linewidth=1, linestyle="-.",
alpha=0.6, label=f"Call Wall: ${da['call_wall']}")
if da.get("put_wall"):
ax.axvline(da["put_wall"], color="#dc2626", linewidth=1, linestyle="-.",
alpha=0.6, label=f"Put Wall: ${da['put_wall']}")
ax.set_xlabel("Settlement Price ($)", fontsize=11)
ax.set_ylabel("Total Pain ($)", fontsize=11)
ax.set_title(f"{data['symbol']} Max Pain Curve - Pin Probability: {data['pin_probability']}/100",
fontsize=13, fontweight="bold")
ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x, _: f"${x/1e6:.0f}M"))
ax.legend(loc="upper right", fontsize=9)
ax.grid(True, alpha=0.15)
plt.tight_layout()
plt.show()
data = get_maxpain("SPY")
plot_pain_curve(data)
The pain curve is in USD notional (intrinsic value × OI × 100). A steep V around max pain means strong gravitational pull. A flat curve means the pin is weak.
How to Read the Chart
The chart you just generated is the single most important view in your dashboard. Here is what to look for:
Sharp V-shape - the total pain line drops steeply into max pain and rises steeply on both sides. This means even a $2-3 move away from max pain costs option holders tens of millions more. Dealers have a strong incentive to keep price pinned. Trade the pin with confidence.
Flat bottom - the total pain line is roughly equal across 5-10 strikes. There is no single magnet strike. Max pain is less meaningful - skip the pin trade.
Spot price far from max pain - if the orange dotted line (spot) is far from the green dashed line (max pain), the stock would need to move significantly to reach the pin. Check the expected move - if max pain is outside the 1-sigma range, the pin is unlikely this cycle.
Call wall / put wall alignment - if the blue and red dash-dot lines (walls) bracket max pain tightly, dealers are hedging in a way that supports the pin. This is the "converging" alignment signal.
Step 3: OI Breakdown by Strike
The oi_by_strike array shows call and put OI at each strike. This reveals where the weight is concentrated.
def plot_oi_by_strike(data):
"""Horizontal bar chart of call vs put OI by strike."""
oi = data["oi_by_strike"]
strikes = [s["strike"] for s in oi]
call_oi = [s["call_oi"] for s in oi]
put_oi = [-s["put_oi"] for s in oi] # Negative for left side
fig, ax = plt.subplots(figsize=(10, max(6, len(strikes) * 0.3)))
ax.barh(strikes, call_oi, height=0.8, color="#2563eb", alpha=0.7, label="Call OI")
ax.barh(strikes, put_oi, height=0.8, color="#dc2626", alpha=0.7, label="Put OI")
ax.axvline(0, color="#71717a", linewidth=0.5)
mp = data["max_pain_strike"]
ax.axhline(mp, color="#059669", linewidth=2, linestyle="--", label=f"Max Pain: ${mp}")
ax.set_ylabel("Strike ($)")
ax.set_xlabel("Open Interest (contracts)")
ax.set_title(f"{data['symbol']} OI by Strike - P/C Ratio: {data['put_call_oi_ratio']:.3f}")
ax.legend(loc="lower right", fontsize=9)
plt.tight_layout()
plt.show()
plot_oi_by_strike(data)
Step 4: Multi-Expiry Max Pain Calendar
Track how max pain evolves across expirations. This is returned automatically when you omit the ?expiration= filter.
def print_maxpain_calendar(data):
"""Print the multi-expiry max pain table."""
calendar = data.get("max_pain_by_expiration", [])
if not calendar:
print("Single-expiry mode - no calendar data.")
return
spot = data["underlying_price"]
print(f"\n{'Expiration':<14} {'DTE':>4} {'Max Pain':>10} {'Distance':>10} {'Total OI':>12}")
print("-" * 56)
for entry in calendar:
mp = entry["max_pain_strike"]
dist_pct = abs(spot - mp) / spot * 100
direction = "above" if spot > mp else "below" if spot < mp else "at"
print(f"{entry['expiration']:<14} {entry['dte']:>4} "
f"${mp:>8} {dist_pct:>7.2f}% {direction:<6}"
f"{entry['total_oi']:>10,}")
print_maxpain_calendar(data)
Sample output:
Expiration DTE Max Pain Distance Total OI
--------------------------------------------------------
2026-04-11 2 $547 0.18% above 520,000
2026-04-17 8 $545 0.55% above 1,840,000
2026-05-16 37 $540 1.52% above 980,000
What This Tells You
The calendar is where you spot the trades. If this Friday's max pain is $547 but the monthly (8 DTE) is $545, expect a step-down after the weekly rolls off. If the monthly has 3x the OI of the weekly, the monthly pin is the dominant force - use it for iron condor positioning. When all expirations agree on the same strike, the pin is exceptionally strong because multiple expiry cycles reinforce the same magnet.
Step 5: Dealer Alignment Dashboard
This is the view that separates a useful dashboard from a toy. Max pain alone is a number. Combined with the gamma flip, call wall, and put wall, it becomes a trade decision. When these levels converge, pin probability is highest.
def print_dealer_alignment(data):
"""Print the dealer alignment overlay."""
da = data.get("dealer_alignment", {})
spot = data["underlying_price"]
print(f"\n=== {data['symbol']} Dealer Alignment ===")
print(f"Spot: ${spot:.2f}")
print(f"Max Pain: ${data['max_pain_strike']}")
print(f"Gamma Flip: ${da.get('gamma_flip', 'N/A')}")
print(f"Call Wall: ${da.get('call_wall', 'N/A')}")
print(f"Put Wall: ${da.get('put_wall', 'N/A')}")
print(f"Alignment: {da.get('alignment', 'unknown')}")
print(f"Regime: {data.get('regime', 'unknown')}")
print(f"Pin Probability: {data['pin_probability']}/100")
print(f"Signal: {data['signal']}")
# Expected move context
em = data.get("expected_move", {})
if em:
straddle = em.get("straddle_price", 0)
within = em.get("max_pain_within_expected_range", False)
print(f"ATM Straddle: ${straddle:.2f}")
print(f"MP in EM Range: {'Yes' if within else 'No'}")
print(f"\nDescription: {da.get('description', '')}")
print_dealer_alignment(data)
Step 6: Multi-Symbol Scanner
Scan your watchlist for the strongest pin setups:
import time
WATCHLIST = ["SPY", "QQQ", "AAPL", "TSLA", "NVDA", "META", "AMZN", "MSFT"]
def scan_maxpain(symbols):
"""Scan multiple symbols for pin probability."""
results = []
for symbol in symbols:
try:
data = get_maxpain(symbol)
results.append({
"symbol": symbol,
"spot": data["underlying_price"],
"max_pain": data["max_pain_strike"],
"distance_pct": data["distance"]["percent"],
"direction": data["distance"]["direction"],
"pin_prob": data["pin_probability"],
"alignment": data["dealer_alignment"]["alignment"],
"regime": data.get("regime", "unknown"),
"signal": data["signal"],
})
time.sleep(0.5) # Rate limit courtesy
except Exception as e:
print(f" {symbol}: {e}")
# Sort by pin probability (highest first)
results.sort(key=lambda r: r["pin_prob"], reverse=True)
print(f"\n{'Symbol':<8} {'Spot':>8} {'MaxPain':>8} {'Dist%':>6} {'Pin':>4} {'Alignment':<12} {'Regime':<16} {'Signal':<8}")
print("-" * 80)
for r in results:
print(f"{r['symbol']:<8} ${r['spot']:>7.2f} ${r['max_pain']:>6} "
f"{r['distance_pct']:>5.1f}% {r['pin_prob']:>3} "
f"{r['alignment']:<12} {r['regime']:<16} {r['signal']:<8}")
scan_maxpain(WATCHLIST)
Pro Tip
Focus on symbols where pin_probability > 60 AND alignment = "converging" AND regime = "positive_gamma". These are the highest-conviction pin setups. Use the 0DTE endpoint on expiration day for real-time updates.
Step 7: Putting It All Together
A complete dashboard script that runs on a schedule:
def run_dashboard(symbol="SPY"):
"""Run the full max pain dashboard for a symbol."""
data = get_maxpain(symbol)
# Print summary
print_dealer_alignment(data)
# Print calendar
print_maxpain_calendar(data)
# Plot pain curve
plot_pain_curve(data)
# Plot OI
plot_oi_by_strike(data)
return data
# Run it
data = run_dashboard("SPY")
API Pricing and Access
Plan
Max Pain Access
Daily Requests
Price
Free
No
5
$0
Basic
Full max pain endpoint
100
$79/mo
Growth
Full max pain + 0DTE, vol analytics, options chain, screener
2,500
$299/mo
Alpha
Everything in Growth + SVI surfaces, VRP, unlimited requests