Fix Binance option top-of-book via depth limit and ticker fallback.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -37,6 +37,7 @@ class Settings(BaseSettings):
|
|||||||
binance_fapi_base: str = "https://fapi.binance.com"
|
binance_fapi_base: str = "https://fapi.binance.com"
|
||||||
binance_eapi_base: str = "https://eapi.binance.com"
|
binance_eapi_base: str = "https://eapi.binance.com"
|
||||||
binance_futures_ws: str = "wss://fstream.binance.com/stream"
|
binance_futures_ws: str = "wss://fstream.binance.com/stream"
|
||||||
|
# 官方默认根路径 /eoptions;组合流用 /eoptions/stream?streams=
|
||||||
binance_options_ws: str = "wss://nbstream.binance.com/eoptions/stream"
|
binance_options_ws: str = "wss://nbstream.binance.com/eoptions/stream"
|
||||||
binance_http_proxy: str = ""
|
binance_http_proxy: str = ""
|
||||||
|
|
||||||
|
|||||||
@@ -84,8 +84,11 @@ class BinanceExchange:
|
|||||||
ids = [i for i in inst_ids if i]
|
ids = [i for i in inst_ids if i]
|
||||||
for inst in ids:
|
for inst in ids:
|
||||||
try:
|
try:
|
||||||
bids, asks, ts = self.rest.fetch_books(inst, sz=5)
|
bids, asks, ts = self.rest.fetch_books(inst, sz=10)
|
||||||
self.cache.upsert_book(inst, bids=bids, asks=asks, ts_ms=ts)
|
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:
|
except Exception as e:
|
||||||
logger.warning("binance warm book %s failed: %s", inst, e)
|
logger.warning("binance warm book %s failed: %s", inst, e)
|
||||||
try:
|
try:
|
||||||
@@ -111,7 +114,32 @@ class BinanceExchange:
|
|||||||
def quote(self, inst_id: str) -> Quote | None:
|
def quote(self, inst_id: str) -> Quote | None:
|
||||||
return self.cache.get(inst_id)
|
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:
|
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)
|
return self.cache.snapshot(perp_inst_id)
|
||||||
|
|
||||||
def snapshot_dict(self, perp_inst_id: str) -> dict[str, Any]:
|
def snapshot_dict(self, perp_inst_id: str) -> dict[str, Any]:
|
||||||
|
|||||||
@@ -115,22 +115,59 @@ class BinanceRestClient:
|
|||||||
return self.fetch_mark_option(inst_id)
|
return self.fetch_mark_option(inst_id)
|
||||||
return self.fetch_mark_perp(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(
|
def fetch_books(
|
||||||
self, inst_id: str, sz: int = 5
|
self, inst_id: str, sz: int = 5
|
||||||
) -> tuple[list[BookLevel], list[BookLevel], int | None]:
|
) -> tuple[list[BookLevel], list[BookLevel], int | None]:
|
||||||
from .parse import is_option_symbol
|
from .parse import is_option_symbol
|
||||||
|
|
||||||
limit = max(5, min(int(sz), 100))
|
|
||||||
if is_option_symbol(inst_id):
|
if is_option_symbol(inst_id):
|
||||||
body = self._get_json(
|
# 币安期权 depth 的 limit 仅支持 10/20/50/100 等,5 会失败
|
||||||
self._eapi, "/eapi/v1/depth", {"symbol": inst_id, "limit": limit}
|
limit = 10 if int(sz) < 10 else min(int(sz), 100)
|
||||||
)
|
try:
|
||||||
else:
|
body = self._get_json(
|
||||||
body = self._get_json(
|
self._eapi, "/eapi/v1/depth", {"symbol": inst_id, "limit": limit}
|
||||||
self._fapi,
|
)
|
||||||
"/fapi/v1/depth",
|
except Exception:
|
||||||
{"symbol": inst_id.upper(), "limit": min(limit, 20)},
|
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):
|
if not isinstance(body, dict):
|
||||||
return [], [], None
|
return [], [], None
|
||||||
ts = safe_float(body.get("T") or body.get("E") or body.get("time"))
|
ts = safe_float(body.get("T") or body.get("E") or body.get("time"))
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
"""服务器上探测币安期权盘口(只读)。用法: python scripts/probe_binance_options.py"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT / "backend"))
|
||||||
|
|
||||||
|
import httpx # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
proxy = None
|
||||||
|
env = ROOT / ".env"
|
||||||
|
if env.exists():
|
||||||
|
for line in env.read_text(encoding="utf-8").splitlines():
|
||||||
|
if line.startswith("BINANCE_HTTP_PROXY=") or line.startswith("OKX_HTTP_PROXY="):
|
||||||
|
v = line.split("=", 1)[1].strip().strip('"').strip("'")
|
||||||
|
if v:
|
||||||
|
proxy = v
|
||||||
|
print("proxy=", proxy)
|
||||||
|
kw: dict = {"timeout": 30, "trust_env": False}
|
||||||
|
if proxy:
|
||||||
|
kw["proxy"] = proxy
|
||||||
|
with httpx.Client(**kw) as cli:
|
||||||
|
info = cli.get("https://eapi.binance.com/eapi/v1/exchangeInfo")
|
||||||
|
print("exchangeInfo", info.status_code)
|
||||||
|
opts = (info.json() or {}).get("optionSymbols") or []
|
||||||
|
near = [
|
||||||
|
x
|
||||||
|
for x in opts
|
||||||
|
if str(x.get("symbol", "")).startswith("ETH-260726-1850")
|
||||||
|
or str(x.get("symbol", "")).startswith("ETH-250726-1850")
|
||||||
|
]
|
||||||
|
print("near1850", [x.get("symbol") for x in near[:6]])
|
||||||
|
if not near:
|
||||||
|
near = [x for x in opts if str(x.get("symbol", "")).startswith("ETH-") ][:4]
|
||||||
|
print("fallback sample", [x.get("symbol") for x in near])
|
||||||
|
for row in near[:4]:
|
||||||
|
sym = row["symbol"]
|
||||||
|
for limit in (5, 10, 20):
|
||||||
|
d = cli.get(
|
||||||
|
"https://eapi.binance.com/eapi/v1/depth",
|
||||||
|
params={"symbol": sym, "limit": limit},
|
||||||
|
)
|
||||||
|
print(f"depth limit={limit}", sym, d.status_code, d.text[:180])
|
||||||
|
t = cli.get("https://eapi.binance.com/eapi/v1/ticker", params={"symbol": sym})
|
||||||
|
print("ticker", sym, t.status_code, t.text[:240])
|
||||||
|
print("---")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user