""" 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 """ from __future__ import annotations import logging import threading import 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.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 TESTNET_API = "https://api.hyperliquid-testnet.xyz/info" MAINNET_API = "https://api.hyperliquid.xyz/info" 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, ): 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 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._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 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) def stop(self): self._running = False if self._thread: self._thread.join(timeout=5) logger.info("LiveMonitorService stopped") 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:] time.sleep(self._poll_interval) def _update(self): now = time.time() # 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", {}) 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"] # Feed spoof detector (simplified: just track fills/cancels) # In production, this would come from WebSocket trade/cancel events # 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 # 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) # 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._last_update = now def _fetch_all(self) -> tuple[dict[str, float], dict[str, dict]]: prices: dict[str, float] = {} books: dict[str, dict] = {} 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) # Fetch order books per coin for coin 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) return prices, books # ── State API ──────────────────────────────────────────── def state(self, coin: str = "BTC") -> dict: """Combined monitoring state for the live dashboard.""" coin = 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, "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), }, "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), }, "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), }, }