"""币安交易所适配器:USDT 永续 + 欧洲期权公共行情(SIM 只读)。""" from __future__ import annotations import logging from typing import Any, Sequence from ...config import Settings, get_settings from ..book_cache import BookCache from ..types import BookLevel, MarketSnapshot, OptionPair, Quote from .parse import safe_float from .rest import BinanceRestClient from .ws import BinancePublicWs logger = logging.getLogger(__name__) class BinanceExchange: name = "binance" def __init__(self, settings: Settings | None = None) -> None: self.settings = settings or get_settings() self.cache = BookCache() proxy = ( self.settings.binance_http_proxy or self.settings.okx_http_proxy or None ) self.rest = BinanceRestClient( fapi_base=self.settings.binance_fapi_base, eapi_base=self.settings.binance_eapi_base, proxy=proxy, ) self.ws = BinancePublicWs( futures_ws_base=self.settings.binance_futures_ws, options_ws_base=self.settings.binance_options_ws, cache=self.cache, proxy=proxy, ) self._started = False self._ct_cache: dict[str, float] = {} async def start(self) -> None: if self._started: return self._started = True await self.ws.start() logger.info("Binance exchange started") async def stop(self) -> None: self._started = False await self.ws.stop() self.rest.close() logger.info("Binance exchange stopped") def list_option_contracts(self, family: str) -> list[dict[str, Any]]: contracts = self.rest.list_option_contracts(family) for c in contracts: if c.get("ct_mult"): self._ct_cache[str(c["inst_id"])] = float(c["ct_mult"]) return contracts def fetch_index(self, index_id: str) -> float | None: return self.rest.fetch_index(index_id) def fetch_mark(self, inst_id: str) -> float | None: return self.rest.fetch_mark(inst_id) def fetch_book( self, inst_id: str, depth: int = 5 ) -> tuple[list[BookLevel], list[BookLevel], int | None]: return self.rest.fetch_books(inst_id, sz=depth) def get_ct_mult(self, option_inst_id: str, family: str, default: float) -> float: if option_inst_id in self._ct_cache: return self._ct_cache[option_inst_id] # 币安 ETH 期权 unit 常见为 1 return float(default if default > 0 else 1.0) def set_pair(self, pair: OptionPair | None) -> None: self.cache.set_pair(pair) def warm_and_subscribe(self, inst_ids: Sequence[str]) -> None: ids = [i for i in inst_ids if i] for inst in ids: try: bids, asks, ts = self.rest.fetch_books(inst, sz=5) self.cache.upsert_book(inst, bids=bids, asks=asks, ts_ms=ts) except Exception as e: logger.warning("binance warm book %s failed: %s", inst, e) try: mp = self.rest.fetch_mark(inst) if mp: self.cache.set_mark_px(inst, mp) except Exception: pass # 指数 try: idx = self.rest.fetch_index(self.settings.index_inst_id) if idx: self.cache.set_index_px(idx) except Exception as e: logger.warning("binance index failed: %s", e) keep = set(ids) self.cache.drop_except(keep) self.ws.set_instruments(ids) async def resubscribe(self, inst_ids: Sequence[str]) -> None: await self.ws.resubscribe([i for i in inst_ids if i]) def quote(self, inst_id: str) -> Quote | None: return self.cache.get(inst_id) def snapshot(self, perp_inst_id: str) -> MarketSnapshot: return self.cache.snapshot(perp_inst_id) def snapshot_dict(self, perp_inst_id: str) -> dict[str, Any]: return self.snapshot(perp_inst_id).to_dict() def set_index_px(self, px: float | None) -> None: self.cache.set_index_px(px) def set_mark_px(self, inst_id: str, mark_px: float | None) -> None: self.cache.set_mark_px(inst_id, mark_px)