Files
ftdt-quant-lab/microstructure/tick_regime.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

192 lines
7.4 KiB
Python

"""
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}