"""Supplemental price feed for HL-native assets not listed on Binance. Polls HL allMids endpoint every 2 seconds and pumps price_store + tp_sl_monitor for assets in HL_PRICE_ASSETS. Runs alongside run_binance_ws(). WHY THIS EXISTS: tp_sl_monitor.on_price_tick(asset, price) is the sole mechanism that fires stop-loss, take-profit, and trailing-stop for live bot trades. HL-native assets (HYPE, PURR, …) are not listed on Binance, so the Binance WebSocket in binance.py cannot provide their prices. Without this feed, any bot trade on an HL-native asset silently loses ALL TP/SL protection — only the max_hold asyncio timer remains as an exit. COVERAGE: • Binance WS → BTC, ETH, SOL, TRUMP, BNB, DOGE, LINK, AAVE (ASSET_MAP) • This feed → HYPE, PURR (HL_PRICE_ASSETS) Adding a new HL-native asset: 1. Add its symbol (uppercase) to HL_PRICE_ASSETS below. 2. Verify the symbol appears in HL allMids by running: curl -s -X POST https://api.hyperliquid.xyz/info \\ -H 'Content-Type: application/json' \\ -d '{"type":"allMids"}' | python3 -m json.tool | grep HYPE """ from __future__ import annotations import asyncio import logging import time from typing import Optional import httpx from app.services.price_store import price_store from app.ws.manager import manager logger = logging.getLogger(__name__) HL_API_URL = "https://api.hyperliquid.xyz/info" # HL-native assets not covered by the Binance WS. Add new ones here as needed. HL_PRICE_ASSETS: frozenset[str] = frozenset({"HYPE", "PURR"}) # Poll cadence — 2 s gives ~same freshness as a Binance 1-min kline close # without hammering the free HL REST endpoint (30 req/min well within limit). _POLL_INTERVAL = 2.0 # Exponential backoff caps: start at 5 s, max at 60 s, reset on clean loop. _BACKOFF_START = 5 _BACKOFF_MAX = 60 async def run_hl_price_feed() -> None: """Long-running loop. Retries with exponential backoff on connection failure. Designed to be created once as an asyncio task at startup and cancelled at shutdown — matches the run_binance_ws() lifecycle pattern.""" logger.info("HL supplemental price feed starting for %s", sorted(HL_PRICE_ASSETS)) backoff = _BACKOFF_START while True: try: await _poll_loop() except asyncio.CancelledError: logger.info("HL price feed cancelled — shutting down.") return except Exception as exc: logger.warning( "HL price feed outer error: %s — retrying in %ds", exc, backoff ) await asyncio.sleep(backoff) backoff = min(backoff * 2, _BACKOFF_MAX) else: backoff = _BACKOFF_START # reset on clean exit (shouldn't happen normally) async def _poll_loop() -> None: """Inner loop: tick every 2 s. Individual tick failures are swallowed so a transient HTTP 5xx doesn't break the whole loop.""" while True: try: await _tick() except asyncio.CancelledError: raise except Exception as exc: # Transient: network blip, HL rate limit, malformed JSON. logger.debug("HL price tick error (suppressed): %s", exc) await asyncio.sleep(_POLL_INTERVAL) async def _tick() -> None: """Single price fetch + dispatch cycle for all HL_PRICE_ASSETS.""" now_ms = int(time.time() * 1000) async with httpx.AsyncClient(timeout=4.0) as c: r = await c.post(HL_API_URL, json={"type": "allMids"}) r.raise_for_status() mids: dict = r.json() # {"BTC": "74541.0", "HYPE": "13.5", …} for asset in HL_PRICE_ASSETS: raw: Optional[str] = mids.get(asset) if raw is None: # Asset not listed on HL yet — skip silently. continue try: price = float(raw) except (TypeError, ValueError): logger.warning("HL price feed: bad value for %s: %r", asset, raw) continue if price <= 0: continue # Synthetic 1-minute candle aligned to the current minute bucket. # All OHLC set to the mid price (no spread data from allMids). # price_store.update() handles the same-bucket replace logic. candle = { "time": (now_ms // 60_000) * 60_000, "open": price, "high": price, "low": price, "close": price, "volume": 0.0, } price_store.update(asset, candle) # TP/SL + trailing-stop evaluation on every tick. from app.services.tp_sl_monitor import on_price_tick on_price_tick(asset, price) # Price-impact peak tracker for Trump-post windows. from app.services.price_impact_monitor import on_price_tick as pi_tick pi_tick(asset, price, price, price) # Live price broadcast to the frontend dashboard. await manager.broadcast({ "type": "price", "asset": asset, "price": price, "time": now_ms, })