From 2ad9e049f3411e57b20a17444abbb411aa010963 Mon Sep 17 00:00:00 2001 From: dekun Date: Tue, 7 Jul 2026 11:15:06 +0800 Subject: [PATCH] fix(okx): cache options balances and stop SSE from hammering exchange API Co-authored-by: Cursor --- crypto_monitor_okx/app.py | 28 ++++------ lib/common/static/instance_embed.js | 13 +++-- lib/common/static/instance_live.js | 14 ++++- lib/exchange/okx_options_lib.py | 52 ++++++++++++++++--- .../templates/embed_boot_scripts.html | 8 +-- lib/instance/templates/embed_shell.html | 4 +- 6 files changed, 82 insertions(+), 37 deletions(-) diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index 13430f4..426fea2 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -6516,15 +6516,11 @@ def render_main_page(page="trade", embed_mode=None): and embed_mode != "fragment" ): try: - from lib.exchange.okx_options_lib import ( - fetch_options_funding_usdc, - fetch_options_funding_usdt, - fetch_options_trading_usdc, - ) + from lib.exchange.okx_options_lib import options_header_balances - options_trading_usdc = fetch_options_trading_usdc(exchange_options) - options_funding_usdc = fetch_options_funding_usdc(exchange_options) - options_funding_usdt = fetch_options_funding_usdt(exchange_options) + options_trading_usdc, options_funding_usdc, options_funding_usdt = options_header_balances( + exchange_options + ) except Exception: options_trading_usdc = None options_funding_usdc = None @@ -6838,7 +6834,8 @@ def api_account_snapshot(): conn = get_db() session_row = ensure_session(conn, trading_day) local_current_capital = float(session_row["current_capital"]) - funding_capital, trading_capital = get_exchange_capitals(force=True) + force_refresh = (request.args.get("force") or "").strip().lower() in ("1", "true", "yes") + funding_capital, trading_capital = get_exchange_capitals(force=force_refresh) funding_usdt = round(funding_capital, FUNDS_DECIMALS) if funding_capital is not None else None current_capital = round(trading_capital, FUNDS_DECIMALS) if trading_capital is not None else round(local_current_capital, FUNDS_DECIMALS) options_trading_usdc = None @@ -6846,15 +6843,12 @@ def api_account_snapshot(): options_funding_usdt = None if OKX_OPTIONS_ENABLED and exchange_options.apiKey: try: - from lib.exchange.okx_options_lib import ( - fetch_options_funding_usdc, - fetch_options_funding_usdt, - fetch_options_trading_usdc, - ) + from lib.exchange.okx_options_lib import options_header_balances - options_trading_usdc = fetch_options_trading_usdc(exchange_options) - options_funding_usdc = fetch_options_funding_usdc(exchange_options) - options_funding_usdt = fetch_options_funding_usdt(exchange_options) + options_trading_usdc, options_funding_usdc, options_funding_usdt = options_header_balances( + exchange_options, + force=force_refresh, + ) except Exception: options_trading_usdc = None options_funding_usdc = None diff --git a/lib/common/static/instance_embed.js b/lib/common/static/instance_embed.js index 4e64f1f..8cbfe78 100644 --- a/lib/common/static/instance_embed.js +++ b/lib/common/static/instance_embed.js @@ -206,11 +206,14 @@ function preloadAllTabs() { const tabs = Object.keys(TAB_PATH); const current = getTab(); + const heavyLast = new Set(["options", "records", "stats"]); + const ordered = tabs.filter((t) => t !== current && !heavyLast.has(t)) + .concat(tabs.filter((t) => heavyLast.has(t) && t !== current)); let idx = 0; function step() { - if (idx >= tabs.length) return; - const tab = tabs[idx++]; - if (tab === current || tabPanes.has(tab)) { + if (idx >= ordered.length) return; + const tab = ordered[idx++]; + if (tabPanes.has(tab)) { step(); return; } @@ -220,11 +223,11 @@ }) .catch(() => {}) .finally(() => { - setTimeout(step, 120); + setTimeout(step, heavyLast.has(tab) ? 400 : 180); }); } const ric = global.requestIdleCallback || function (fn) { - setTimeout(fn, 1200); + setTimeout(fn, 2000); }; ric(step); } diff --git a/lib/common/static/instance_live.js b/lib/common/static/instance_live.js index 78a2aba..05bdace 100644 --- a/lib/common/static/instance_live.js +++ b/lib/common/static/instance_live.js @@ -6,6 +6,7 @@ let liveReconnectTimer = null; let localLiveVersion = -1; let sseConnected = false; + let refreshTimer = null; function isEmbedShell() { return document.body && document.body.getAttribute("data-embed-shell") === "1"; @@ -21,7 +22,7 @@ function refreshTabData(tab, opts) { const options = opts || {}; if (typeof global.refreshAccountSnapshot === "function") { - global.refreshAccountSnapshot(); + global.refreshAccountSnapshot(options); } if (typeof global.refreshPriceSnapshotConditional === "function") { global.refreshPriceSnapshotConditional(); @@ -31,6 +32,15 @@ } } + function scheduleRefresh() { + if (refreshTimer) return; + refreshTimer = setTimeout(function () { + refreshTimer = null; + if (document.hidden) return; + refreshTabData(currentTab(), { silent: true }); + }, 80); + } + function onLiveEvent(data) { const reason = data && data.reason; const ver = Number(data && data.live_version) || 0; @@ -41,7 +51,7 @@ } if (ver === localLiveVersion) return; localLiveVersion = ver; - refreshTabData(currentTab(), { silent: true }); + scheduleRefresh(); } function closeLiveStream() { diff --git a/lib/exchange/okx_options_lib.py b/lib/exchange/okx_options_lib.py index 1c6eb2d..60cdb95 100644 --- a/lib/exchange/okx_options_lib.py +++ b/lib/exchange/okx_options_lib.py @@ -16,6 +16,8 @@ _OKX_OPTION_ERR_ZH: dict[str, str] = { "51019": "期权买入须使用逐仓模式(全仓模式下不能持有多头净头寸)", } +_OPTIONS_BALANCE_CACHE: dict[str, Any] = {"updated_at": 0.0, "data": None} + def _okx_trade_error_message(exc: BaseException | None = None, resp: Any = None) -> str: row: dict[str, Any] | None = None @@ -199,7 +201,15 @@ def fetch_account_balances_by_type(ex: ccxt.okx, account_type: str) -> dict[str, return out -def fetch_options_balances(ex: ccxt.okx) -> dict[str, Any]: +def fetch_options_balances(ex: ccxt.okx, *, force: bool = False) -> dict[str, Any]: + import os + + ttl = float(os.getenv("OKX_OPTIONS_BALANCE_REFRESH_SEC", "30")) + now = time.time() + cached = _OPTIONS_BALANCE_CACHE.get("data") + if not force and cached is not None and now - float(_OPTIONS_BALANCE_CACHE.get("updated_at") or 0) < ttl: + return dict(cached) + funding = fetch_account_balances_by_type(ex, "funding") trading = fetch_account_balances_by_type(ex, "trading") # 统一账户部分 USDC 可能在 swap 类型 @@ -207,7 +217,7 @@ def fetch_options_balances(ex: ccxt.okx) -> dict[str, Any]: swap_bal = fetch_account_balances_by_type(ex, "swap") if swap_bal.get("USDC") is not None: trading["USDC"] = swap_bal["USDC"] - return { + result = { "funding_usdt": funding.get("USDT"), "funding_usdc": funding.get("USDC"), "funding_usdg": funding.get("USDG"), @@ -215,6 +225,32 @@ def fetch_options_balances(ex: ccxt.okx) -> dict[str, Any]: "trading_usdc": trading.get("USDC"), "trading_usdg": trading.get("USDG"), } + _OPTIONS_BALANCE_CACHE["updated_at"] = now + _OPTIONS_BALANCE_CACHE["data"] = result + return result + + +def options_header_balances( + ex: ccxt.okx, + *, + force: bool = False, +) -> tuple[float | None, float | None, float | None]: + """顶栏三格:交易 USDC、资金 USDC、资金 USDT(单次拉取 + 缓存)。""" + bal = fetch_options_balances(ex, force=force) + + def _round(v: Any) -> float | None: + if v is None: + return None + try: + return round(float(v), 2) + except (TypeError, ValueError): + return None + + return ( + _round(bal.get("trading_usdc")), + _round(bal.get("funding_usdc")), + _round(bal.get("funding_usdt")), + ) def fetch_index_price(ex: ccxt.okx, uly: str) -> float | None: @@ -525,24 +561,24 @@ def transfer_ccy( _OKX_ACCT_CODE = {"funding": "6", "trading": "18", "spot": "18"} -def fetch_options_trading_usdc(ex: ccxt.okx) -> float | None: - bal = fetch_options_balances(ex) +def fetch_options_trading_usdc(ex: ccxt.okx, *, force: bool = False) -> float | None: + bal = fetch_options_balances(ex, force=force) v = bal.get("trading_usdc") if v is None: return None return round(float(v), 2) -def fetch_options_funding_usdc(ex: ccxt.okx) -> float | None: - bal = fetch_options_balances(ex) +def fetch_options_funding_usdc(ex: ccxt.okx, *, force: bool = False) -> float | None: + bal = fetch_options_balances(ex, force=force) v = bal.get("funding_usdc") if v is None: return None return round(float(v), 2) -def fetch_options_funding_usdt(ex: ccxt.okx) -> float | None: - bal = fetch_options_balances(ex) +def fetch_options_funding_usdt(ex: ccxt.okx, *, force: bool = False) -> float | None: + bal = fetch_options_balances(ex, force=force) v = bal.get("funding_usdt") if v is None: return None diff --git a/lib/instance/templates/embed_boot_scripts.html b/lib/instance/templates/embed_boot_scripts.html index a340676..e2dc8cc 100644 --- a/lib/instance/templates/embed_boot_scripts.html +++ b/lib/instance/templates/embed_boot_scripts.html @@ -1054,8 +1054,10 @@ function formatOptionsFundingLabel(usdc, usdt) { return parts.length ? parts.join(" · ") : "—"; } -function refreshAccountSnapshot(){ - fetch("/api/account_snapshot").then(r=>r.json()).then(data=>{ +function refreshAccountSnapshot(opts){ + const options = opts || {}; + const qs = options.force ? "?force=1" : ""; + fetch("/api/account_snapshot" + qs).then(r=>r.json()).then(data=>{ if (typeof data.funding_usdt !== "undefined") { const el = document.getElementById("total-capital"); if(el) el.innerText = (data.funding_usdt === null || data.funding_usdt === undefined) ? "—" : `${Number(data.funding_usdt).toFixed(2)}U`; @@ -1186,7 +1188,7 @@ if(window.ManualOrderRrPreview){ refreshAccountSnapshot(); const settingsRefreshFunds = document.getElementById("settings-refresh-funds"); -if (settingsRefreshFunds) settingsRefreshFunds.addEventListener("click", refreshAccountSnapshot); +if (settingsRefreshFunds) settingsRefreshFunds.addEventListener("click", function(){ refreshAccountSnapshot({ force: true }); }); if (window.AccountRiskBadge) AccountRiskBadge.startTicker(); const _journalFormEl = document.getElementById("journal-form"); if(_journalFormEl){ diff --git a/lib/instance/templates/embed_shell.html b/lib/instance/templates/embed_shell.html index bb45d6b..02af0fd 100644 --- a/lib/instance/templates/embed_shell.html +++ b/lib/instance/templates/embed_shell.html @@ -90,7 +90,7 @@ const ORDER_ENTRY_MODEL_CODE_TO_CATEGORY = {{ entry_model_code_to_category | toj {% include 'embed_boot_scripts.html' %} - - + +