From 7bbb5663c9145c91060bb8eaae0c3ba06666763e Mon Sep 17 00:00:00 2001 From: dekun Date: Tue, 7 Jul 2026 15:22:11 +0800 Subject: [PATCH] Dashboard OKX card: 3-column perpetual/options split when options enabled. Show perpetual and options funding metrics separately, total funds row, then positions; other exchanges keep the 2-column layout. Co-authored-by: Cursor --- manual_trading_hub/hub_dashboard.py | 25 +++++++ manual_trading_hub/static/dashboard.css | 65 ++++++++++++++++++ manual_trading_hub/static/dashboard.js | 87 ++++++++++++++++--------- manual_trading_hub/static/index.html | 4 +- 4 files changed, 148 insertions(+), 33 deletions(-) diff --git a/manual_trading_hub/hub_dashboard.py b/manual_trading_hub/hub_dashboard.py index 5b86654..6194509 100644 --- a/manual_trading_hub/hub_dashboard.py +++ b/manual_trading_hub/hub_dashboard.py @@ -38,6 +38,29 @@ def _account_capital_base(ac: dict) -> Optional[float]: return None +def _options_layout_enabled(ac: dict) -> bool: + if str(ac.get("key") or "").lower() != "okx": + return False + snap = ac.get("options_snapshot") + if isinstance(snap, dict) and snap.get("enabled") is not False: + return True + return ac.get("options_funding_usdt") is not None or ac.get("options_trading_usdt") is not None + + +def _perpetual_float_pnl_u(ac: dict) -> Optional[float]: + try: + total = float(ac.get("float_pnl_u") or 0) + except (TypeError, ValueError): + total = 0.0 + opt = ac.get("options_float_pnl_u") + if opt is None: + return round(total, 4) + try: + return round(total - float(opt), 4) + except (TypeError, ValueError): + return round(total, 4) + + def _enrich_account_row(ac: dict) -> dict: st = ac.get("trade_stats") or {} capital = _account_capital_base(ac) @@ -61,6 +84,8 @@ def _enrich_account_row(ac: dict) -> dict: "options_trading_usdt": ac.get("options_trading_usdt"), "options_float_pnl_u": ac.get("options_float_pnl_u"), "options_open_position_count": ac.get("options_open_position_count"), + "options_layout": _options_layout_enabled(ac), + "perpetual_float_pnl_u": _perpetual_float_pnl_u(ac), "capital_total_usdt": round(capital, 4) if capital is not None else None, "available_trading_usdt": ac.get("available_trading_usdt"), "pnl_u": st.get("total_pnl_u"), diff --git a/manual_trading_hub/static/dashboard.css b/manual_trading_hub/static/dashboard.css index 3b95eb8..be719bd 100644 --- a/manual_trading_hub/static/dashboard.css +++ b/manual_trading_hub/static/dashboard.css @@ -276,6 +276,71 @@ body.hub-page-dashboard .page#page-dashboard { font-size: 0.76rem; } +.dash-ac-metrics-3col { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.dash-ac-section-label { + grid-column: 1 / -1; + font-size: 0.68rem; + font-weight: 700; + letter-spacing: 0.06em; + color: var(--dash-accent); + margin-top: 2px; + padding-bottom: 2px; + border-bottom: 1px dashed color-mix(in srgb, var(--dash-card-border) 80%, transparent); +} + +.dash-ac-section-label:not(:first-child) { + margin-top: 8px; +} + +.dash-ac-metric-empty { + visibility: hidden; + min-height: 0; + padding: 0; + margin: 0; +} + +.dash-ac-total-row { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 12px; + margin-top: 10px; + padding: 8px 10px; + border-radius: 8px; + border: 1px solid var(--dash-card-border); + background: color-mix(in srgb, var(--dash-accent) 6%, var(--dash-card-bg)); + font-family: JetBrains Mono, monospace; + font-size: 0.76rem; +} + +.dash-ac-total-row span { + color: var(--dash-muted); + font-size: 0.68rem; +} + +.dash-ac-total-row strong { + color: var(--dash-text); + font-size: 0.88rem; +} + +.dash-ac-card-options .dash-ac-remark { + margin-top: 8px; + padding-top: 8px; + border-top: 1px dashed color-mix(in srgb, var(--dash-card-border) 80%, transparent); +} + +.dash-ac-metrics-3col .dash-ac-metric { + text-align: center; +} + +.dash-ac-metrics-3col .dash-ac-metric span, +.dash-ac-metrics-3col .dash-ac-metric strong { + text-align: center; +} + .dash-ac-metric span { display: block; color: var(--dash-muted); diff --git a/manual_trading_hub/static/dashboard.js b/manual_trading_hub/static/dashboard.js index 8b7cf5b..1218a26 100644 --- a/manual_trading_hub/static/dashboard.js +++ b/manual_trading_hub/static/dashboard.js @@ -170,8 +170,57 @@ }); } + function dashMetric(label, value, valCls) { + return `
${esc(label)}${value}
`; + } + + function dashSectionLabel(text) { + return ``; + } + + function renderOkxOptionsMetrics(ac) { + const pnl = Number(ac.pnl_u); + const perpFloat = Number( + ac.perpetual_float_pnl_u != null ? ac.perpetual_float_pnl_u : ac.float_pnl_u + ); + const optFloat = Number(ac.options_float_pnl_u); + const pf = + ac.perpetual_funding_usdt != null ? ac.perpetual_funding_usdt : ac.funding_usdt; + const pt = + ac.perpetual_trading_usdt != null ? ac.perpetual_trading_usdt : ac.trading_usdt; + return `
+ ${dashSectionLabel("永续账户")} + ${dashMetric("资金账户", `${fmt(pf, 2)}U`)} + ${dashMetric("交易账户", `${fmt(pt, 2)}U`)} + ${dashMetric("今日盈亏", pnlSigned(pnl, 2), pnlClass(pnl))} + ${dashMetric("平仓笔数", String(Number(ac.closed_count) || 0))} + ${dashMetric("浮盈亏", pnlSigned(perpFloat, 2), pnlClass(perpFloat))} + + ${dashSectionLabel("期权账户")} + ${dashMetric("资金账户", `${fmt(ac.options_funding_usdt, 2)}U`)} + ${dashMetric("交易账户", `${fmt(ac.options_trading_usdt, 2)}U`)} + ${dashMetric("期权浮盈亏", pnlSigned(optFloat, 2), pnlClass(optFloat))} +
+
+ 总资金 + ${fmt(ac.capital_total_usdt, 2)}U +
`; + } + + function renderStandardMetrics(ac) { + const pnl = Number(ac.pnl_u); + const floatPnl = Number(ac.float_pnl_u); + return `
+ ${dashMetric("资金账户", `${fmt(ac.funding_usdt, 2)}U`)} + ${dashMetric("交易账户", `${fmt(ac.trading_usdt, 2)}U`)} + ${dashMetric("资金合计", `${fmt(ac.capital_total_usdt, 2)}U`)} + ${dashMetric("今日盈亏", pnlSigned(pnl, 2), pnlClass(pnl))} + ${dashMetric("平仓笔数", String(Number(ac.closed_count) || 0))} + ${dashMetric("浮盈亏", pnlSigned(floatPnl, 2), pnlClass(floatPnl))} +
`; + } + function renderAccounts(accounts, threshold) { - if (!elAccounts) return; const rows = Array.isArray(accounts) ? accounts : []; if (!rows.length) { elAccounts.innerHTML = '
暂无账户数据
'; @@ -181,8 +230,6 @@ .map((ac) => { const alert = !!ac.loss_alert; const unmon = !ac.monitored; - const pnl = Number(ac.pnl_u); - const floatPnl = Number(ac.float_pnl_u); const lossPct = Number(ac.daily_loss_pct); const barW = alert && Number.isFinite(lossPct) @@ -195,38 +242,16 @@ alert && barW > 0 ? `
` : ""; - const optFunding = ac.options_funding_usdt; - const optTrading = ac.options_trading_usdt; - const optFloat = Number(ac.options_float_pnl_u); - const optPos = Number(ac.options_open_position_count) || 0; - const optMetrics = - optFunding != null || optTrading != null || optPos > 0 - ? `
期权资金${fmt( - optFunding != null && optTrading != null - ? Number(optFunding) + Number(optTrading) - : null, - 2 - )}U
-
期权持仓${optPos} 仓
-
期权浮盈${pnlSigned( - optFloat, - 2 - )}
` - : ""; - return `
+ const metricsHtml = ac.options_layout + ? renderOkxOptionsMetrics(ac) + : renderStandardMetrics(ac); + const cardCls = ac.options_layout ? " dash-ac-card-options" : ""; + return `
${esc(ac.name || "—")}
${badge}
-
-
资金账户${fmt(ac.funding_usdt, 2)}U
-
交易账户${fmt(ac.trading_usdt, 2)}U
-
资金合计${fmt(ac.capital_total_usdt, 2)}U
-
今日盈亏${pnlSigned(pnl, 2)}
-
平仓笔数${Number(ac.closed_count) || 0}
-
浮盈亏${pnlSigned(floatPnl, 2)}
- ${optMetrics} -
+ ${metricsHtml} ${lossBar} ${renderAccountDetail(ac)}
`; diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index a5ba0d1..83a1451 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -19,7 +19,7 @@ - + @@ -1146,7 +1146,7 @@ - +