From bab42b1b532c8b1f03a71adb032c9223dcd056ce Mon Sep 17 00:00:00 2001 From: dekun Date: Tue, 11 Aug 2026 12:30:27 +0800 Subject: [PATCH] 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 --- crypto_monitor_binance/app.py | 6 +-- crypto_monitor_gate/app.py | 23 ++++++-- crypto_monitor_okx/app.py | 6 +-- ...®¡ä¿®å¤æŠ¥å‘Š-WS回滚与中控å¯ç”¨-2026-08-11-R1.md | 53 +++++++++++++++++++ ...®¡ä¿®å¤æŠ¥å‘Š-WS回滚与中控å¯ç”¨-2026-08-11-R2.md | 41 ++++++++++++++ lib/common/static/options_panel.js | 22 ++++++-- lib/exchange/okx_options_lib.py | 32 ++++++++++- lib/options/options_hub_lib.py | 3 +- lib/options/options_positions_lib.py | 7 ++- lib/options/templates/options_panel.html | 2 +- manual_trading_hub/hub.py | 29 +++++++--- manual_trading_hub/static/app.js | 24 +++++---- 12 files changed, 209 insertions(+), 39 deletions(-) create mode 100644 docs/å®¡è®¡ä¿®å¤æŠ¥å‘Š-WS回滚与中控å¯ç”¨-2026-08-11-R1.md create mode 100644 docs/å®¡è®¡ä¿®å¤æŠ¥å‘Š-WS回滚与中控å¯ç”¨-2026-08-11-R2.md diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index fb1da1f..8fe87cc 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -10042,14 +10042,14 @@ def _hub_meta_bundle(): def _hub_account_bundle(): - funding_capital, trading_capital = get_exchange_capitals(force=True) + # 中控看æ¿é«˜é¢‘拉å–:ä»…èµ°ä½™é¢ç¼“å­˜,é¿å…é¢å¤– fetch_balance + funding_capital, trading_capital = get_exchange_capitals(force=False) funding_usdt = round(funding_capital, FUNDS_DECIMALS) if funding_capital is not None else None trading_usdt = round(trading_capital, FUNDS_DECIMALS) if trading_capital is not None else None - available = get_available_trading_usdt() return { "funding_usdt": funding_usdt, "trading_usdt": trading_usdt, - "available_trading_usdt": round(available, FUNDS_DECIMALS) if available is not None else None, + "available_trading_usdt": trading_usdt, "trading_day": get_trading_day(app_now()), } diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index 6c611ed..a37848c 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -467,6 +467,8 @@ from lib.exchange.gate_ccxt_lib import gate_ccxt_class # Gate.io USDT 永续(swap) exchange = gate_ccxt_class()({ "enableRateLimit": True, + # é¿å…关键ä½ç›‘控/è´¦æˆ·æ‹‰å–æ— é™æŒ‚起拖垮中控 + "timeout": int(os.getenv("GATE_CCXT_TIMEOUT_MS", "8000")), "options": { "defaultType": "swap", "defaultMarginMode": _GATE_DEFAULT_MARGIN_MODE, @@ -4611,14 +4613,25 @@ def _finalize_key_monitor_one_shot(conn, row, last_msg, close_reason): conn.execute("DELETE FROM key_monitors WHERE id=?", (row["id"],)) +_RS_BAR_CACHE: dict[str, dict] = {} +_RS_BAR_CACHE_TTL_SEC = float(os.getenv("GATE_RS_BAR_CACHE_SEC", "45")) + + def _fetch_last_closed_bar(symbol): - """æœ€è¿‘ä¸€æ ¹é—­åˆ K:[ts, o, h, l, c, v] 或 None.""" + """æœ€è¿‘ä¸€æ ¹é—­åˆ K:[ts, o, h, l, c, v] 或 None.短缓存å‡è½»å…³é”®ä½ç›‘控打爆 ccxt.""" ex_sym = normalize_exchange_symbol(symbol) + now = time.time() + cached = _RS_BAR_CACHE.get(ex_sym) + if cached and now - float(cached.get("updated_at") or 0) < _RS_BAR_CACHE_TTL_SEC: + return cached.get("bar") bars = exchange.fetch_ohlcv(ex_sym, timeframe=KLINE_TIMEFRAME, limit=5) or [] if len(bars) < 2: + _RS_BAR_CACHE[ex_sym] = {"updated_at": now, "bar": None} return None closed = bars[:-1] - return closed[-1] if closed else None + bar = closed[-1] if closed else None + _RS_BAR_CACHE[ex_sym] = {"updated_at": now, "bar": bar} + return bar def _key_rs_gate_preview(symbol, upper, lower): @@ -9893,14 +9906,14 @@ def _hub_meta_bundle(): def _hub_account_bundle(): - funding_capital, trading_capital = get_exchange_capitals(force=True) + # 中控看æ¿é«˜é¢‘拉å–:ä»…èµ°ä½™é¢ç¼“å­˜;ä¸å†é¢å¤– fetch_balance(会与关键ä½ç›‘控争用 ccxt) + funding_capital, trading_capital = get_exchange_capitals(force=False) funding_usdt = round(funding_capital, 2) if funding_capital is not None else None trading_usdt = round(trading_capital, 2) if trading_capital is not None else None - available = get_available_trading_usdt() return { "funding_usdt": funding_usdt, "trading_usdt": trading_usdt, - "available_trading_usdt": round(available, 2) if available is not None else None, + "available_trading_usdt": trading_usdt, "trading_day": get_trading_day(app_now()), } diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index 7a609be..c7be6b0 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -9665,14 +9665,14 @@ def _hub_meta_bundle(): def _hub_account_bundle(): - funding_capital, trading_capital = get_exchange_capitals(force=True) + # 中控看æ¿é«˜é¢‘拉å–:ä»…èµ°ä½™é¢ç¼“å­˜,é¿å…é¢å¤– fetch_balance + funding_capital, trading_capital = get_exchange_capitals(force=False) funding_usdt = round(funding_capital, FUNDS_DECIMALS) if funding_capital is not None else None trading_usdt = round(trading_capital, FUNDS_DECIMALS) if trading_capital is not None else None - available = get_available_trading_usdt() return { "funding_usdt": funding_usdt, "trading_usdt": trading_usdt, - "available_trading_usdt": round(available, FUNDS_DECIMALS) if available is not None else None, + "available_trading_usdt": trading_usdt, "trading_day": get_trading_day(app_now()), } diff --git a/docs/å®¡è®¡ä¿®å¤æŠ¥å‘Š-WS回滚与中控å¯ç”¨-2026-08-11-R1.md b/docs/å®¡è®¡ä¿®å¤æŠ¥å‘Š-WS回滚与中控å¯ç”¨-2026-08-11-R1.md new file mode 100644 index 0000000..bfa2a1e --- /dev/null +++ b/docs/å®¡è®¡ä¿®å¤æŠ¥å‘Š-WS回滚与中控å¯ç”¨-2026-08-11-R1.md @@ -0,0 +1,53 @@ +# å®¡è®¡ä¿®å¤æŠ¥å‘Š · WS 回滚与中控å¯ç”¨æ€§(2026-08-11 · 第 1 è½®) + +## 背景 + +期æƒé“¾æŽ¥å…¥ OKX WS 推é€åŽ,ç”Ÿäº§ä¸­æŽ§å‡ºçŽ°ã€ŒæœŸæƒæ•°æ®ä¸å¯ç”¨ / å­ä»£ç†ä¸å¯ç”¨ã€ã€‚æŒ‰è¦æ±‚**先回滚 WS 链路**,å†å…¨é‡å®¡è®¡å¹¶ä¿®å¤ã€‚ + +## 回滚 + +| æäº¤ | 说明 | +|------|------| +| `a488e2f` | Revert fast-path(ä¾èµ– WS 热缓存) | +| `d592632` | Revert OKX WS + SSE æŽ¨é€æ•´æ ˆ | + +æ¢å¤ä¸º **REST 拉链 + å‰ç«¯çº¦ 15s soft-poll**(commit `24bb853` 行为),删除: + +- `lib/exchange/okx_public_ws_lib.py` +- `lib/options/options_quote_live_lib.py` +- `tests/test_options_quote_live_lib.py` + +## 根因结论(éžä»… WS) + +| 级别 | 问题 | è¯æ® | +|------|------|------| +| Critical | Gate å…³é”®ä½ RS 监控在åŽå°çº¿ç¨‹é«˜é¢‘ `fetch_ohlcv`,与 `/api/hub/account` 争用åŒä¸€ ccxt 客户端,账户/å­ä»£ç†è¶…æ—¶ | 日志 `[key_rs_level_alert] BTC/USDT id=13`;本机 `5000/api/hub/account` 25s è¶…æ—¶ | +| Critical | 中控期æƒå¿«ç…§å¯¹æ¯ä»“拉 books 深度,易超 `HUB_FLASK_TIMEOUT=10` | `build_display_option_positions` → `attach_close_preview` → `fetch_option_book_depth` | +| High | Soft 拉链 3 次é‡è¯• + æ— å•飞,易与 SSE tick å æ‰“ OKX | `options_panel.js` loadChain | +| High | ä¸­æŽ§è´¦æˆ·æŽ¥å£æ¯è½® `force=True` 绕过余é¢ç¼“å­˜ | Gate/OKX/Binance `_hub_account_bundle` | +| Medium | Flask è¶…æ—¶é”™è¯¯åªæœ‰ `error` æ—  `msg`,å‰ç«¯æ˜“è½é»˜è®¤æ–‡æ¡ˆ | `hub.py` `_fetch_flask_json` | +| Medium | `options` 为 null æ—¶å‰ç«¯å½“æˆã€Œ0 仓ã€è€Œéžä¸å¯ç”¨ | `app.js` renderOptionsSectionBody | + +WS 部署触å‘çš„**全进程é‡å¯**放大了 Gate 争用与快照超时,表现为「全ä¸å¯ç”¨ã€;OKX 快照在轻负载下ä»å¯ `ok:true`。 + +## æœ¬è½®ä¿®å¤ + +1. **Hub 期æƒå¿«ç…§**关闭é€ä»“ `close_preview`/books(`with_close_preview=False`) +2. **Hub 账户**三所改为 `get_exchange_capitals(force=False)` +3. **Gate ccxt** 增加 `timeout=8000ms`;RS K 线 **45s 缓存** +4. **æœŸæƒ tickers** æ¢å¤ **10s** 短缓存(æ—  WS) +5. **å‰ç«¯ soft 拉链**:å•飞 + soft ä»… 1 次å°è¯•;已有链ä¸å…ˆæ¸…空表格 +6. **Hub**:è¶…æ—¶è¡¥ `msg`;期æƒå¿«ç…§ä¸Ž account/monitor **并行 gather** +7. **中控 UI**:capabilities å« options 且 snapshot 缺失时显å¼ã€ŒæœŸæƒæ•°æ®ä¸å¯ç”¨ã€ + +## 测试建议 + +- 强刷中控监控区:OKX 期æƒèµ„金/æŒä»“应æ¢å¤;Gate å­ä»£ç† status 应在数秒内æ¢å¤ +- 期æƒé¡µã€Œåˆ·æ–°é“¾ã€ä¸åº”长时间白å±;指数行显示约 15s é™é»˜åˆ·æ–° +- Gate 关键ä½ç›‘控日志ä¸åº”冿¯ç§’åˆ·å± `fetch_ohlcv` 失败 + +## 残留风险(交第 2 è½®) + +- Gate ä»ä¸Žç›‘控共用å•一 ccxt 客户端(未加全局é”) +- 中控 board ä»å¯èƒ½è¢«æœ€æ…¢äº¤æ˜“所拉长整轮等待 +- Soft-poll 仿˜¯ REST,éžçœŸÂ·å®žæ—¶ diff --git a/docs/å®¡è®¡ä¿®å¤æŠ¥å‘Š-WS回滚与中控å¯ç”¨-2026-08-11-R2.md b/docs/å®¡è®¡ä¿®å¤æŠ¥å‘Š-WS回滚与中控å¯ç”¨-2026-08-11-R2.md new file mode 100644 index 0000000..da1f87f --- /dev/null +++ b/docs/å®¡è®¡ä¿®å¤æŠ¥å‘Š-WS回滚与中控å¯ç”¨-2026-08-11-R2.md @@ -0,0 +1,41 @@ +# å®¡è®¡ä¿®å¤æŠ¥å‘Š · WS 回滚与中控å¯ç”¨æ€§(2026-08-11 · 第 2 è½®) + +## 范围 + +夿Ÿ¥ç¬¬ 1 è½®ä¿®å¤æ˜¯å¦å¼•入回归,并扫清ä»ä¼šå¯¼è‡´ã€Œä¸­æŽ§ä¸å¯ç”¨ã€çš„æ®‹ç•™é«˜ä¼˜å…ˆçº§é—®é¢˜ã€‚ + +## 夿Ÿ¥ç»“论 + +| 项 | 结论 | +|----|------| +| Hub 并行 options ç´¢å¼•è¿›ä½ | 正确(day / options ç»„åˆæ— é”™ä½) | +| Hub 关闭 close_preview | UI é™çº§ä¸º upl/`—`,ä¸å´© | +| Gate RS 缓存 / timeout | timeout 已为 int;ç¼“å­˜å¯æŽ¥å— | +| board row capabilities | `_fetch_agent_status` 始终带上 | +| `with_close_preview` 默认 | 实例路径ä»ä¸º True | + +## 本轮新å‘çŽ°é—®é¢˜ä¸Žä¿®å¤ + +| 级别 | 问题 | ä¿®å¤ | +|------|------|------| +| High | Hub 账户在 `force=False` åŽä»è°ƒç”¨ `get_available_trading_usdt()` 冿‰“一枪 `fetch_balance`,Gate äº‰ç”¨ä¾æ—§ | 三所 `_hub_account_bundle` 改为用缓存的 `trading_usdt` 作为 `available_trading_usdt` | +| Medium | `loadChain` soft é—¨ç¦åœ¨ `seq++` 之åŽ,å åˆ·å¯å¯¼è‡´ `chainLoadInFlight` æ°¸ä¸æ¸…ç† | soft é—¨ç¦ç§»åˆ° `seq++` ä¹‹å‰ | + +## 与第 1 è½®ä¸€å¹¶äº¤ä»˜çš„çŠ¶æ€ + +- WS 推é€é“¾è·¯å·²å›žæ»š(REST + 15s soft-poll) +- 中控期æƒå¿«ç…§è½»é‡åŒ– + å¹¶è¡Œæ‹‰å– +- Gate RS K 线短缓存 + ccxt timeout +- æœŸæƒ tickers 10s 缓存;soft å•飞/啿¬¡å°è¯• + +## 已知残留(ä¸é˜»å¡žæœ¬æ¬¡éƒ¨ç½²) + +- Gate 监控与账户ä»å…±ç”¨å•一 ccxt 客户端(无全局é”) +- 中控 board ä»å¯èƒ½è¢«æœ€æ…¢äº¤æ˜“所拉长整轮 +- Soft-poll éžçœŸÂ·å®žæ—¶æŠ¥ä»· + +## 部署åŽéªŒæ”¶ + +1. 中控强刷:OKX 期æƒåŒºæœ‰èµ„金数字,ä¸å†é•¿æœŸã€ŒæœŸæƒæ•°æ®ä¸å¯ç”¨ã€ +2. Gate å¡:å­ä»£ç†æ¢å¤ç»¿è‰²/有资金;ä¸å†é•¿æ—¶é—´ã€Œå­ä»£ç†ä¸å¯ç”¨ã€ +3. 期æƒé¡µåˆ·æ–°é“¾ä¸ç™½å±;约 15s é™é»˜æ›´æ–°æ—¶é—´æˆ³ diff --git a/lib/common/static/options_panel.js b/lib/common/static/options_panel.js index b8504d3..7085b12 100644 --- a/lib/common/static/options_panel.js +++ b/lib/common/static/options_panel.js @@ -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 @@ ""; } } finally { - if (seq === chainLoadSeq && btn) btn.disabled = false; + if (seq === chainLoadSeq) { + chainLoadInFlight = false; + if (btn) btn.disabled = false; + } } } diff --git a/lib/exchange/okx_options_lib.py b/lib/exchange/okx_options_lib.py index 4fb4e15..e961f1b 100644 --- a/lib/exchange/okx_options_lib.py +++ b/lib/exchange/okx_options_lib.py @@ -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 diff --git a/lib/options/options_hub_lib.py b/lib/options/options_hub_lib.py index 604a185..4053c8a 100644 --- a/lib/options/options_hub_lib.py +++ b/lib/options/options_hub_lib.py @@ -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"]() diff --git a/lib/options/options_positions_lib.py b/lib/options/options_positions_lib.py index a917f2c..2367021 100644 --- a/lib/options/options_positions_lib.py +++ b/lib/options/options_positions_lib.py @@ -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() diff --git a/lib/options/templates/options_panel.html b/lib/options/templates/options_panel.html index 7ba1c7b..9fe468f 100644 --- a/lib/options/templates/options_panel.html +++ b/lib/options/templates/options_panel.html @@ -322,4 +322,4 @@ - + diff --git a/manual_trading_hub/hub.py b/manual_trading_hub/hub.py index 46cbe6f..25bc494 100644 --- a/manual_trading_hub/hub.py +++ b/manual_trading_hub/hub.py @@ -2028,7 +2028,8 @@ async def _fetch_flask_json( return parsed return _parse_http_json_body(r) except Exception as e: - return {"ok": False, "error": str(e)} + err = str(e) + return {"ok": False, "error": err, "msg": err} async def _notify_instance_user_close( @@ -2567,8 +2568,8 @@ def _merge_flask_exchange_tpsl(agent_row: dict, snap: dict | None, hub_mon: dict async def _fetch_exchange_flask_bundle( client: httpx.AsyncClient, ex: dict, *, trading_day: str | None = None -) -> tuple[dict | None, dict | None, list | None, dict | None, dict | None, dict | None]: - """啿‰€ Flask:monitor / meta / price_snapshot / account / trades/today(有 flask_url æ—¶)并行拉å–.""" +) -> tuple: + """啿‰€ Flask:monitor / meta / price_snapshot / account / trades/today / options 并行拉å–.""" caps = ex.get("capabilities") or [] tasks = [ _fetch_flask_json(client, ex, "/api/hub/monitor"), @@ -2576,6 +2577,7 @@ async def _fetch_exchange_flask_bundle( ] has_flask = bool((ex.get("flask_url") or "").strip()) day = (trading_day or "").strip() + want_options = has_flask and "options" in caps if has_flask: tasks.extend( [ @@ -2592,15 +2594,26 @@ async def _fetch_exchange_flask_bundle( params={"trading_day": day}, ) ) + if want_options: + tasks.append(_fetch_flask_json(client, ex, "/api/hub/options/snapshot")) results = await asyncio.gather(*tasks) hub_mon = results[0] meta = results[1] - snap = results[2] if has_flask and len(results) > 2 else None - account = results[3] if has_flask and len(results) > 3 else None - trades_today = results[4] if has_flask and day and len(results) > 4 else None + idx = 2 + snap = None + account = None + trades_today = None options_snap = None - if has_flask and "options" in caps: - options_snap = await _fetch_flask_json(client, ex, "/api/hub/options/snapshot") + if has_flask: + snap = results[idx] + idx += 1 + account = results[idx] + idx += 1 + if day: + trades_today = results[idx] + idx += 1 + if want_options: + options_snap = results[idx] key_prices = None want_prices = HUB_BOARD_KEY_PRICES and "key" in caps if want_prices and isinstance(snap, dict): diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index 652ca17..0a892e5 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -4022,20 +4022,26 @@ function renderOptionsSectionBody(row, opts) { const options = opts || {}; const layout = options.layout || "table"; - const opt = row.options || {}; + const caps = Array.isArray(row.capabilities) ? row.capabilities : []; + const wantsOptions = caps.indexOf("options") >= 0; + const opt = row.options; let html = ""; - if (opt.enabled === false) { - html += renderOptionsAccountStatRow(opt); + if (wantsOptions && (opt == null || typeof opt !== "object")) { + html += '
æœŸæƒæŒä»“
'; + html += `
æœŸæƒæ•°æ®ä¸å¯ç”¨
`; + } else if ((opt || {}).enabled === false) { + html += renderOptionsAccountStatRow(opt || {}); html += '
æœŸæƒæŒä»“
'; html += '
æœŸæƒæœªå¯ç”¨(OKX_OPTIONS_ENABLED)
'; - } else if (opt.ok === false) { - html += renderOptionsAccountStatRow(opt); + } else if ((opt || {}).ok === false) { + html += renderOptionsAccountStatRow(opt || {}); html += '
æœŸæƒæŒä»“
'; - html += `
${esc(opt.msg || "æœŸæƒæ•°æ®ä¸å¯ç”¨")}
`; + html += `
${esc((opt && (opt.msg || opt.error)) || "æœŸæƒæ•°æ®ä¸å¯ç”¨")}
`; } else { - const pos = Array.isArray(opt.positions) ? opt.positions : []; - const targets = Array.isArray(opt.target_monitors) ? opt.target_monitors : []; - html += renderOptionsAccountStatRow(opt); + const optSafe = opt || {}; + const pos = Array.isArray(optSafe.positions) ? optSafe.positions : []; + const targets = Array.isArray(optSafe.target_monitors) ? optSafe.target_monitors : []; + html += renderOptionsAccountStatRow(optSafe); html += `
æœŸæƒæŒä»“ · ${pos.length} 仓
`; html += layout === "cards"