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(),
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
"""
|
||||
Tick-size and lot-size regime exploitation.
|
||||
|
||||
Exchanges dynamically adjust minimum price increments (tick size)
|
||||
and minimum order sizes (lot size) based on asset price. When an
|
||||
asset crosses these thresholds, spreads widen or narrow instantly.
|
||||
|
||||
Hyperliquid tick sizes (example — actual values from exchange):
|
||||
BTC: 0.1 USD tick (<$100K), 0.5 USD tick ($100K-$1M) — approximate
|
||||
ETH: 0.01 USD tick
|
||||
SOL: 0.001 USD tick
|
||||
|
||||
Strategy: when price crosses a tick-size boundary, be the first to adjust
|
||||
your quoting engine. Competitors quoting at old tick sizes will be either
|
||||
non-competitive (too wide) or illegal (too tight). Capture the spread gap.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import deque
|
||||
from typing import Optional
|
||||
|
||||
|
||||
# Hyperliquid tick size schedule (example values — confirm from exchange docs)
|
||||
TICK_SCHEDULE = {
|
||||
"BTC": [
|
||||
(float("-inf"), 100000, 0.1),
|
||||
(100000, float("inf"), 0.5),
|
||||
],
|
||||
"ETH": [
|
||||
(float("-inf"), 10000, 0.01),
|
||||
(10000, float("inf"), 0.05),
|
||||
],
|
||||
"SOL": [
|
||||
(float("-inf"), 1000, 0.001),
|
||||
(1000, float("inf"), 0.005),
|
||||
],
|
||||
# Default for unknown coins
|
||||
"DEFAULT": [
|
||||
(float("-inf"), float("inf"), 0.01),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class TickRegimeMonitor:
|
||||
"""Monitor and exploit tick-size regime changes.
|
||||
|
||||
Tracks when an asset price approaches a tick-size boundary
|
||||
and generates signals to adjust quoting before competitors.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
approach_threshold_pct: float = 1.0, # 1% from boundary
|
||||
history_window: int = 100,
|
||||
):
|
||||
self._approach_threshold = approach_threshold_pct / 100
|
||||
self._prices: dict[str, deque[float]] = {}
|
||||
self._current_tick: dict[str, float] = {}
|
||||
self._approaching_boundary: dict[str, dict] = {}
|
||||
self._window = history_window
|
||||
|
||||
# ── Data feed ────────────────────────────────────────────
|
||||
|
||||
def get_tick_size(self, coin: str, price: float) -> float:
|
||||
"""Get current tick size for a coin at given price."""
|
||||
schedule = TICK_SCHEDULE.get(coin.upper(), TICK_SCHEDULE["DEFAULT"])
|
||||
for lo, hi, tick in schedule:
|
||||
if lo <= price < hi:
|
||||
return tick
|
||||
return schedule[-1][2] # fallback
|
||||
|
||||
def update_price(self, coin: str, price: float):
|
||||
"""Feed a new price observation."""
|
||||
c = coin.upper()
|
||||
self._prices.setdefault(c, deque(maxlen=self._window)).append(price)
|
||||
self._current_tick[c] = self.get_tick_size(c, price)
|
||||
|
||||
# Check if approaching boundary
|
||||
schedule = TICK_SCHEDULE.get(c, TICK_SCHEDULE["DEFAULT"])
|
||||
for lo, hi, tick in schedule:
|
||||
if lo <= price < hi:
|
||||
# Check approach to upper boundary
|
||||
if hi != float("inf"):
|
||||
dist_up = (hi - price) / price
|
||||
if 0 < dist_up < self._approach_threshold:
|
||||
next_tick = self.get_tick_size(c, hi)
|
||||
if next_tick != tick:
|
||||
self._approaching_boundary[c] = {
|
||||
"direction": "up",
|
||||
"current_tick": tick,
|
||||
"next_tick": next_tick,
|
||||
"boundary_price": hi,
|
||||
"distance_pct": round(dist_up * 100, 2),
|
||||
"crosses_in_bars": self._estimate_cross_bars(c, hi),
|
||||
}
|
||||
return
|
||||
# Check approach to lower boundary
|
||||
if lo != float("-inf"):
|
||||
dist_down = (price - lo) / price
|
||||
if 0 < dist_down < self._approach_threshold:
|
||||
prev_tick = self.get_tick_size(c, lo - 1)
|
||||
if prev_tick != tick:
|
||||
self._approaching_boundary[c] = {
|
||||
"direction": "down",
|
||||
"current_tick": tick,
|
||||
"next_tick": prev_tick,
|
||||
"boundary_price": lo,
|
||||
"distance_pct": round(dist_down * 100, 2),
|
||||
"crosses_in_bars": self._estimate_cross_bars(c, lo),
|
||||
}
|
||||
return
|
||||
|
||||
self._approaching_boundary.pop(c, None)
|
||||
|
||||
def _estimate_cross_bars(self, coin: str, boundary_price: float) -> int:
|
||||
"""Estimate how many bars until price crosses the boundary."""
|
||||
prices = list(self._prices.get(coin, []))
|
||||
if len(prices) < 5:
|
||||
return -1
|
||||
|
||||
recent = prices[-min(20, len(prices)):]
|
||||
if len(recent) < 5:
|
||||
return -1
|
||||
|
||||
# Simple linear trend estimate
|
||||
xs = list(range(len(recent)))
|
||||
n = len(xs)
|
||||
sum_x = sum(xs)
|
||||
sum_y = sum(recent)
|
||||
sum_xy = sum(x * y for x, y in zip(xs, recent))
|
||||
sum_x2 = sum(x * x for x in xs)
|
||||
|
||||
slope = (n * sum_xy - sum_x * sum_y) / (n * sum_x2 - sum_x * sum_x) if (n * sum_x2 - sum_x * sum_x) != 0 else 0
|
||||
last_price = recent[-1]
|
||||
|
||||
if abs(slope) < 1e-10:
|
||||
return -1
|
||||
|
||||
bars_to_cross = int((boundary_price - last_price) / slope)
|
||||
return max(0, bars_to_cross) if bars_to_cross > 0 else -1
|
||||
|
||||
# ── Signal ──────────────────────────────────────────────
|
||||
|
||||
def signal(self, coin: str) -> dict:
|
||||
"""Generate tick-size regime signal.
|
||||
|
||||
Returns action to take before competitors adjust.
|
||||
"""
|
||||
c = coin.upper()
|
||||
boundary = self._approaching_boundary.get(c)
|
||||
|
||||
current_tick = self._current_tick.get(c, 0.01)
|
||||
|
||||
if not boundary:
|
||||
return {
|
||||
"action": "none",
|
||||
"current_tick": current_tick,
|
||||
"reason": "no_regime_change",
|
||||
}
|
||||
|
||||
tick_delta = boundary["next_tick"] - boundary["current_tick"]
|
||||
bars = boundary["crosses_in_bars"]
|
||||
|
||||
if tick_delta > 0:
|
||||
# Tick size INCREASING → spread will widen → HFTs will post wider quotes
|
||||
# Be the first to widen to the new tick
|
||||
action = "widen_quotes"
|
||||
reason = f"Tick increasing {boundary['current_tick']}→{boundary['next_tick']} "
|
||||
reason += f"({boundary['distance_pct']}% away" + (f", ~{bars} bars" if bars > 0 else "") + ")"
|
||||
else:
|
||||
# Tick size DECREASING → spread will narrow → HFTs will tighten
|
||||
# Be the first to tighten to the new tick
|
||||
action = "tighten_quotes"
|
||||
reason = f"Tick decreasing {boundary['current_tick']}→{boundary['next_tick']} "
|
||||
reason += f"({boundary['distance_pct']}% away" + (f", ~{bars} bars" if bars > 0 else "") + ")"
|
||||
|
||||
return {
|
||||
"action": action,
|
||||
"current_tick": boundary["current_tick"],
|
||||
"next_tick": boundary["next_tick"],
|
||||
"tick_delta": round(tick_delta, 5),
|
||||
"boundary_price": boundary["boundary_price"],
|
||||
"distance_pct": boundary["distance_pct"],
|
||||
"estimated_bars_to_cross": bars,
|
||||
"reason": reason,
|
||||
"urgent": boundary["distance_pct"] < 0.5, # within 0.5% = urgent
|
||||
}
|
||||
|
||||
def summary(self) -> dict:
|
||||
return {coin: self.signal(coin) for coin in self._prices}
|
||||
Reference in New Issue
Block a user