Fix intermittent empty options expiry dropdown after coin select.

Retry instrument fetch, reject empty chains instead of caching them, and ignore stale loadChain races.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-16 12:04:08 +08:00
parent b50eebe37c
commit 12c4e55f24
4 changed files with 173 additions and 38 deletions
+46 -10
View File
@@ -596,13 +596,10 @@ def fetch_option_instruments(
ex: ccxt.okx,
inst_family: str,
) -> list[dict[str, Any]]:
try:
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"]
except Exception:
return []
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"]
def fetch_option_tickers(ex: ccxt.okx, inst_family: str) -> dict[str, dict[str, Any]]:
@@ -634,9 +631,27 @@ def build_option_chain(
idx = index_px if index_px is not None else fetch_index_price(ex, uly)
now_ms = time.time() * 1000
max_ms = now_ms + max_dte_days * 86400 * 1000
instruments = fetch_option_instruments(ex, family)
instruments_err = ""
instruments: list[dict[str, Any]] = []
for attempt in range(2):
try:
instruments = fetch_option_instruments(ex, family)
instruments_err = ""
if instruments:
break
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)
tickers = fetch_option_tickers(ex, family)
expiries: dict[str, list[dict[str, Any]]] = {}
skipped_no_index = 0
for meta in instruments:
try:
exp_ms = int(meta.get("expTime") or 0)
@@ -646,7 +661,10 @@ def build_option_chain(
continue
opt_type = str(meta.get("optType") or "")
strike = _safe_float(meta.get("stk"))
if strike is None or idx is None:
if strike is None:
continue
if idx is None:
skipped_no_index += 1
continue
if itm_only and not is_shallow_itm(
opt_type=opt_type,
@@ -702,7 +720,25 @@ def build_option_chain(
for exp_ms_str, contracts in sorted(expiries.items(), key=lambda x: int(x[0])):
contracts.sort(key=lambda c: (c["opt_type"], c["strike"]))
exp_list.append({"exp_time": int(exp_ms_str), "contracts": contracts})
return {"underlying": u, "index_px": idx, "inst_family": family, "expiries": exp_list}
out: dict[str, Any] = {
"underlying": u,
"index_px": idx,
"inst_family": family,
"expiries": exp_list,
"instruments_count": len(instruments),
}
if not exp_list:
if instruments_err:
out["chain_error"] = f"拉取期权合约失败: {instruments_err}"
elif idx is None:
out["chain_error"] = "指数价获取失败,无法构建期权链"
elif skipped_no_index:
out["chain_error"] = "指数价缺失,合约已跳过"
elif instruments:
out["chain_error"] = f"{max_dte_days:g} 日内无可用到期(已过滤 {len(instruments)} 个合约)"
else:
out["chain_error"] = "期权合约列表为空,请稍后刷新"
return out
def quote_option_contract(ex: ccxt.okx, inst_id: str) -> dict[str, Any]: