From ee3c1bcdcac56617ec71e71666fbf763ffd6cede Mon Sep 17 00:00:00 2001 From: dekun Date: Mon, 20 Jul 2026 12:27:19 +0800 Subject: [PATCH] Unify hub dashboard positions into one card with exchange links. Co-authored-by: Cursor --- manual_trading_hub/hub_ai/context.py | 10 ++ manual_trading_hub/static/dashboard.css | 34 ++++- manual_trading_hub/static/dashboard.js | 178 +++++++++++------------- manual_trading_hub/static/index.html | 4 +- 4 files changed, 130 insertions(+), 96 deletions(-) diff --git a/manual_trading_hub/hub_ai/context.py b/manual_trading_hub/hub_ai/context.py index b6f8031..bc96493 100644 --- a/manual_trading_hub/hub_ai/context.py +++ b/manual_trading_hub/hub_ai/context.py @@ -1100,6 +1100,11 @@ def format_dashboard_account_detail(ac: dict) -> dict[str, Any]: contracts = p.get("size") upnl = _position_float_pnl(p) source = resolve_position_monitor_source(p, hub_mon) + entry = _safe_float(p.get("entry_price")) + mark = _safe_float(p.get("mark_price")) + notional = _safe_float(p.get("notional_usdt")) + if notional is None: + notional = _safe_float(p.get("notional")) position_lines.append( { "kind": "position", @@ -1107,6 +1112,11 @@ def format_dashboard_account_detail(ac: dict) -> dict[str, Any]: "symbol": sym, "side": side, "contracts": contracts, + "entry_price": entry, + "entry_price_fmt": p.get("entry_price_fmt"), + "mark_price": mark, + "mark_price_fmt": p.get("mark_price_fmt"), + "notional_usdt": notional, "text": f"{sym} {side}", "pnl": round(upnl, 4), } diff --git a/manual_trading_hub/static/dashboard.css b/manual_trading_hub/static/dashboard.css index 6579fcc..6312062 100644 --- a/manual_trading_hub/static/dashboard.css +++ b/manual_trading_hub/static/dashboard.css @@ -247,11 +247,43 @@ body.hub-page-dashboard .page#page-dashboard { .dash-ac-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr)); + grid-template-columns: 1fr; gap: 12px; padding: 14px; } +.dash-pos-unified-card { + width: 100%; +} + +.dash-ex-link { + appearance: none; + border: 0; + background: transparent; + color: var(--dash-accent); + font: inherit; + font-weight: 600; + padding: 0; + cursor: pointer; + text-decoration: underline; + text-underline-offset: 2px; +} + +.dash-ex-link:hover { + color: color-mix(in srgb, var(--dash-accent) 80%, #fff); +} + +.dash-empty-inline { + padding: 8px 0 4px; + font-size: 0.78rem; +} + +.dash-pos-alert-note { + margin-top: 4px; + font-size: 0.75rem; + color: var(--dash-warn); +} + .dash-ac-card { position: relative; padding: 14px 16px; diff --git a/manual_trading_hub/static/dashboard.js b/manual_trading_hub/static/dashboard.js index d87064a..6e23f7d 100644 --- a/manual_trading_hub/static/dashboard.js +++ b/manual_trading_hub/static/dashboard.js @@ -88,22 +88,6 @@ `; } - function renderMonitorCountChips(counts) { - const mc = counts || {}; - const chips = []; - const keys = Number(mc.keys) || 0; - const orders = Number(mc.orders) || 0; - const trends = Number(mc.trends) || 0; - const rolls = Number(mc.rolls) || 0; - if (keys > 0) chips.push(`关键位 ${keys}`); - if (orders > 0) { - chips.push(`下单监控 ${orders}`); - } - if (trends > 0) chips.push(`趋势回调 ${trends}`); - if (rolls > 0) chips.push(`顺势加仓 ${rolls}`); - return chips; - } - function dashOptionsExpiryCd(expMs) { const ms = expMs != null && expMs !== "" ? String(expMs) : ""; if (!ms) return "—"; @@ -130,34 +114,75 @@ return perp.length > 0 || (ac && ac.options_layout && optionsPositions.length > 0); } - function sourceBadgeClass(source) { - const s = String(source || ""); - if (s.indexOf("对冲") >= 0) return "is-hedge"; - if (s.indexOf("纯期权") >= 0 || s === "期权") return "is-opt"; - if (s.indexOf("顺势") >= 0) return "is-roll"; - if (s.indexOf("趋势") >= 0) return "is-trend"; - if (s.indexOf("关键位") >= 0) return "is-key"; - if (s.indexOf("下单") >= 0) return "is-order"; - return "is-none"; + function exchangeLinkCell(ac) { + const name = esc((ac && ac.name) || "—"); + const exId = ac && ac.id != null ? String(ac.id) : ""; + if (!exId) return name; + return ( + `` + ); } - function renderDashboardPerpTable(lines) { - const rows = Array.isArray(lines) ? lines : []; - if (!rows.length) return ""; + function priceCell(fmtVal, raw) { + if (fmtVal != null && String(fmtVal).trim() !== "") return esc(String(fmtVal)); + if (raw != null && Number.isFinite(Number(raw))) return esc(fmt(raw, 4).replace(/\.?0+$/, "")); + return "—"; + } + + function floatPctText(pnl, notional) { + const n = Number(pnl); + const notion = Number(notional); + if (!Number.isFinite(n) || !Number.isFinite(notion) || Math.abs(notion) < 1e-8) return "—"; + const pct = (n / Math.abs(notion)) * 100; + const abs = Math.abs(pct).toFixed(2); + if (Math.abs(pct) < 1e-9) return "0.00%"; + return `${pct > 0 ? "+" : "-"}${abs}%`; + } + + function collectUnifiedPositions(accounts) { + const perp = []; + const options = []; + (Array.isArray(accounts) ? accounts : []).forEach((ac) => { + if (!accountHasOpenPositions(ac)) return; + accountPerpLines(ac).forEach((ln) => { + if (!ln) return; + perp.push({ ac: ac, ln: ln }); + }); + if (ac && ac.options_layout) { + (Array.isArray(ac.options_positions) ? ac.options_positions : []).forEach((p) => { + if (!p) return; + options.push({ ac: ac, p: p }); + }); + } + }); + return { perp: perp, options: options }; + } + + function renderUnifiedPerpTable(rows) { + if (!rows.length) { + return `
+ +
当前无永续持仓
+
`; + } const body = rows - .map((ln) => { - const source = String((ln && ln.source) || "—"); + .map(({ ac, ln }) => { const symbol = esc((ln && (ln.symbol || ln.text)) || "—"); const side = esc((ln && ln.side) || "—"); const contracts = ln && ln.contracts != null && ln.contracts !== "" ? esc(String(ln.contracts)) : "—"; const pnl = ln && ln.pnl != null ? Number(ln.pnl) : NaN; + const pct = floatPctText(pnl, ln && ln.notional_usdt); return ` - ${esc(source)} + ${exchangeLinkCell(ac)} ${symbol} ${side} + ${priceCell(ln && ln.entry_price_fmt, ln && ln.entry_price)} + ${priceCell(ln && ln.mark_price_fmt, ln && ln.mark_price)} ${contracts} ${Number.isFinite(pnl) ? pnlSigned(pnl, 2) : "—"} + ${pct} `; }) .join(""); @@ -166,7 +191,7 @@
- + ${body}
来源合约方向张数浮盈交易所合约方向开仓价标记价张数盈利金额浮盈
@@ -188,23 +213,26 @@ return null; } - function renderDashboardOptionsTable(positions) { - const pos = Array.isArray(positions) ? positions : []; - if (!pos.length) return ""; - const rows = pos - .map((p) => { + function renderUnifiedOptionsTable(rows) { + if (!rows.length) { + return `
+ +
当前无期权持仓
+
`; + } + const body = rows + .map(({ ac, p }) => { const optType = (p.opt_type || "").toUpperCase() === "C" ? "Call" : (p.opt_type || "").toUpperCase() === "P" ? "Put" : p.opt_type || "—"; - const source = String(p.source_label || p.source || "纯期权"); const target = String(p.target_monitor_text || "—"); const targetCls = target && target !== "—" ? "dash-target-monitor is-on" : "dash-target-monitor"; const net = optionsNetPnl(p); return ` - ${esc(source)} + ${exchangeLinkCell(ac)} ${esc(shortDashInst(p.inst_id))} ${esc(optType)} ${dashOptionsExpiryCd(p.exp_time_ms != null ? p.exp_time_ms : p.exp_time)} @@ -219,33 +247,17 @@
- + - ${rows} + ${body}
来源合约类型到期倒计时指数目标监控净盈亏交易所合约类型到期倒计时指数目标监控净盈亏
`; } - function renderAccountPositions(ac) { - const perpLines = accountPerpLines(ac); - const optionsPositions = Array.isArray(ac && ac.options_positions) ? ac.options_positions : []; - const issues = Array.isArray(ac && ac.issues) ? ac.issues : []; - const chips = renderMonitorCountChips((ac && ac.monitor_counts) || {}); - const monitorRow = chips.length - ? `
${chips.join("")}
` - : ""; - const perpHtml = renderDashboardPerpTable(perpLines); - const optionsHtml = ac && ac.options_layout ? renderDashboardOptionsTable(optionsPositions) : ""; - const issueHtml = issues - .map((text) => `
${esc(text)}
`) - .join(""); - return `${monitorRow}${perpHtml}${optionsHtml}${issueHtml}`; - } - function bindDashboardExpand() { if (!elAccounts) return; - elAccounts.querySelectorAll(".dash-ac-expand-btn").forEach((btn) => { + elAccounts.querySelectorAll("[data-dash-ex-id]").forEach((btn) => { btn.addEventListener("click", (ev) => { ev.preventDefault(); ev.stopPropagation(); @@ -256,44 +268,24 @@ } function renderAccounts(accounts, threshold) { - const rows = (Array.isArray(accounts) ? accounts : []).filter(accountHasOpenPositions); - if (!rows.length) { + const unified = collectUnifiedPositions(accounts); + if (!unified.perp.length && !unified.options.length) { elAccounts.innerHTML = '
当前无持仓账户
'; return; } - elAccounts.innerHTML = rows - .map((ac) => { - const alert = !!ac.loss_alert; - const unmon = !ac.monitored; - const lossPct = Number(ac.daily_loss_pct); - const barW = - alert && Number.isFinite(lossPct) - ? Math.min(100, (lossPct / Math.max(threshold, 1)) * 100) - : 0; - const badge = alert - ? `单日亏损 ≥${threshold}%` - : `${esc(ac.status || "—")}`; - const exId = ac && ac.id != null ? String(ac.id) : ""; - const expandBtn = exId - ? `` - : ""; - const lossBar = - alert && barW > 0 - ? `
` - : ""; - const cardCls = ac.options_layout ? " dash-ac-card-options" : ""; - return `
-
-
${esc(ac.name || "—")}
-
${badge}${expandBtn}
-
- ${lossBar} -
${renderAccountPositions(ac)}
-
`; - }) - .join(""); + const alertAccounts = (Array.isArray(accounts) ? accounts : []).filter((a) => a && a.loss_alert); + const alertNote = alertAccounts.length + ? `
风险: ${esc( + alertAccounts.map((a) => a.name || "").filter(Boolean).join("、") + )} 单日亏损 ≥${threshold}%
` + : ""; + elAccounts.innerHTML = `
+
+ ${renderUnifiedPerpTable(unified.perp)} + ${renderUnifiedOptionsTable(unified.options)} + ${alertNote} +
+
`; bindDashboardExpand(); if (window.OptionsExpiryCountdown && OptionsExpiryCountdown.ensureTimer) { OptionsExpiryCountdown.ensureTimer(); diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index 207fbd0..400868b 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -20,7 +20,7 @@ - + @@ -1385,7 +1385,7 @@ - +