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:
@@ -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()),
|
||||
}
|
||||
|
||||
|
||||
@@ -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()),
|
||||
}
|
||||
|
||||
|
||||
@@ -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()),
|
||||
}
|
||||
|
||||
|
||||
@@ -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,非真·实时
|
||||
@@ -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 静默更新时间戳
|
||||
@@ -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>
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 += '<div class="section-title hub-options-title">期权持仓</div>';
|
||||
html += `<div class="err">期权数据不可用</div>`;
|
||||
} else if ((opt || {}).enabled === false) {
|
||||
html += renderOptionsAccountStatRow(opt || {});
|
||||
html += '<div class="section-title hub-options-title">期权持仓</div>';
|
||||
html += '<div class="empty-hint">期权未启用(OKX_OPTIONS_ENABLED)</div>';
|
||||
} else if (opt.ok === false) {
|
||||
html += renderOptionsAccountStatRow(opt);
|
||||
} else if ((opt || {}).ok === false) {
|
||||
html += renderOptionsAccountStatRow(opt || {});
|
||||
html += '<div class="section-title hub-options-title">期权持仓</div>';
|
||||
html += `<div class="err">${esc(opt.msg || "期权数据不可用")}</div>`;
|
||||
html += `<div class="err">${esc((opt && (opt.msg || opt.error)) || "期权数据不可用")}</div>`;
|
||||
} 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 += `<div class="section-title hub-options-title">期权持仓 · ${pos.length} 仓</div>`;
|
||||
html +=
|
||||
layout === "cards"
|
||||
|
||||
Reference in New Issue
Block a user