import asyncio import json import logging import httpx import websockets from app.config import settings from app.services.price_store import price_store from app.ws.manager import manager logger = logging.getLogger(__name__) ASSET_MAP = { "btcusdt": "BTC", "ethusdt": "ETH", } async def _process_message(raw: str): try: data = json.loads(raw) # Combined stream wraps payload under "data" payload = data.get("data", data) if payload.get("e") != "kline": return kline = payload["k"] symbol = kline["s"].lower() asset = ASSET_MAP.get(symbol) if asset is None: return candle = { "time": kline["t"], # open time ms "open": float(kline["o"]), "high": float(kline["h"]), "low": float(kline["l"]), "close": float(kline["c"]), "volume": float(kline["v"]), } price_store.update(asset, candle) # TP/SL check on every tick (cheap no-op when no trades are being watched) from app.services.tp_sl_monitor import on_price_tick on_price_tick(asset, candle["close"]) # Price-impact peak tracker (updates rolling max for open post windows) from app.services.price_impact_monitor import on_price_tick as pi_tick pi_tick(asset, candle["high"], candle["low"], candle["close"]) # Broadcast live price tick await manager.broadcast({ "type": "price", "asset": asset, "price": candle["close"], "time": candle["time"], }) except Exception as exc: logger.warning("Error processing Binance message: %s", exc) async def fetch_historical(asset: str, symbol: str, interval: str = "1m", limit: int = 500): """Fetch historical klines from Binance REST API to pre-fill price_store.""" url = f"{settings.binance_rest_url}/api/v3/klines?symbol={symbol.upper()}&interval={interval}&limit={limit}" try: async with httpx.AsyncClient(timeout=15) as client: resp = await client.get(url) resp.raise_for_status() for row in resp.json(): candle = { "time": row[0], # open time ms "open": float(row[1]), "high": float(row[2]), "low": float(row[3]), "close": float(row[4]), "volume": float(row[5]), } price_store.update(asset, candle) logger.info("Loaded %d historical %s candles for %s", limit, interval, asset) except Exception as exc: logger.error("Failed to fetch historical data for %s: %s", asset, exc) async def run_binance_ws(): """Connect to Binance kline stream with exponential back-off on disconnect. Binance routinely cycles connections every ~24h, and intermediate proxies can drop without sending a close frame — both surface here as exceptions. The reconnect must be fast (sub-second) so price ticks aren't visibly stuck on the dashboard during a hiccup. Resilience defenses: * ping_interval/ping_timeout — peer-of-record liveness check (websockets lib) * close_timeout — bound how long we wait for a clean close handshake * open_timeout — bound the initial connect handshake so a hung TCP doesn't permanently park this task * max_queue — drop frames if downstream can't keep up rather than back the WS receive buffer up indefinitely """ # Pre-fill with historical data await fetch_historical("BTC", "btcusdt", limit=500) await fetch_historical("ETH", "ethusdt", limit=500) backoff = 1 while True: try: logger.info("Connecting to Binance WebSocket: %s", settings.binance_ws_url) async with websockets.connect( settings.binance_ws_url, ping_interval=20, ping_timeout=10, close_timeout=5, open_timeout=10, max_queue=128, ) as ws: backoff = 1 # reset on successful connection logger.info("Binance WebSocket connected.") async for message in ws: await _process_message(message) # If we exit the `async with` without an exception, the server # closed normally — reconnect immediately rather than backing off. logger.info("Binance WebSocket closed cleanly, reconnecting immediately.") except asyncio.CancelledError: logger.info("Binance WebSocket task cancelled.") return except Exception as exc: # Use exception() once so a TRACE-back-with-context is logged the # first time something interesting happens; subsequent same-error # retries log compactly to avoid drowning the log. logger.warning( "Binance WebSocket error: %s (%s). Reconnecting in %ds.", type(exc).__name__, exc, backoff, ) await asyncio.sleep(backoff) backoff = min(backoff * 2, 60)