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
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
"""
|
||||
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(),
|
||||
}
|
||||
Reference in New Issue
Block a user