""" Dealer Gamma Exposure (GEX) estimation and pinning/fading signals. In options markets, dealers delta-hedge their portfolios. Their gamma exposure determines whether they amplify or suppress price movements: - Long gamma → dealers buy low, sell high → suppress vol, "pin" price - Short gamma → dealers buy high, sell low → amplify vol, create gamma squeeze GEX = Σ (gamma_per_contract × open_interest × spot_price) For crypto (no options on Hyperliquid yet), we approximate GEX using: - BTC/ETH options open interest from Deribit - Estimated dealer gamma via Black-Scholes - Pin levels at strikes with max GEX Strategy: when dealer GEX is heavily positive at a strike, fade breakouts. When GEX is negative, ride the momentum with the dealers. """ from __future__ import annotations import math from collections import defaultdict from typing import Optional class DealerGEX: """Estimate dealer gamma exposure and generate pinning/fading signals. Uses option open interest data and Black-Scholes gamma to compute per-strike GEX. Aggregates to net GEX and identifies pin levels. Usage: gex = DealerGEX(spot=64500, risk_free=0.05) gex.add_strike(strike=65000, call_oi=500, put_oi=300, expiry_days=7, iv=0.60) print(gex.pin_levels()) print(gex.signal()) """ def __init__( self, spot: float = 0.0, risk_free: float = 0.05, pin_threshold: float = 0.01, # 1% of spot to consider "at pin level" ): self._spot = spot self._rf = risk_free self._pin_threshold = pin_threshold self._strikes: list[dict] = [] # [{strike, call_oi, put_oi, gamma, gex_usd, ...}] self._net_gex: float = 0.0 self._pin_levels: list[dict] = [] # ── Data feed ──────────────────────────────────────────── def set_spot(self, spot: float): self._spot = spot def add_strike( self, strike: float, call_oi: float = 0, put_oi: float = 0, expiry_days: float = 30, iv: float = 0.50, ): """Add OI data at a specific strike.""" gamma_call = self._bs_gamma(strike, "call", expiry_days, iv) gamma_put = self._bs_gamma(strike, "put", expiry_days, iv) # GEX per strike = gamma × OI × spot² × 0.01 (scaling) gex_call = gamma_call * call_oi * self._spot * self._spot * 0.01 gex_put = gamma_put * put_oi * self._spot * self._spot * 0.01 self._strikes.append({ "strike": strike, "call_oi": call_oi, "put_oi": put_oi, "gamma_call": round(gamma_call, 8), "gamma_put": round(gamma_put, 8), "gex_call_usd": round(gex_call, 2), "gex_put_usd": round(gex_put, 2), "gex_total_usd": round(gex_call + gex_put, 2), }) def compute(self): """Aggregate GEX and find pin levels.""" self._net_gex = sum(s["gex_total_usd"] for s in self._strikes) # Find pin levels: strikes where GEX is highest (magnets) # and is positive (dealers long gamma = pinning) if self._strikes: max_gex = max(abs(s["gex_total_usd"]) for s in self._strikes) self._pin_levels = [ {"strike": s["strike"], "gex_usd": s["gex_total_usd"], "is_pin": s["gex_total_usd"] > 0} for s in self._strikes if abs(s["gex_total_usd"]) > max_gex * 0.3 ] self._pin_levels.sort(key=lambda l: abs(l["gex_usd"]), reverse=True) # ── Signals ───────────────────────────────────────────── def nearest_pin(self) -> dict | None: """Find the nearest pin level to current spot.""" if not self._spot or not self._strikes: return None distances = [] for s in self._strikes: dist_pct = abs(s["strike"] - self._spot) / self._spot if s["gex_total_usd"] > 0: # only positive GEX acts as pin distances.append({ "strike": s["strike"], "distance_pct": round(dist_pct * 100, 2), "gex_usd": s["gex_total_usd"], "strength": "strong" if s["gex_total_usd"] > abs(self._net_gex) * 0.1 else "weak", }) if not distances: return None return min(distances, key=lambda d: d["distance_pct"]) def signal(self) -> dict: """Generate trading signal based on dealer GEX. Returns: signal: "fade_breakout", "ride_momentum", "neutral" reason: explanation """ self.compute() if not self._spot: return {"signal": "neutral", "reason": "no_spot"} pin = self.nearest_pin() dist_pct = pin["distance_pct"] if pin else 100.0 if self._net_gex > 1e6 and pin and dist_pct < self._pin_threshold * 100: return { "signal": "fade_breakout", "reason": f"Dealers long gamma ${self._net_gex:,.0f} — pinning at ${pin['strike']:,.0f} ({dist_pct:.1f}%)", "net_gex_usd": round(self._net_gex, 2), "pin_strike": pin["strike"], "direction": "mean_reversion", } elif self._net_gex < -1e6: return { "signal": "ride_momentum", "reason": f"Dealers short gamma ${self._net_gex:,.0f} — amplifying moves", "net_gex_usd": round(self._net_gex, 2), "direction": "trend_following", } else: return {"signal": "neutral", "reason": "balanced_gex", "net_gex_usd": round(self._net_gex, 2)} # ── Black-Scholes gamma ───────────────────────────────── def _bs_gamma(self, strike: float, opt_type: str, T_days: float, sigma: float) -> float: """Black-Scholes gamma for a European option.""" if self._spot <= 0 or strike <= 0 or T_days <= 0 or sigma <= 0: return 0.0 T = T_days / 365.0 d1 = (math.log(self._spot / strike) + (self._rf + 0.5 * sigma * sigma) * T) / (sigma * math.sqrt(T)) phi = math.exp(-d1 * d1 / 2.0) / math.sqrt(2.0 * math.pi) return phi / (self._spot * sigma * math.sqrt(T)) # ── Summary ───────────────────────────────────────────── def summary(self) -> dict: pin = self.nearest_pin() return { "spot": self._spot, "net_gex_usd": round(self._net_gex, 2), "net_gex_m": round(self._net_gex / 1e6, 2), "strikes_tracked": len(self._strikes), "pin_levels": self._pin_levels[:5], "nearest_pin": pin, "signal": self.signal(), }