diff --git a/lib/common/static/options_panel.js b/lib/common/static/options_panel.js
index 804446e..fffd075 100644
--- a/lib/common/static/options_panel.js
+++ b/lib/common/static/options_panel.js
@@ -1450,14 +1450,8 @@
const uly = state.underlying;
const seq = ++chainLoadSeq;
const btn = document.getElementById("opt-load-chain");
- const hadChain = chainHasExpiries(state.chain) && state.chain.underlying === uly;
- if (btn && !soft) {
- btn.disabled = true;
- if (!btn.dataset.origText) btn.dataset.origText = btn.textContent || "刷新链";
- btn.textContent = "刷新中…";
- }
- // 已有链时不先清空表格,避免「白屏等很久」的体感
- if (!soft && !hadChain) {
+ if (btn && !soft) btn.disabled = true;
+ if (!soft) {
setExpirySelectStatus("加载到期日中…");
const tbody = document.getElementById("opt-strike-tbody");
if (tbody) {
@@ -1468,18 +1462,9 @@
try {
let d = null;
let lastMsg = "";
- const expMs = (document.getElementById("opt-exp-select") || {}).value || "";
- // WS 已热时走 fast,跳过最慢的整家族 REST tickers
- const useFast = soft || quoteLiveWsOk || hadChain;
- const maxAttempts = soft ? 2 : 3;
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
+ for (let attempt = 0; attempt < 3; attempt++) {
if (seq !== chainLoadSeq) return;
- let url =
- "/api/options/chain?underlying=" +
- encodeURIComponent(uly) +
- (useFast ? "&fast=1" : "");
- if (expMs) url += "&exp_time=" + encodeURIComponent(expMs);
- d = await apiJson(url);
+ d = await apiJson("/api/options/chain?underlying=" + encodeURIComponent(uly));
if (seq !== chainLoadSeq) return;
if (d && d.ok && chainHasExpiries(d)) break;
lastMsg = (d && (d.msg || d.chain_error)) || "暂无到期日";
@@ -1487,14 +1472,14 @@
!!(d && d.rate_limited) ||
/50011|Too Many Requests|过于频繁/i.test(String(lastMsg || ""));
d = null;
- if (attempt < maxAttempts - 1) {
- if (!soft && !hadChain) {
+ if (attempt < 2) {
+ if (!soft) {
setExpirySelectStatus(
rateLimited ? "OKX 限频,稍后重试…" : "重试加载到期日…"
);
}
await new Promise(function (resolve) {
- setTimeout(resolve, rateLimited ? 1200 * (attempt + 1) : 300);
+ setTimeout(resolve, rateLimited ? 1200 * (attempt + 1) : 400);
});
}
}
@@ -1504,7 +1489,6 @@
if (!soft) {
renderExpiries();
renderStrikes();
- void watchCurrentExpiryQuotes();
}
return;
}
@@ -1526,7 +1510,7 @@
alert(friendly);
return;
}
- const keepExp = soft || hadChain ? (document.getElementById("opt-exp-select") || {}).value : "";
+ const keepExp = soft ? (document.getElementById("opt-exp-select") || {}).value : "";
state.chain = d;
panelCache.chain = d;
panelCache.underlying = uly;
@@ -1534,7 +1518,7 @@
chainQuotedAt = Date.now();
lastChainSoftAt = chainQuotedAt;
syncAskLiqFilterFromChain(d);
- if (!soft && !hadChain) {
+ if (!soft) {
state.selectedInst = null;
resetMoneyFilterToAll();
state.strikeExpandAll = false;
@@ -1544,19 +1528,18 @@
}
updateUnderlyingLabel();
renderExpiries();
- if (keepExp) {
+ if (soft && keepExp) {
const sel = document.getElementById("opt-exp-select");
if (sel && Array.from(sel.options).some(function (o) { return o.value === keepExp; })) {
sel.value = keepExp;
}
}
- // soft/已有链时保留 selectedInst;renderStrikes 会先 park 再按 prevSelected 静默重挂下单面板
+ // soft 时保留 selectedInst;renderStrikes 会先 park 再按 prevSelected 静默重挂下单面板
renderStrikes();
void watchCurrentExpiryQuotes();
startQuoteLiveStream();
} catch (e) {
if (seq !== chainLoadSeq || soft) return;
- if (hadChain) return;
setExpirySelectStatus("选择到期日");
const tbody = document.getElementById("opt-strike-tbody");
if (tbody) {
@@ -1566,10 +1549,7 @@
"";
}
} finally {
- if (seq === chainLoadSeq && btn) {
- btn.disabled = false;
- if (btn.dataset.origText) btn.textContent = btn.dataset.origText;
- }
+ if (seq === chainLoadSeq && btn) btn.disabled = false;
}
}
diff --git a/lib/exchange/okx_options_lib.py b/lib/exchange/okx_options_lib.py
index 5093e7b..4fb4e15 100644
--- a/lib/exchange/okx_options_lib.py
+++ b/lib/exchange/okx_options_lib.py
@@ -30,9 +30,6 @@ _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:
@@ -715,40 +712,17 @@ def fetch_option_instruments(
return []
-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"])
-
+def fetch_option_tickers(ex: ccxt.okx, inst_family: str) -> dict[str, dict[str, Any]]:
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": family}
+ {"instType": "OPTION", "instFamily": inst_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
@@ -756,8 +730,6 @@ def fetch_option_tickers(
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
@@ -771,9 +743,6 @@ 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"
@@ -794,13 +763,7 @@ def build_option_chain(
rate_limited = _is_okx_rate_limit(e)
if rate_limited:
instruments_err = "OKX 请求过于频繁(50011),请稍后点「刷新链」重试"
- 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}
+ tickers = fetch_option_tickers(ex, family)
expiries: dict[str, list[dict[str, Any]]] = {}
skipped_no_index = 0
for meta in instruments:
diff --git a/lib/options/options_quote_live_lib.py b/lib/options/options_quote_live_lib.py
index ba15b91..22a9e66 100644
--- a/lib/options/options_quote_live_lib.py
+++ b/lib/options/options_quote_live_lib.py
@@ -158,58 +158,7 @@ class OptionsQuoteLive:
args = [{"channel": "tickers", "instId": iid} for iid in merged]
for iid in sorted(index_insts):
args.append({"channel": "index-tickers", "instId": iid})
- # 订阅可能分片 sleep,不能堵 Flask 请求线程
- threading.Thread(
- target=self._ws.set_subscriptions,
- args=(args,),
- name="okx-options-quote-sub",
- daemon=True,
- ).start()
-
- def as_okx_tickers(self, underlying: str | None = None) -> dict[str, dict[str, Any]]:
- """转成 build_option_chain 可用的 OKX ticker 字段."""
- u = (underlying or "").upper()
- out: dict[str, dict[str, Any]] = {}
- with self._lock:
- for inst_id, q in self._tickers.items():
- if u and str(q.get("underlying") or "").upper() not in ("", u):
- continue
- row: dict[str, Any] = {"instId": inst_id}
- if q.get("ask") is not None and not q.get("ask_estimated"):
- row["askPx"] = q.get("ask")
- row["askSz"] = q.get("ask_sz")
- if q.get("bid") is not None:
- row["bidPx"] = q.get("bid")
- row["bidSz"] = q.get("bid_sz")
- if q.get("mark_px") is not None:
- row["markPx"] = q.get("mark_px")
- out[inst_id] = row
- return out
-
- def index_px_for(self, underlying: str) -> float | None:
- u = (underlying or "").upper()
- with self._lock:
- return self._index_by_uly.get(u)
-
- def is_ws_fresh(self, *, max_age_sec: float = 15.0) -> bool:
- if not self._ws.connected:
- return False
- last = float(self._ws.last_msg_at or 0)
- return last > 0 and (time.time() - last) <= max_age_sec
-
- def schedule_seed_from_chain(
- self,
- chain: dict[str, Any],
- *,
- exp_time: str | int | None = None,
- watcher_id: str | None = None,
- ) -> None:
- threading.Thread(
- target=self.seed_from_chain,
- kwargs={"chain": chain, "exp_time": exp_time, "watcher_id": watcher_id},
- name="options-quote-seed",
- daemon=True,
- ).start()
+ self._ws.set_subscriptions(args)
def seed_from_chain(
self,
diff --git a/lib/options/options_register.py b/lib/options/options_register.py
index 79d0991..c57c37c 100644
--- a/lib/options/options_register.py
+++ b/lib/options/options_register.py
@@ -374,24 +374,6 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
u = (request.args.get("underlying") or cfg["default_underly"]).upper()
# 热更新:链展示天数每次读 env,保存后刷新链即可
chain_max_dte = _env_float("OKX_OPTIONS_CHAIN_MAX_DTE_DAYS", float(cfg.get("chain_max_dte_days") or 14))
- fast = (request.args.get("fast") or "").strip().lower() in ("1", "true", "yes")
- force_tickers = (request.args.get("force_tickers") or "").strip().lower() in ("1", "true", "yes")
- watch_exp = (request.args.get("exp_time") or "").strip() or None
- live_index = None
- live_tickers = None
- ws_fresh = False
- try:
- from lib.options.options_quote_live_lib import options_quote_live
-
- ws_fresh = options_quote_live.is_ws_fresh()
- live_index = options_quote_live.index_px_for(u)
- live_tickers = options_quote_live.as_okx_tickers(u) or None
- except Exception:
- pass
- # fast: WS 已热则跳过整家族 REST tickers(最慢的一步),用 WS 缓存覆盖
- fetch_tickers = True
- if fast and ws_fresh and not force_tickers:
- fetch_tickers = False
try:
chain = cfg["build_option_chain"](
ex,
@@ -399,10 +381,6 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
max_dte_days=chain_max_dte,
itm_only=False,
itm_max_dist_usd=cfg["itm_max_dist"],
- index_px=live_index,
- tickers_override=live_tickers,
- fetch_tickers=fetch_tickers,
- force_tickers=force_tickers,
)
except Exception as e:
return jsonify({"ok": False, "msg": f"加载期权链失败: {e}"})
@@ -415,7 +393,8 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
try:
from lib.options.options_quote_live_lib import options_quote_live
- options_quote_live.schedule_seed_from_chain(chain, exp_time=watch_exp)
+ watch_exp = (request.args.get("exp_time") or "").strip() or None
+ options_quote_live.seed_from_chain(chain, exp_time=watch_exp)
except Exception:
pass
if not expiries:
@@ -428,8 +407,6 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
"ask_liq_filter_enabled": ask_liq_filter,
"budget_buffer": budget_buffer,
"trade_budget": cfg["trade_budget"],
- "chain_fast": fast,
- "ws_fresh": ws_fresh,
}
)
return jsonify(
@@ -441,9 +418,6 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
"budget_buffer": budget_buffer,
"trade_budget": cfg["trade_budget"],
"quote_live": True,
- "chain_fast": fast,
- "ws_fresh": ws_fresh,
- "tickers_fetched": fetch_tickers,
}
)
diff --git a/lib/options/templates/options_panel.html b/lib/options/templates/options_panel.html
index f2f7b0d..84306f7 100644
--- a/lib/options/templates/options_panel.html
+++ b/lib/options/templates/options_panel.html
@@ -322,4 +322,4 @@
-
+