Fix Binance option top-of-book via depth limit and ticker fallback.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-25 12:08:33 +08:00
parent 78fd046fb6
commit d701d30c04
4 changed files with 134 additions and 12 deletions
+1
View File
@@ -37,6 +37,7 @@ 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"
binance_http_proxy: str = ""
+30 -2
View File
@@ -84,8 +84,11 @@ class BinanceExchange:
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)
bids, asks, ts = self.rest.fetch_books(inst, sz=10)
if bids or asks:
self.cache.upsert_book(inst, bids=bids, asks=asks, ts_ms=ts)
else:
logger.warning("binance warm book empty: %s", inst)
except Exception as e:
logger.warning("binance warm book %s failed: %s", inst, e)
try:
@@ -111,7 +114,32 @@ class BinanceExchange:
def quote(self, inst_id: str) -> Quote | None:
return self.cache.get(inst_id)
def _refresh_quote_if_stale(self, inst_id: str) -> None:
if not inst_id:
return
q = self.cache.get(inst_id)
if q is not None and (q.bid is not None or q.ask is not None):
return
try:
bids, asks, ts = self.rest.fetch_books(inst_id, sz=10)
if bids or asks:
self.cache.upsert_book(inst_id, bids=bids, asks=asks, ts_ms=ts)
except Exception as e:
logger.debug("binance refresh quote %s: %s", inst_id, e)
def snapshot(self, perp_inst_id: str) -> MarketSnapshot:
# 期权 WS 偶发无推送时,用 REST/ticker 补买卖一,避免 UI 一直是 -
self._refresh_quote_if_stale(perp_inst_id)
pair = None
try:
# BookCache 无公开 getter;从 snapshot 前先按 pair 刷新
with self.cache._lock: # noqa: SLF001
pair = self.cache._pair
except Exception:
pair = None
if pair is not None:
self._refresh_quote_if_stale(pair.call_inst_id)
self._refresh_quote_if_stale(pair.put_inst_id)
return self.cache.snapshot(perp_inst_id)
def snapshot_dict(self, perp_inst_id: str) -> dict[str, Any]:
+47 -10
View File
@@ -115,22 +115,59 @@ class BinanceRestClient:
return self.fetch_mark_option(inst_id)
return self.fetch_mark_perp(inst_id)
def fetch_option_ticker(self, symbol: str) -> dict[str, Any] | None:
body = self._get_json(self._eapi, "/eapi/v1/ticker", {"symbol": symbol})
if isinstance(body, list) and body:
row = body[0]
return row if isinstance(row, dict) else None
if isinstance(body, dict):
return body
return None
def fetch_books(
self, inst_id: str, sz: int = 5
) -> tuple[list[BookLevel], list[BookLevel], int | None]:
from .parse import is_option_symbol
limit = max(5, min(int(sz), 100))
if is_option_symbol(inst_id):
body = self._get_json(
self._eapi, "/eapi/v1/depth", {"symbol": inst_id, "limit": limit}
)
else:
body = self._get_json(
self._fapi,
"/fapi/v1/depth",
{"symbol": inst_id.upper(), "limit": min(limit, 20)},
)
# 币安期权 depth 的 limit 仅支持 10/20/50/100 等,5 会失败
limit = 10 if int(sz) < 10 else min(int(sz), 100)
try:
body = self._get_json(
self._eapi, "/eapi/v1/depth", {"symbol": inst_id, "limit": limit}
)
except Exception:
body = None
bids: list[BookLevel] = []
asks: list[BookLevel] = []
ts_ms = None
if isinstance(body, dict):
ts = safe_float(body.get("T") or body.get("E") or body.get("time"))
ts_ms = int(ts) if ts is not None else None
bids = _levels(body.get("bids") or body.get("b") or [])
asks = _levels(body.get("asks") or body.get("a") or [])
# depth 空盘时回退 ticker 买卖一
if not bids or not asks:
tick = self.fetch_option_ticker(inst_id)
if tick:
bid = safe_float(tick.get("bidPrice") or tick.get("b"))
ask = safe_float(tick.get("askPrice") or tick.get("a"))
bid_sz = safe_float(tick.get("bidQty") or tick.get("B")) or 1.0
ask_sz = safe_float(tick.get("askQty") or tick.get("A")) or 1.0
ts = safe_float(tick.get("time") or tick.get("E") or tick.get("T"))
ts_ms = int(ts) if ts is not None else ts_ms
if bid is not None and bid > 0 and not bids:
bids = [BookLevel(px=bid, sz=bid_sz)]
if ask is not None and ask > 0 and not asks:
asks = [BookLevel(px=ask, sz=ask_sz)]
return bids, asks, ts_ms
limit = max(5, min(int(sz), 20))
body = self._get_json(
self._fapi,
"/fapi/v1/depth",
{"symbol": inst_id.upper(), "limit": limit},
)
if not isinstance(body, dict):
return [], [], None
ts = safe_float(body.get("T") or body.get("E") or body.get("time"))