diff --git a/backend/app/config.py b/backend/app/config.py index 5162f45..3b5dc6b 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -37,8 +37,8 @@ class Settings(BaseSettings): binance_fapi_base: str = "https://fapi.binance.com" binance_eapi_base: str = "https://eapi.binance.com" binance_futures_ws: str = "wss://fstream.binance.com/stream" - # 官方默认根路径 /eoptions;组合流用 /eoptions/stream?streams= - binance_options_ws: str = "wss://nbstream.binance.com/eoptions/stream" + # 欧式期权公共流(2025-12 起):/public/stream;旧 nbstream/eoptions 已 404 + binance_options_ws: str = "wss://fstream.binance.com/public/stream" binance_http_proxy: str = "" perp_inst_id: str = "ETH-USDT-SWAP" diff --git a/backend/app/exchange/binance/ws.py b/backend/app/exchange/binance/ws.py index 86f256c..dc86f36 100644 --- a/backend/app/exchange/binance/ws.py +++ b/backend/app/exchange/binance/ws.py @@ -35,6 +35,7 @@ class BinancePublicWs: self._inst_ids: list[str] = [] self._tasks: list[asyncio.Task[None]] = [] self._stop = asyncio.Event() + self._alive: set[str] = set() def set_instruments(self, inst_ids: list[str]) -> None: self._inst_ids = [i for i in inst_ids if i] @@ -49,6 +50,14 @@ class BinancePublicWs: perps.append(i.upper()) return perps, opts + def _mark_alive(self, kind: str, ok: bool) -> None: + if ok: + self._alive.add(kind) + else: + self._alive.discard(kind) + # 任一腿连上即认为行情可用(期权腿偶发失败时仍可用 REST/永续) + self.cache.set_connected(bool(self._alive)) + async def start(self) -> None: if self._tasks and any(not t.done() for t in self._tasks): return @@ -65,6 +74,7 @@ class BinancePublicWs: except asyncio.CancelledError: pass self._tasks = [] + self._alive.clear() self.cache.set_connected(False) async def resubscribe(self, inst_ids: list[str]) -> None: @@ -76,13 +86,15 @@ class BinancePublicWs: async def _spawn(self) -> None: perps, opts = self._split() self._tasks = [] + self._alive.clear() if perps: url = self._combined_url(self.futures_ws_base, [f"{p.lower()}@bookTicker" for p in perps]) self._tasks.append( asyncio.create_task(self._run_forever(url, kind="futures"), name="bn-fapi-ws") ) if opts: - streams = [f"{s}@bookTicker" for s in opts] + # 期权 stream 名必须小写:eth-260726-1850-c@bookTicker + streams = [f"{s.lower()}@bookTicker" for s in opts] url = self._combined_url(self.options_ws_base, streams) self._tasks.append( asyncio.create_task(self._run_forever(url, kind="options"), name="bn-eapi-ws") @@ -127,9 +139,9 @@ class BinancePublicWs: while not self._stop.is_set(): try: async with await self._open_connection(url) as ws: - self.cache.set_connected(True) + self._mark_alive(kind, True) backoff = 1.0 - logger.info("Binance %s WS connected: %s", kind, url[:120]) + logger.info("Binance %s WS connected: %s", kind, url[:160]) waiter = asyncio.create_task(self._stop.wait()) reader = asyncio.create_task(self._read_loop(ws)) pinger = asyncio.create_task(self._ping_loop(ws)) @@ -147,13 +159,13 @@ class BinancePublicWs: raise except Exception as e: logger.warning("Binance %s WS disconnected: %s", kind, e) - self.cache.set_connected(False) + self._mark_alive(kind, False) try: await asyncio.wait_for(self._stop.wait(), timeout=backoff) break except asyncio.TimeoutError: backoff = min(backoff * 2, 30.0) - self.cache.set_connected(False) + self._mark_alive(kind, False) async def _ping_loop(self, ws: ClientConnection) -> None: while True: