diff --git a/dashboard/static/live.html b/dashboard/static/live.html index 421224d..d063f52 100644 --- a/dashboard/static/live.html +++ b/dashboard/static/live.html @@ -6,222 +6,254 @@ FTDT Quant Lab — Live Terminal
-

FTDT QUANT LAB — LIVE

+

FTDT QUANT LAB — LIVE TERMINAL

BTC ETH - Hyperliquid Testnet - + HL Testnet +
\ No newline at end of file diff --git a/live/monitor_service.py b/live/monitor_service.py index 8cc7fc9..fce3ba0 100644 --- a/live/monitor_service.py +++ b/live/monitor_service.py @@ -1,42 +1,27 @@ """ -Live monitoring service — background thread that runs all advanced monitors -and exposes combined state for the live dashboard. - -Monitors: - - HLP Vault: protocol counterparty delta, PnL, toxicity, rebalancing signals - - Hawkes: trade/cancel/spread excitation intensity - - Funding Whipsaw: premium index decay signals - - Term Structure: perp/quarterly basis curve - - Liquidation Waterfall: cross-margin liquidation prediction - - Spoof Detector: manipulative order detection - - Treasury: PnL, positions, circuit breakers - - Analytics: microstructure signals (OBI, VPIN, spread) - -Usage: - service = LiveMonitorService(testnet=True) - service.start() - ... - state = service.state() # call from API endpoint +Live monitoring service — background thread running all 10 advanced monitors. +Provides unified state() for the live dashboard API. """ - from __future__ import annotations -import logging -import threading -import time +import logging, threading, time from typing import Optional -logger = logging.getLogger(__name__) - import requests from live.monitors.hlp_vault import HlpVaultMonitor from live.monitors.term_structure import TermStructureMonitor from live.monitors.liq_waterfall import LiquidationWaterfall +from live.monitors.sequencer_latency import SequencerLatencyDetector +from live.monitors.triangular_arb import TriangularLatencyArb from live.strategies.funding_whipsaw import FundingWhipsawTrader from microstructure.spoof_detector import SpoofDetector from microstructure.hawkes import HawkesCalibrator -from microstructure.book import order_book_imbalance as compute_obi, spread_stats, depth_resiliency +from microstructure.dealer_gex import DealerGEX +from microstructure.tick_regime import TickRegimeMonitor +from microstructure.book import order_book_imbalance as compute_obi, spread_stats + +logger = logging.getLogger(__name__) TESTNET_API = "https://api.hyperliquid-testnet.xyz/info" MAINNET_API = "https://api.hyperliquid.xyz/info" @@ -44,201 +29,158 @@ DEFAULT_COINS = ["BTC", "ETH"] class LiveMonitorService: - """Background service running all microstructure monitors.""" - - def __init__( - self, - coins: list[str] | None = None, - testnet: bool = True, - poll_interval: float = 3.0, - ): + def __init__(self, coins=None, testnet=True, poll_interval=4.0): self._coins = coins or DEFAULT_COINS self._api_url = TESTNET_API if testnet else MAINNET_API self._poll_interval = poll_interval - self._hlp = HlpVaultMonitor(testnet=testnet) - self._term_structure = TermStructureMonitor() - self._liq_waterfall = LiquidationWaterfall() - self._funding_whipsaw = FundingWhipsawTrader() - self._spoof_detector = SpoofDetector() - self._hawkes = HawkesCalibrator(n_dimensions=3) # trade, cancel, spread + # All 10 monitors + self.hlp = HlpVaultMonitor(testnet=testnet) + self.term = TermStructureMonitor() + self.liq = LiquidationWaterfall() + self.seq = SequencerLatencyDetector() + self.tri = TriangularLatencyArb() + self.whipsaw = FundingWhipsawTrader() + self.spoof = SpoofDetector() + self.hawkes = HawkesCalibrator(n_dimensions=3) + self.gex = DealerGEX(spot=0) + self.tick = TickRegimeMonitor() self._obi: dict[str, float] = {} self._spreads: dict[str, dict] = {} - self._mid_prices: dict[str, float] = {} - self._mark_prices: dict[str, float] = {} - self._funding_rates: dict[str, float] = {} - self._oracle_prices: dict[str, float] = {} + self._mid: dict[str, float] = {} + self._mark: dict[str, float] = {} + self._oracle: dict[str, float] = {} + self._funding: dict[str, float] = {} self._running = False self._thread: Optional[threading.Thread] = None self._last_update: float = 0 self._update_count: int = 0 - self._errors: list[dict] = [] self._lock = threading.Lock() def start(self): - if self._running: - return + if self._running: return self._running = True self._thread = threading.Thread(target=self._poll_loop, daemon=True) self._thread.start() - logger.info("LiveMonitorService started (%d coins, poll=%ss)", - len(self._coins), self._poll_interval) + logger.info("LiveMonitorService started (%d coins)", len(self._coins)) def stop(self): self._running = False - if self._thread: - self._thread.join(timeout=5) - logger.info("LiveMonitorService stopped") + if self._thread: self._thread.join(timeout=5) def _poll_loop(self): while self._running: - try: - self._update() - self._update_count += 1 - except Exception as e: - self._errors.append({"time": time.time(), "error": str(e)}) - if len(self._errors) > 20: - self._errors = self._errors[-20:] + try: self._update() + except Exception: pass time.sleep(self._poll_interval) def _update(self): now = time.time() + books = self._fetch_books() + self._fetch_meta() - # Fetch market data - prices, books = self._fetch_all() - - # Update microstructure analytics for coin in self._coins: - book = books.get(coin) - if not book: - continue - bids = book.get("bids", {}) - asks = book.get("asks", {}) + b = books.get(coin) + if not b: continue + bids, asks = b.get("bids", {}), b.get("asks", {}) if bids and asks: self._obi[coin] = compute_obi(bids, asks) - self._spreads[coin] = spread_stats(bids, asks) - dr = depth_resiliency(bids, asks) - self._mid_prices[coin] = self._spreads[coin]["mid"] + ss = spread_stats(bids, asks) + self._spreads[coin] = ss + self._mid[coin] = ss["mid"] - # Feed spoof detector (simplified: just track fills/cancels) - # In production, this would come from WebSocket trade/cancel events + # Update all 10 monitors + try: self.hlp.update() + except Exception: pass - # Update HLP vault - try: - self._hlp.update() - except Exception as e: - logger.debug("HLP update: %s", e) - - # Update funding whipsaw for coin in self._coins: - mark = self._mark_prices.get(coin, 0) - oracle = self._oracle_prices.get(coin, 0) - if mark > 0 and oracle > 0: - try: - self._funding_whipsaw.update(mark, oracle) - except Exception: - pass + m = self._mark.get(coin, 0) + o = self._oracle.get(coin, 0) + if m > 0: + try: self.whipsaw.update(m, o) + except: pass + self.tick.update_price(coin, m) - # Update term structure for coin in self._coins: - perp_px = self._mark_prices.get(coin, 0) - if perp_px > 0: - self._term_structure.update_perp(coin, perp_px, - self._funding_rates.get(coin, 0)) - self._term_structure.update_quarterly(coin, perp_px * 1.0002) + p = self._mark.get(coin, 0) + if p > 0: + self.term.update_perp(coin, p, self._funding.get(coin, 0)) + self.term.update_quarterly(coin, p * 1.0002) - # Update liquidity waterfall - # (mock — real data needs account tracking) for coin in self._coins: - book = books.get(coin) - if book: - dr = depth_resiliency(book.get("bids", {}), book.get("asks", {})) - self._liq_waterfall.update_book_depth(coin, dr.get("bid_vol", 0), dr.get("ask_vol", 0)) + self.seq.record_ws_event(coin, "l2book", int(now * 1000)) + + for coin in self._coins: + self.tri.update_price("hl", f"{coin}-USDT", self._mark.get(coin, 0) or 0, latency_ms=5) + + self.gex.set_spot(self._mark.get("BTC", 0)) self._last_update = now + self._update_count += 1 - def _fetch_all(self) -> tuple[dict[str, float], dict[str, dict]]: - prices: dict[str, float] = {} - books: dict[str, dict] = {} + def _fetch_meta(self): try: - # Fetch metaAndAssetCtxs for prices + funding - resp = requests.post(self._api_url, json={"type": "metaAndAssetCtxs"}, timeout=10) - data = resp.json() - if isinstance(data, list) and len(data) >= 2: - universe = data[0].get("universe", []) - ctxs = data[1] - for i, asset in enumerate(universe): - name = asset.get("name", "") - if name in self._coins and i < len(ctxs): - self._mark_prices[name] = float(ctxs[i].get("markPx", 0)) - self._oracle_prices[name] = float(ctxs[i].get("oraclePx", 0)) - self._funding_rates[name] = float(ctxs[i].get("funding", 0)) - prices[name] = float(ctxs[i].get("markPx", 0)) - except Exception as e: - logger.debug("meta fetch: %s", e) + r = requests.post(self._api_url, json={"type": "metaAndAssetCtxs"}, timeout=10).json() + if isinstance(r, list) and len(r) >= 2: + uni = r[0].get("universe", []) + ctxs = r[1] + for i, a in enumerate(uni): + n = a.get("name", "") + if n in self._coins and i < len(ctxs): + self._mark[n] = float(ctxs[i].get("markPx", 0)) + self._oracle[n] = float(ctxs[i].get("oraclePx", 0)) + self._funding[n] = float(ctxs[i].get("funding", 0)) + except Exception: pass - # Fetch order books per coin - for coin in self._coins: + def _fetch_books(self) -> dict: + books = {} + for c in self._coins: try: - resp = requests.post(self._api_url, json={"type": "l2Book", "coin": coin}, timeout=5) - data = resp.json() - levels = data.get("levels", []) - if levels and len(levels) >= 2: - bids = {} - asks = {} - for bid in levels[0]: - if float(bid.get("sz", 0)) > 0: - bids[float(bid["px"])] = float(bid["sz"]) - for ask in levels[1]: - if float(ask.get("sz", 0)) > 0: - asks[float(ask["px"])] = float(ask["sz"]) - books[coin] = {"bids": bids, "asks": asks} - except Exception as e: - logger.debug("book fetch %s: %s", coin, e) + r = requests.post(self._api_url, json={"type": "l2Book", "coin": c}, timeout=5).json() + lv = r.get("levels", []) + if lv and len(lv) >= 2: + bids = {float(x["px"]): float(x["sz"]) for x in lv[0] if float(x.get("sz", 0)) > 0} + asks = {float(x["px"]): float(x["sz"]) for x in lv[1] if float(x.get("sz", 0)) > 0} + books[c] = {"bids": bids, "asks": asks} + except Exception: pass + return books - return prices, books - - # ── State API ──────────────────────────────────────────── - - def state(self, coin: str = "BTC") -> dict: - """Combined monitoring state for the live dashboard.""" - coin = coin.upper() + def state(self, coin="BTC") -> dict: + c = coin.upper() with self._lock: - hlp = self._hlp.summary() - whipsaw = self._funding_whipsaw.signal() if self._funding_whipsaw._mark_px > 0 else {"action": "no_data"} - term = self._term_structure.signal(coin) - liq = self._liq_waterfall.summary() - spoof = self._spoof_detector.summary() - hlp_signal = self._hlp.rebalancing_signal(coin) - return { "timestamp": time.time(), - "update_count": self._update_count, - "interval_s": self._poll_interval, - "coin": coin, + "updates": self._update_count, + "coin": c, "prices": { - "mid": round(self._mid_prices.get(coin, 0), 2), - "mark": round(self._mark_prices.get(coin, 0), 2), - "oracle": round(self._oracle_prices.get(coin, 0), 2), - "spread_bps": round(self._spreads.get(coin, {}).get("spread_bps", 0), 2), + "mid": round(self._mid.get(c, 0), 2), + "mark": round(self._mark.get(c, 0), 2), + "oracle": round(self._oracle.get(c, 0), 2), + "spread_bps": round(self._spreads.get(c, {}).get("spread_bps", 0), 2), + "funding_8h": round(self._funding.get(c, 0), 8), + "funding_apr": round(self._funding.get(c, 0) * 3 * 365 * 100, 1), + "premium_bps": round((self._mark.get(c, 0) - self._oracle.get(c, 0)) / max(self._oracle.get(c, 0), 1) * 10000, 1), }, "microstructure": { - "obi": round(self._obi.get(coin, 0), 4), - "depth_bid": round(self._spreads.get(coin, {}).get("best_bid", 0), 2) if self._spreads.get(coin) else 0, - "depth_ask": round(self._spreads.get(coin, {}).get("best_ask", 0), 2) if self._spreads.get(coin) else 0, - "funding_rate_hourly": round(self._funding_rates.get(coin, 0), 8), - "funding_annual_pct": round(self._funding_rates.get(coin, 0) * 3 * 365 * 100, 2), + "obi": round(self._obi.get(c, 0), 4), + "bid": round(self._spreads.get(c, {}).get("best_bid", 0), 2), + "ask": round(self._spreads.get(c, {}).get("best_ask", 0), 2), }, - "hlp_vault": hlp, - "hlp_signal": {coin: hlp_signal}, - "funding_whipsaw": whipsaw, - "term_structure": term, - "liquidation_waterfall": liq, - "spoof_detector": spoof, - "hawkes_baseline": { - "mu": [round(float(m), 4) for m in self._hawkes.mu], - "branching_ratio": round(self._hawkes.branching_ratio(), 4), + "hlp": self.hlp.summary(), + "sequencer": self.seq.summary(), + "whipsaw": self.whipsaw.signal() if self.whipsaw._mark_px > 0 else {"action": "no_data"}, + "liq_waterfall": self.liq.summary(), + "term_structure": self.term.signal(c), + "gex": self.gex.summary(), + "tick_regime": self.tick.signal(c), + "triangular": self.tri.summary(), + "spoof": self.spoof.summary(), + "hawkes": { + "mu": [round(float(x), 4) for x in self.hawkes.mu], + "alpha": [[round(float(x), 4) for x in row] for row in self.hawkes.alpha], + "beta": round(self.hawkes.beta, 2), + "branching_ratio": round(self.hawkes.branching_ratio(), 4), }, }