fix(hub): restore pre-WS quote path and relieve Gate/account contention
Two audit rounds after WS rollback: lighten hub options snapshot, cache hub balances without extra fetch_balance, soft-poll single-flight, and document fixes in R1/R2 reports. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -40,6 +40,7 @@
|
||||
let chainSoftTimer = null;
|
||||
let lastChainSoftAt = 0;
|
||||
let chainQuotedAt = 0;
|
||||
let chainLoadInFlight = false;
|
||||
let pendingTtlSeconds = 600;
|
||||
const POSITIONS_STALE_MS = 45000;
|
||||
const PENDING_POLL_MS = 8000;
|
||||
@@ -669,6 +670,7 @@
|
||||
function softRefreshChainThrottled(force) {
|
||||
if (document.hidden) return;
|
||||
if (!document.getElementById("options-root")) return;
|
||||
if (chainLoadInFlight) return;
|
||||
const now = Date.now();
|
||||
if (!force && now - lastChainSoftAt < CHAIN_SOFT_POLL_MS) return;
|
||||
lastChainSoftAt = now;
|
||||
@@ -1239,11 +1241,16 @@
|
||||
|
||||
async function loadChain(opts) {
|
||||
const soft = !!(opts && opts.soft);
|
||||
// soft 门禁必须在 seq++ 之前,否则叠刷会抬高 seq 导致 inFlight 永不清理
|
||||
if (chainLoadInFlight && soft) return;
|
||||
const uly = state.underlying;
|
||||
const seq = ++chainLoadSeq;
|
||||
const btn = document.getElementById("opt-load-chain");
|
||||
const hadChain = chainHasExpiries(state.chain) && state.chain.underlying === uly;
|
||||
chainLoadInFlight = true;
|
||||
if (btn && !soft) btn.disabled = true;
|
||||
if (!soft) {
|
||||
// 已有链时不先清空,避免刷新白屏
|
||||
if (!soft && !hadChain) {
|
||||
setExpirySelectStatus("加载到期日中…");
|
||||
const tbody = document.getElementById("opt-strike-tbody");
|
||||
if (tbody) {
|
||||
@@ -1254,7 +1261,9 @@
|
||||
try {
|
||||
let d = null;
|
||||
let lastMsg = "";
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
// soft 只试 1 次,避免与 15s 轮询叠加重试打爆 OKX
|
||||
const maxAttempts = soft ? 1 : 3;
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||
if (seq !== chainLoadSeq) return;
|
||||
d = await apiJson("/api/options/chain?underlying=" + encodeURIComponent(uly));
|
||||
if (seq !== chainLoadSeq) return;
|
||||
@@ -1264,8 +1273,8 @@
|
||||
!!(d && d.rate_limited) ||
|
||||
/50011|Too Many Requests|过于频繁/i.test(String(lastMsg || ""));
|
||||
d = null;
|
||||
if (attempt < 2) {
|
||||
if (!soft) {
|
||||
if (attempt < maxAttempts - 1) {
|
||||
if (!soft && !hadChain) {
|
||||
setExpirySelectStatus(
|
||||
rateLimited ? "OKX 限频,稍后重试…" : "重试加载到期日…"
|
||||
);
|
||||
@@ -1339,7 +1348,10 @@
|
||||
"</td></tr>";
|
||||
}
|
||||
} finally {
|
||||
if (seq === chainLoadSeq && btn) btn.disabled = false;
|
||||
if (seq === chainLoadSeq) {
|
||||
chainLoadInFlight = false;
|
||||
if (btn) btn.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,9 @@ _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 = float(os.getenv("OKX_OPTIONS_TICKERS_CACHE_SEC", "10") or "10")
|
||||
|
||||
|
||||
def invalidate_options_balance_cache() -> None:
|
||||
@@ -712,17 +715,40 @@ def fetch_option_instruments(
|
||||
return []
|
||||
|
||||
|
||||
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,
|
||||
*,
|
||||
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) < max(1.0, _TICKERS_CACHE_TTL_SEC)
|
||||
and isinstance(cached.get("rows"), dict)
|
||||
and cached["rows"]
|
||||
):
|
||||
return dict(cached["rows"])
|
||||
|
||||
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": inst_family}
|
||||
{"instType": "OPTION", "instFamily": 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
|
||||
@@ -730,6 +756,8 @@ def fetch_option_tickers(ex: ccxt.okx, inst_family: str) -> dict[str, dict[str,
|
||||
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
|
||||
|
||||
@@ -22,7 +22,8 @@ def build_options_hub_snapshot(cfg: dict[str, Any]) -> dict[str, Any]:
|
||||
raw = cfg["fetch_option_positions"](ex)
|
||||
if raw is None:
|
||||
return {"ok": False, "enabled": True, "msg": "获取期权持仓失败"}
|
||||
positions = build_display_option_positions(cfg, ex, raw)
|
||||
# 中控看板不拉逐仓 books(易超 HUB_FLASK_TIMEOUT);实例页仍走完整 preview
|
||||
positions = build_display_option_positions(cfg, ex, raw, with_close_preview=False)
|
||||
target_monitors: list[dict[str, Any]] = []
|
||||
try:
|
||||
conn = cfg["get_db"]()
|
||||
|
||||
@@ -145,8 +145,10 @@ def build_display_option_positions(
|
||||
cfg: dict[str, Any],
|
||||
ex: Any,
|
||||
raw_positions: list[dict[str, Any]],
|
||||
*,
|
||||
with_close_preview: bool = True,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""与实例 /api/options/positions 相同 enrichment + close_preview."""
|
||||
"""与实例 /api/options/positions 相同 enrichment;中控可关 close_preview 避免逐仓拉盘口超时."""
|
||||
meta_cache: dict[str, dict[str, Any] | None] = {}
|
||||
rows: list[dict[str, Any]] = []
|
||||
conn = cfg["get_db"]()
|
||||
@@ -162,7 +164,8 @@ def build_display_option_positions(
|
||||
meta_cache=meta_cache,
|
||||
premium_override=premium_override,
|
||||
)
|
||||
attach_close_preview(cfg, ex, row, premium_paid=_safe_float(row.get("premium_paid")))
|
||||
if with_close_preview:
|
||||
attach_close_preview(cfg, ex, row, premium_paid=_safe_float(row.get("premium_paid")))
|
||||
rows.append(row)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@@ -322,4 +322,4 @@
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/options_expiry_countdown.js?v=1"></script>
|
||||
<script src="/static/options_panel.js?v=57"></script>
|
||||
<script src="/static/options_panel.js?v=60"></script>
|
||||
|
||||
Reference in New Issue
Block a user