diff --git a/lib/common/static/instance_theme.css b/lib/common/static/instance_theme.css
index 2e19ce9..7e5bf2b 100644
--- a/lib/common/static/instance_theme.css
+++ b/lib/common/static/instance_theme.css
@@ -3754,8 +3754,11 @@ html[data-theme="light"] .opt-pending-item {
.options-page-wrap .opt-order-panel-inner .opt-order-title {
font-size: 0.82rem;
}
-.opt-order-panel-host {
- display: none;
+.opt-order-panel-host:not([hidden]) {
+ display: block;
+}
+.opt-order-panel-host[hidden] {
+ display: none !important;
}
.options-order-grid {
display: grid;
diff --git a/lib/common/static/options_panel.js b/lib/common/static/options_panel.js
index 2c827e8..ff46fd2 100644
--- a/lib/common/static/options_panel.js
+++ b/lib/common/static/options_panel.js
@@ -27,11 +27,16 @@
let lastGoodPositionsAt = 0;
let positionsRefreshSeq = 0;
let chainLoadSeq = 0;
+ let selectSeq = 0;
let refreshAllTimer = null;
let pendingRefreshTimer = null;
let pendingTtlSeconds = 600;
const POSITIONS_STALE_MS = 45000;
const PENDING_POLL_MS = 8000;
+ const orderPanelHome = (function () {
+ const host = document.getElementById("opt-order-panel-host");
+ return host ? host.parentElement : null;
+ })();
function fmt(v, d) {
if (v === null || v === undefined || Number.isNaN(Number(v))) return "—";
@@ -75,59 +80,71 @@
return document.getElementById("opt-order-panel-host");
}
+ function syncPickButtons(instId) {
+ document.querySelectorAll(".opt-pick-btn").forEach(function (btn) {
+ const on = !!instId && btn.getAttribute("data-inst") === instId;
+ btn.classList.toggle("active", on);
+ btn.disabled = false;
+ if (!btn.dataset.origText) btn.dataset.origText = "选择";
+ btn.textContent = on ? "已选" : btn.dataset.origText;
+ });
+ }
+
function parkOrderPanel() {
stopPendingOrdersPoll();
const panel = orderPanel();
const host = orderPanelHost();
- // 先把面板移回 host,再删行内 tr,避免 tbody.innerHTML 清空时把 #opt-order-panel 一起销毁
- if (panel && host) {
- if (panel.parentElement !== host) host.appendChild(panel);
- host.hidden = true;
- panel.style.display = "none";
+ // 把整块 host(含面板)移回原位,再删行内 tr,避免 tbody 重绘销毁下单 DOM
+ if (host && orderPanelHome && host.parentElement !== orderPanelHome) {
+ orderPanelHome.appendChild(host);
+ } else if (panel && host && panel.parentElement !== host) {
+ host.appendChild(panel);
}
+ if (host) host.hidden = true;
+ if (panel) panel.style.display = "none";
const inline = document.querySelector(".opt-order-inline-row");
if (inline) inline.remove();
document.querySelectorAll(".opt-strike-row").forEach(function (r) {
r.classList.remove("opt-row-selected");
});
- document.querySelectorAll(".opt-pick-btn").forEach(function (b) {
- b.classList.remove("active");
- b.disabled = false;
- if (b.dataset.origText) b.textContent = b.dataset.origText;
- });
+ syncPickButtons(null);
}
function placeOrderPanelAfter(instId) {
const panel = orderPanel();
- if (!panel || !instId) return;
+ const host = orderPanelHost();
+ if (!panel || !host || !instId) {
+ syncPickButtons(instId || null);
+ return false;
+ }
const row =
document.querySelector('#opt-strike-tbody tr.opt-strike-row[data-inst="' + CSS.escape(instId) + '"]') ||
document.querySelector('#opt-strike-tbody tr.opt-strike-row[data-call-inst="' + CSS.escape(instId) + '"]') ||
document.querySelector('#opt-strike-tbody tr.opt-strike-row[data-put-inst="' + CSS.escape(instId) + '"]');
- if (!row) return;
+ if (!row) {
+ syncPickButtons(null);
+ return false;
+ }
document.querySelectorAll(".opt-strike-row").forEach(function (r) {
r.classList.toggle("opt-row-selected", r === row);
});
- document.querySelectorAll(".opt-pick-btn").forEach(function (btn) {
- const on = btn.getAttribute("data-inst") === instId;
- btn.classList.toggle("active", on);
- if (!btn.dataset.origText) btn.dataset.origText = btn.textContent;
- if (on) btn.textContent = "已选";
- });
+ syncPickButtons(instId);
const oldInline = document.querySelector(".opt-order-inline-row");
if (oldInline) oldInline.remove();
+ if (panel.parentElement !== host) host.appendChild(panel);
const tr = document.createElement("tr");
tr.className = "opt-order-inline-row";
const td = document.createElement("td");
td.colSpan = strikeTableColspan();
- td.appendChild(panel);
+ td.appendChild(host);
tr.appendChild(td);
row.after(tr);
+ host.hidden = false;
panel.style.display = "";
- orderPanelHost().hidden = true;
tr.scrollIntoView({ behavior: "smooth", block: "nearest" });
refreshPendingOrders();
startPendingOrdersPoll();
+ return true;
}
function fmtPendingAge(sec) {
@@ -920,6 +937,7 @@
}
async function selectContract(instId, pickBtn, silent) {
+ const seq = ++selectSeq;
state.selectedInst = instId;
placeOrderPanelAfter(instId);
if (pickBtn) {
@@ -929,15 +947,14 @@
}
try {
const d = await apiJson(quoteUrl(instId));
+ if (seq !== selectSeq) return d;
fillOrderPanel(d);
return d;
} finally {
- if (pickBtn) {
- pickBtn.disabled = false;
- pickBtn.textContent = "已选";
- pickBtn.classList.add("active");
+ if (seq === selectSeq) {
+ syncPickButtons(instId);
+ if (!silent) placeOrderPanelAfter(instId);
}
- if (!silent) placeOrderPanelAfter(instId);
}
}
diff --git a/lib/exchange/okx_options_lib.py b/lib/exchange/okx_options_lib.py
index fdf053b..68f10a7 100644
--- a/lib/exchange/okx_options_lib.py
+++ b/lib/exchange/okx_options_lib.py
@@ -370,24 +370,62 @@ def normalize_option_exp_ms(exp_time: Any, inst_id: str = "") -> int | None:
return expiry_ms_from_inst_id(inst_id)
+def _is_okx_rate_limit(err: BaseException) -> bool:
+ text = str(err) or ""
+ name = err.__class__.__name__
+ return "50011" in text or "Too Many Requests" in text or "RateLimit" in name
+
+
+def _meta_from_inst_id_fallback(inst_id: str) -> dict[str, Any]:
+ """行情在但 instruments 限频时,用合约 ID 拼最小 meta,避免误报「合约不存在」."""
+ family = inst_family_from_inst_id(inst_id) or ""
+ opt_type, strike = option_fields_from_inst_id(inst_id)
+ uly = family.replace("_UM", "") if family else ""
+ return {
+ "instId": inst_id,
+ "instFamily": family,
+ "uly": uly,
+ "optType": opt_type,
+ "stk": strike,
+ "ctMult": 0.01,
+ "minSz": "1",
+ "tickSz": "0.0001",
+ "state": "live",
+ }
+
+
def fetch_option_instrument_meta(ex: ccxt.okx, inst_id: str) -> dict[str, Any] | None:
family = inst_family_from_inst_id(inst_id)
if not family:
return None
- 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 []
- for r in rows:
- if isinstance(r, dict) and str(r.get("instId")) == inst_id:
- return r
- except Exception:
- return None
+ last_err: BaseException | None = None
+ for attempt in range(3):
+ 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 []
+ 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))
+ continue
+ break
+ if last_err is not None and _is_okx_rate_limit(last_err):
+ try:
+ t_rows = ex.public_get_market_ticker({"instId": inst_id}).get("data") or []
+ if t_rows:
+ return _meta_from_inst_id_fallback(inst_id)
+ except Exception:
+ pass
return None
@@ -747,9 +785,25 @@ def quote_option_contract(ex: ccxt.okx, inst_id: str) -> dict[str, Any]:
return {"ok": False, "msg": "缺少 inst_id"}
try:
meta = fetch_option_instrument_meta(ex, inst_id)
+ t_rows: list[Any] = []
+ ticker_err: BaseException | None = None
+ for attempt in range(3):
+ try:
+ t_rows = ex.public_get_market_ticker({"instId": inst_id}).get("data") or []
+ ticker_err = None
+ break
+ except Exception as e:
+ ticker_err = e
+ if _is_okx_rate_limit(e) and attempt < 2:
+ time.sleep(0.45 * (attempt + 1))
+ continue
+ break
+ if not meta and t_rows:
+ meta = _meta_from_inst_id_fallback(inst_id)
if not meta:
+ if ticker_err is not None and _is_okx_rate_limit(ticker_err):
+ return {"ok": False, "msg": "行情限频,请稍后重试"}
return {"ok": False, "msg": "合约不存在"}
- t_rows = ex.public_get_market_ticker({"instId": inst_id}).get("data") or []
t = t_rows[0] if t_rows else {}
ask = _safe_float(t.get("askPx"))
bid = _safe_float(t.get("bidPx"))
diff --git a/lib/options/templates/options_panel.html b/lib/options/templates/options_panel.html
index 7a32bfc..18de9d2 100644
--- a/lib/options/templates/options_panel.html
+++ b/lib/options/templates/options_panel.html
@@ -273,4 +273,4 @@
-
+