fix(options): speed up chain refresh with fast path and non-blocking UI

Skip full REST tickers when WS is warm, seed subscriptions off-request, and keep the old chain visible while refreshing.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-11 12:06:16 +08:00
parent 14a7adae1f
commit 6fad68f7b1
5 changed files with 153 additions and 19 deletions
+40 -3
View File
@@ -30,6 +30,9 @@ _INSTRUMENTS_CACHE: dict[str, dict[str, Any]] = {}
_INSTRUMENTS_CACHE_LOCK = threading.Lock()
_INSTRUMENTS_CACHE_TTL_SEC = 90.0
_INSTRUMENTS_STALE_SEC = 600.0
_TICKERS_CACHE: dict[str, dict[str, Any]] = {}
_TICKERS_CACHE_LOCK = threading.Lock()
_TICKERS_CACHE_TTL_SEC = 8.0
def invalidate_options_balance_cache() -> None:
@@ -712,17 +715,40 @@ def fetch_option_instruments(
return []
def fetch_option_tickers(ex: ccxt.okx, inst_family: str) -> dict[str, dict[str, Any]]:
def fetch_option_tickers(
ex: ccxt.okx,
inst_family: str,
*,
force: bool = False,
) -> dict[str, dict[str, Any]]:
family = (inst_family or "").strip()
if not family:
return {}
now = time.time()
with _TICKERS_CACHE_LOCK:
cached = _TICKERS_CACHE.get(family)
if (
not force
and cached
and now - float(cached.get("updated_at") or 0) < _TICKERS_CACHE_TTL_SEC
and isinstance(cached.get("rows"), dict)
and cached["rows"]
):
return dict(cached["rows"])
out: dict[str, dict[str, Any]] = {}
last_err: BaseException | None = None
for attempt in range(3):
try:
rows = ex.public_get_market_tickers(
{"instType": "OPTION", "instFamily": inst_family}
{"instType": "OPTION", "instFamily": family}
).get("data") or []
for r in rows:
if isinstance(r, dict) and r.get("instId"):
out[str(r["instId"])] = r
if out:
with _TICKERS_CACHE_LOCK:
_TICKERS_CACHE[family] = {"updated_at": time.time(), "rows": dict(out)}
return out
except Exception as e:
last_err = e
@@ -730,6 +756,8 @@ def fetch_option_tickers(ex: ccxt.okx, inst_family: str) -> dict[str, dict[str,
time.sleep(0.6 * (attempt + 1))
continue
break
if cached and isinstance(cached.get("rows"), dict) and cached["rows"]:
return dict(cached["rows"])
if last_err is not None and _is_okx_rate_limit(last_err):
return out
return out
@@ -743,6 +771,9 @@ def build_option_chain(
itm_only: bool = True,
itm_max_dist_usd: float = 30.0,
index_px: float | None = None,
tickers_override: dict[str, dict[str, Any]] | None = None,
fetch_tickers: bool = True,
force_tickers: bool = False,
) -> dict[str, Any]:
u = (underlying or "ETH").upper()
family = f"{u}-USD_UM"
@@ -763,7 +794,13 @@ def build_option_chain(
rate_limited = _is_okx_rate_limit(e)
if rate_limited:
instruments_err = "OKX 请求过于频繁(50011),请稍后点「刷新链」重试"
tickers = fetch_option_tickers(ex, family)
tickers: dict[str, dict[str, Any]] = {}
if fetch_tickers:
tickers = fetch_option_tickers(ex, family, force=force_tickers)
if tickers_override:
for iid, row in tickers_override.items():
if isinstance(row, dict) and iid:
tickers[str(iid)] = {**(tickers.get(str(iid)) or {}), **row}
expiries: dict[str, list[dict[str, Any]]] = {}
skipped_no_index = 0
for meta in instruments: