From a354811a6e3ad5371e71b2a5a9cc58bb02e8a0b9 Mon Sep 17 00:00:00 2001 From: dekun Date: Tue, 11 Aug 2026 17:31:46 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=9C=9F=E6=9D=83=E9=93=BE?= =?UTF-8?q?=E6=8B=89=E5=8F=96=E8=A7=A6=E5=8F=91=20OKX=2050011=20=E9=99=90?= =?UTF-8?q?=E9=A2=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为 instruments 加进程缓存并在限频时回退旧数据,前端遇 50011 不再连打重试。 Co-authored-by: Cursor --- lib/common/static/options_panel.js | 6 +- lib/exchange/okx_options_lib.py | 105 +++++++++++++++++------ lib/options/templates/options_panel.html | 2 +- 3 files changed, 86 insertions(+), 27 deletions(-) diff --git a/lib/common/static/options_panel.js b/lib/common/static/options_panel.js index c2f7a69..de480a3 100644 --- a/lib/common/static/options_panel.js +++ b/lib/common/static/options_panel.js @@ -1262,10 +1262,14 @@ if (seq !== chainLoadSeq) return; if (d && d.ok && chainHasExpiries(d)) break; lastMsg = (d && (d.msg || d.chain_error)) || "暂无到期日"; + const rateLimited = + /50011|Too Many Requests|RateLimit/i.test(String(lastMsg || "")); d = null; - if (attempt === 0) { + if (attempt === 0 && !rateLimited) { if (!soft) setExpirySelectStatus("重试加载到期日…"); await new Promise(function (resolve) { setTimeout(resolve, 400); }); + } else { + break; } } if (seq !== chainLoadSeq) return; diff --git a/lib/exchange/okx_options_lib.py b/lib/exchange/okx_options_lib.py index d477ab3..c709c71 100644 --- a/lib/exchange/okx_options_lib.py +++ b/lib/exchange/okx_options_lib.py @@ -26,12 +26,26 @@ _OKX_OPTION_ERR_ZH: dict[str, str] = { _OPTIONS_BALANCE_CACHE: dict[str, Any] = {"updated_at": 0.0, "data": None} +# public/instruments 全族缓存:合约列表变化慢,限频时用旧数据保活 +_OPTION_INSTRUMENTS_CACHE: dict[str, dict[str, Any]] = {} +_OPTION_INSTRUMENTS_CACHE_LOCK = threading.Lock() +_OPTION_INSTRUMENTS_CACHE_TTL = 90.0 +_OPTION_INSTRUMENTS_STALE_MAX = 600.0 + def invalidate_options_balance_cache() -> None: _OPTIONS_BALANCE_CACHE["updated_at"] = 0.0 _OPTIONS_BALANCE_CACHE["data"] = None +def invalidate_option_instruments_cache(inst_family: str | None = None) -> None: + with _OPTION_INSTRUMENTS_CACHE_LOCK: + if inst_family: + _OPTION_INSTRUMENTS_CACHE.pop(str(inst_family), None) + else: + _OPTION_INSTRUMENTS_CACHE.clear() + + def _okx_trade_error_message(exc: BaseException | None = None, resp: Any = None) -> str: row: dict[str, Any] | None = None if isinstance(resp, dict): @@ -407,25 +421,31 @@ def fetch_option_instrument_meta(ex: ccxt.okx, inst_id: str) -> dict[str, Any] | family = inst_family_from_inst_id(inst_id) if not family: return None + # 优先从全族缓存取,避免每选一腿再打 instruments + try: + cached_rows = fetch_option_instruments(ex, family, allow_stale=True) + for r in cached_rows: + if isinstance(r, dict) and str(r.get("instId")) == inst_id: + return r + except Exception: + pass last_err: BaseException | None = None - for attempt in range(3): + for attempt in range(2): try: rows = ex.public_get_public_instruments( {"instType": "OPTION", "instFamily": family, "instId": inst_id} ).get("data") or [] if rows and isinstance(rows[0], dict): return rows[0] - rows = ex.public_get_public_instruments( - {"instType": "OPTION", "instFamily": family} - ).get("data") or [] + rows = fetch_option_instruments(ex, family, allow_stale=True) for r in rows: if isinstance(r, dict) and str(r.get("instId")) == inst_id: return r return None except Exception as e: last_err = e - if _is_okx_rate_limit(e) and attempt < 2: - time.sleep(0.45 * (attempt + 1)) + if _is_okx_rate_limit(e) and attempt < 1: + time.sleep(1.2) continue break if last_err is not None and _is_okx_rate_limit(last_err): @@ -645,11 +665,42 @@ def fetch_index_price(ex: ccxt.okx, uly: str) -> float | None: def fetch_option_instruments( ex: ccxt.okx, inst_family: str, + *, + force: bool = False, + allow_stale: bool = True, ) -> list[dict[str, Any]]: - rows = ex.public_get_public_instruments( - {"instType": "OPTION", "instFamily": inst_family} - ).get("data") or [] - return [r for r in rows if isinstance(r, dict) and r.get("state") == "live"] + """拉取 OPTION instruments;进程内缓存,50011 时回退旧列表.""" + family = str(inst_family or "").strip() + if not family: + return [] + now = time.time() + with _OPTION_INSTRUMENTS_CACHE_LOCK: + entry = _OPTION_INSTRUMENTS_CACHE.get(family) + if ( + not force + and entry is not None + and entry.get("rows") is not None + and now - float(entry.get("updated_at") or 0) < _OPTION_INSTRUMENTS_CACHE_TTL + ): + return list(entry["rows"]) + + try: + rows = ex.public_get_public_instruments( + {"instType": "OPTION", "instFamily": family} + ).get("data") or [] + live = [r for r in rows if isinstance(r, dict) and r.get("state") == "live"] + with _OPTION_INSTRUMENTS_CACHE_LOCK: + _OPTION_INSTRUMENTS_CACHE[family] = {"updated_at": now, "rows": live} + return list(live) + except Exception as e: + if allow_stale: + with _OPTION_INSTRUMENTS_CACHE_LOCK: + entry = _OPTION_INSTRUMENTS_CACHE.get(family) + if entry is not None and entry.get("rows") is not None: + age = now - float(entry.get("updated_at") or 0) + if age <= _OPTION_INSTRUMENTS_STALE_MAX: + return list(entry["rows"]) + raise def fetch_option_tickers(ex: ccxt.okx, inst_family: str) -> dict[str, dict[str, Any]]: @@ -683,22 +734,26 @@ def build_option_chain( max_ms = now_ms + max_dte_days * 86400 * 1000 instruments_err = "" instruments: list[dict[str, Any]] = [] - for attempt in range(2): - try: - instruments = fetch_option_instruments(ex, family) - instruments_err = "" - if instruments: - break + try: + instruments = fetch_option_instruments(ex, family) + if not instruments: + # 空列表可能是瞬时空;短退避后强制再拉一次(非 50011) + time.sleep(0.5) + instruments = fetch_option_instruments(ex, family, force=True) + if not instruments: instruments_err = "期权合约列表为空" - except Exception as e: - instruments = [] - instruments_err = str(e) or e.__class__.__name__ - if attempt == 0: - time.sleep(0.35) - continue - break - if attempt == 0 and not instruments: - time.sleep(0.35) + except Exception as e: + instruments = [] + instruments_err = str(e) or e.__class__.__name__ + # 限频:再等一下用 stale/缓存,不要连打 + if _is_okx_rate_limit(e): + time.sleep(1.5) + try: + instruments = fetch_option_instruments(ex, family, allow_stale=True) + if instruments: + instruments_err = "" + except Exception as e2: + instruments_err = str(e2) or e2.__class__.__name__ tickers = fetch_option_tickers(ex, family) expiries: dict[str, list[dict[str, Any]]] = {} skipped_no_index = 0 diff --git a/lib/options/templates/options_panel.html b/lib/options/templates/options_panel.html index 161c831..363a8d8 100644 --- a/lib/options/templates/options_panel.html +++ b/lib/options/templates/options_panel.html @@ -324,4 +324,4 @@ - +