|
| 1 | +# # Max Pain Analysis — dealer alignment, pain curve, and pin probability |
| 2 | +# |
| 3 | +# The Max Pain endpoint returns the strike where total option holder payout is |
| 4 | +# minimized. This analysis overlays dealer positioning (gamma flip, call/put |
| 5 | +# walls) to identify whether max pain acts as a convergence magnet or is |
| 6 | +# overridden by directional gamma flow. |
| 7 | +# |
| 8 | +# Docs: https://flashalpha.com/docs |
| 9 | +# Install: pip install "flashalpha>=0.3.2" |
| 10 | +# |
| 11 | +# Tier: Growth+ required |
| 12 | + |
| 13 | +import os |
| 14 | +from flashalpha import FlashAlpha |
| 15 | + |
| 16 | +fa = FlashAlpha(os.environ["FLASHALPHA_API_KEY"]) |
| 17 | + |
| 18 | +# --------------------------------------------------------------------------- |
| 19 | +# 1) Full-chain max pain (all expirations) |
| 20 | +# --------------------------------------------------------------------------- |
| 21 | +result = fa.max_pain("SPY") |
| 22 | +print(f"Max pain strike: {result['max_pain_strike']}") |
| 23 | +print(f"Spot: {result['underlying_price']}") |
| 24 | +print(f"Distance: {result['distance']['absolute']:.2f} ({result['distance']['percent']:.2f}%) {result['distance']['direction']}") |
| 25 | +print(f"Signal: {result['signal']}") |
| 26 | +print(f"Pin probability: {result['pin_probability']}/100") |
| 27 | + |
| 28 | +# --------------------------------------------------------------------------- |
| 29 | +# 2) Dealer alignment overlay |
| 30 | +# --------------------------------------------------------------------------- |
| 31 | +da = result["dealer_alignment"] |
| 32 | +print(f"\nDealer alignment: {da['alignment']}") |
| 33 | +print(f" {da['description']}") |
| 34 | +print(f" Gamma flip: {da['gamma_flip']}") |
| 35 | +print(f" Call wall: {da['call_wall']}") |
| 36 | +print(f" Put wall: {da['put_wall']}") |
| 37 | +print(f" Regime: {result['regime']}") |
| 38 | + |
| 39 | +# --------------------------------------------------------------------------- |
| 40 | +# 3) Expected move context |
| 41 | +# --------------------------------------------------------------------------- |
| 42 | +em = result["expected_move"] |
| 43 | +print(f"\nATM straddle: ${em['straddle_price']:.2f}") |
| 44 | +print(f"ATM IV: {em['atm_iv']}%") |
| 45 | +print(f"Max pain within expected range: {em['max_pain_within_expected_range']}") |
| 46 | + |
| 47 | +# --------------------------------------------------------------------------- |
| 48 | +# 4) Pain curve — top 5 strikes by lowest total pain |
| 49 | +# --------------------------------------------------------------------------- |
| 50 | +curve = sorted(result["pain_curve"], key=lambda x: x["total_pain"]) |
| 51 | +print("\nPain curve (lowest pain strikes):") |
| 52 | +for row in curve[:5]: |
| 53 | + print(f" {row['strike']:>8} call_pain=${row['call_pain']:>12,.0f} put_pain=${row['put_pain']:>12,.0f} total=${row['total_pain']:>12,.0f}") |
| 54 | + |
| 55 | +# --------------------------------------------------------------------------- |
| 56 | +# 5) OI distribution around max pain |
| 57 | +# --------------------------------------------------------------------------- |
| 58 | +mp = result["max_pain_strike"] |
| 59 | +oi_near = [s for s in result["oi_by_strike"] if abs(s["strike"] - mp) <= 10] |
| 60 | +print(f"\nOI near max pain ({mp}):") |
| 61 | +for s in oi_near[:5]: |
| 62 | + print(f" {s['strike']:>8} call_oi={s['call_oi']:>8,} put_oi={s['put_oi']:>8,} total={s['total_oi']:>8,}") |
| 63 | + |
| 64 | +print(f"\nPut/call OI ratio: {result['put_call_oi_ratio']:.3f}") |
| 65 | + |
| 66 | +# --------------------------------------------------------------------------- |
| 67 | +# 6) Multi-expiry calendar |
| 68 | +# --------------------------------------------------------------------------- |
| 69 | +if result.get("max_pain_by_expiration"): |
| 70 | + print("\nMax pain by expiry:") |
| 71 | + for entry in result["max_pain_by_expiration"][:5]: |
| 72 | + print(f" {entry['expiration']} strike={entry['max_pain_strike']} DTE={entry['dte']} OI={entry['total_oi']:,}") |
| 73 | + |
| 74 | +# --------------------------------------------------------------------------- |
| 75 | +# 7) Single-expiry max pain |
| 76 | +# --------------------------------------------------------------------------- |
| 77 | +# Get first available expiration |
| 78 | +opts = fa.options("SPY") |
| 79 | +if opts.get("expirations"): |
| 80 | + exp = opts["expirations"][0]["expiration"] |
| 81 | + single = fa.max_pain("SPY", expiration=exp) |
| 82 | + print(f"\nMax pain for {exp}: {single['max_pain_strike']}") |
| 83 | + print(f" Pin probability: {single['pin_probability']}/100") |
| 84 | + print(f" Dealer alignment: {single['dealer_alignment']['alignment']}") |
0 commit comments