修复期权链拉取触发 OKX 50011 限频
为 instruments 加进程缓存并在限频时回退旧数据,前端遇 50011 不再连打重试。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1262,10 +1262,14 @@
|
|||||||
if (seq !== chainLoadSeq) return;
|
if (seq !== chainLoadSeq) return;
|
||||||
if (d && d.ok && chainHasExpiries(d)) break;
|
if (d && d.ok && chainHasExpiries(d)) break;
|
||||||
lastMsg = (d && (d.msg || d.chain_error)) || "暂无到期日";
|
lastMsg = (d && (d.msg || d.chain_error)) || "暂无到期日";
|
||||||
|
const rateLimited =
|
||||||
|
/50011|Too Many Requests|RateLimit/i.test(String(lastMsg || ""));
|
||||||
d = null;
|
d = null;
|
||||||
if (attempt === 0) {
|
if (attempt === 0 && !rateLimited) {
|
||||||
if (!soft) setExpirySelectStatus("重试加载到期日…");
|
if (!soft) setExpirySelectStatus("重试加载到期日…");
|
||||||
await new Promise(function (resolve) { setTimeout(resolve, 400); });
|
await new Promise(function (resolve) { setTimeout(resolve, 400); });
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (seq !== chainLoadSeq) return;
|
if (seq !== chainLoadSeq) return;
|
||||||
|
|||||||
@@ -26,12 +26,26 @@ _OKX_OPTION_ERR_ZH: dict[str, str] = {
|
|||||||
|
|
||||||
_OPTIONS_BALANCE_CACHE: dict[str, Any] = {"updated_at": 0.0, "data": None}
|
_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:
|
def invalidate_options_balance_cache() -> None:
|
||||||
_OPTIONS_BALANCE_CACHE["updated_at"] = 0.0
|
_OPTIONS_BALANCE_CACHE["updated_at"] = 0.0
|
||||||
_OPTIONS_BALANCE_CACHE["data"] = None
|
_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:
|
def _okx_trade_error_message(exc: BaseException | None = None, resp: Any = None) -> str:
|
||||||
row: dict[str, Any] | None = None
|
row: dict[str, Any] | None = None
|
||||||
if isinstance(resp, dict):
|
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)
|
family = inst_family_from_inst_id(inst_id)
|
||||||
if not family:
|
if not family:
|
||||||
return None
|
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
|
last_err: BaseException | None = None
|
||||||
for attempt in range(3):
|
for attempt in range(2):
|
||||||
try:
|
try:
|
||||||
rows = ex.public_get_public_instruments(
|
rows = ex.public_get_public_instruments(
|
||||||
{"instType": "OPTION", "instFamily": family, "instId": inst_id}
|
{"instType": "OPTION", "instFamily": family, "instId": inst_id}
|
||||||
).get("data") or []
|
).get("data") or []
|
||||||
if rows and isinstance(rows[0], dict):
|
if rows and isinstance(rows[0], dict):
|
||||||
return rows[0]
|
return rows[0]
|
||||||
rows = ex.public_get_public_instruments(
|
rows = fetch_option_instruments(ex, family, allow_stale=True)
|
||||||
{"instType": "OPTION", "instFamily": family}
|
|
||||||
).get("data") or []
|
|
||||||
for r in rows:
|
for r in rows:
|
||||||
if isinstance(r, dict) and str(r.get("instId")) == inst_id:
|
if isinstance(r, dict) and str(r.get("instId")) == inst_id:
|
||||||
return r
|
return r
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
last_err = e
|
last_err = e
|
||||||
if _is_okx_rate_limit(e) and attempt < 2:
|
if _is_okx_rate_limit(e) and attempt < 1:
|
||||||
time.sleep(0.45 * (attempt + 1))
|
time.sleep(1.2)
|
||||||
continue
|
continue
|
||||||
break
|
break
|
||||||
if last_err is not None and _is_okx_rate_limit(last_err):
|
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(
|
def fetch_option_instruments(
|
||||||
ex: ccxt.okx,
|
ex: ccxt.okx,
|
||||||
inst_family: str,
|
inst_family: str,
|
||||||
|
*,
|
||||||
|
force: bool = False,
|
||||||
|
allow_stale: bool = True,
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
rows = ex.public_get_public_instruments(
|
"""拉取 OPTION instruments;进程内缓存,50011 时回退旧列表."""
|
||||||
{"instType": "OPTION", "instFamily": inst_family}
|
family = str(inst_family or "").strip()
|
||||||
).get("data") or []
|
if not family:
|
||||||
return [r for r in rows if isinstance(r, dict) and r.get("state") == "live"]
|
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]]:
|
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
|
max_ms = now_ms + max_dte_days * 86400 * 1000
|
||||||
instruments_err = ""
|
instruments_err = ""
|
||||||
instruments: list[dict[str, Any]] = []
|
instruments: list[dict[str, Any]] = []
|
||||||
for attempt in range(2):
|
try:
|
||||||
try:
|
instruments = fetch_option_instruments(ex, family)
|
||||||
instruments = fetch_option_instruments(ex, family)
|
if not instruments:
|
||||||
instruments_err = ""
|
# 空列表可能是瞬时空;短退避后强制再拉一次(非 50011)
|
||||||
if instruments:
|
time.sleep(0.5)
|
||||||
break
|
instruments = fetch_option_instruments(ex, family, force=True)
|
||||||
|
if not instruments:
|
||||||
instruments_err = "期权合约列表为空"
|
instruments_err = "期权合约列表为空"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
instruments = []
|
instruments = []
|
||||||
instruments_err = str(e) or e.__class__.__name__
|
instruments_err = str(e) or e.__class__.__name__
|
||||||
if attempt == 0:
|
# 限频:再等一下用 stale/缓存,不要连打
|
||||||
time.sleep(0.35)
|
if _is_okx_rate_limit(e):
|
||||||
continue
|
time.sleep(1.5)
|
||||||
break
|
try:
|
||||||
if attempt == 0 and not instruments:
|
instruments = fetch_option_instruments(ex, family, allow_stale=True)
|
||||||
time.sleep(0.35)
|
if instruments:
|
||||||
|
instruments_err = ""
|
||||||
|
except Exception as e2:
|
||||||
|
instruments_err = str(e2) or e2.__class__.__name__
|
||||||
tickers = fetch_option_tickers(ex, family)
|
tickers = fetch_option_tickers(ex, family)
|
||||||
expiries: dict[str, list[dict[str, Any]]] = {}
|
expiries: dict[str, list[dict[str, Any]]] = {}
|
||||||
skipped_no_index = 0
|
skipped_no_index = 0
|
||||||
|
|||||||
@@ -324,4 +324,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/static/options_expiry_countdown.js?v=1"></script>
|
<script src="/static/options_expiry_countdown.js?v=1"></script>
|
||||||
<script src="/static/options_panel.js?v=55"></script>
|
<script src="/static/options_panel.js?v=56"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user