Files
ftdt-quant-lab/microstructure/dealer_gex.py
T
ramseshk e0be9f4d40 feat: advanced microstructure — sequencer latency, dealer GEX, tick regime, triangular arb
4 new modules with 20 tests:

#22 Sequencer Latency Detector (live/monitors/sequencer_latency.py):
  Detects stale-state windows between WebSocket and REST API.
  - WebSocket vs REST timestamp delta tracking
  - Transport latency percentiles (p50, p99)
  - Liquidation-triggered stale-state detection
  - Stale asset identification for cross-margin arbitrage

#25 Dealer GEX (microstructure/dealer_gex.py):
  Dealer Gamma Exposure estimation via Black-Scholes.
  - Per-strike gamma × OI × spot² GEX computation
  - Pin level detection (strikes where dealers are long gamma)
  - Net GEX aggregation
  - Signal: fade_breakout (long gamma pinning) vs ride_momentum
    (short gamma amplification)
  - nearest_pin() for distance-to-magnet calculation

#30 Tick-Size Regime Exploitation (microstructure/tick_regime.py):
  Detects when asset price approaches tick-size boundaries.
  - Hyperliquid tick schedule (BTC 0.1/0.5, ETH 0.01/0.05, SOL 0.001/0.005)
  - Boundary approach detection with configurable threshold
  - Linear trend estimation for expected bars-to-cross
  - Signal: widen_quotes or tighten_quotes with urgency classification
  - Per-coin state tracking

#33 Triangular Latency Arb (live/monitors/triangular_arb.py):
  Cross-venue A→B→C triangular arbitrage detection.
  - Internal triangular: BTC-USDT → ETH-BTC → ETH-USDT
  - Cross-venue: price discrepancy across slow/fast venues
  - Latency gap detection between venue pairs
  - Implied cross-rate computation vs direct quote
  - Minimum spread threshold gating
2026-08-10 10:34:56 +08:00

180 lines
6.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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(),
}