From 977d62bddf720a3c1e84e1356d32bcf453d43418 Mon Sep 17 00:00:00 2001 From: dekun Date: Mon, 20 Jul 2026 12:38:11 +0800 Subject: [PATCH] Group hedge option legs on dashboard; show long/short direction colors. Co-authored-by: Cursor --- manual_trading_hub/static/dashboard.css | 48 ++++++++ manual_trading_hub/static/dashboard.js | 152 ++++++++++++++++++++---- manual_trading_hub/static/index.html | 4 +- 3 files changed, 177 insertions(+), 27 deletions(-) diff --git a/manual_trading_hub/static/dashboard.css b/manual_trading_hub/static/dashboard.css index 4f99578..128cf30 100644 --- a/manual_trading_hub/static/dashboard.css +++ b/manual_trading_hub/static/dashboard.css @@ -278,6 +278,54 @@ body.hub-page-dashboard .page#page-dashboard { font-variant-numeric: tabular-nums; } +.dash-side { + font-weight: 700; +} + +.dash-side-long { + color: #3dd68c; +} + +.dash-side-short { + color: #ff6b7a; +} + +.dash-opt-group-head td { + background: color-mix(in srgb, var(--dash-accent) 10%, var(--dash-card-bg)); + border-top: 1px solid color-mix(in srgb, var(--dash-accent) 35%, var(--dash-card-border)); + padding-top: 10px; + padding-bottom: 8px; +} + +.dash-opt-group-label { + display: inline-flex; + align-items: center; + gap: 6px; + font-weight: 700; + color: var(--dash-accent); + letter-spacing: 0.02em; +} + +.dash-opt-group-label::before { + content: "一组"; + font-size: 0.62rem; + font-weight: 700; + padding: 1px 6px; + border-radius: 999px; + border: 1px solid color-mix(in srgb, var(--dash-accent) 45%, transparent); + color: var(--dash-accent); + background: color-mix(in srgb, var(--dash-accent) 12%, transparent); +} + +.dash-opt-group-row td { + background: color-mix(in srgb, var(--dash-accent) 4%, transparent); + box-shadow: inset 3px 0 0 color-mix(in srgb, var(--dash-accent) 55%, transparent); +} + +.dash-opt-group-row.is-last td { + border-bottom-color: color-mix(in srgb, var(--dash-accent) 28%, var(--dash-card-border)); +} + .dash-empty-inline { padding: 8px 0 4px; font-size: 0.78rem; diff --git a/manual_trading_hub/static/dashboard.js b/manual_trading_hub/static/dashboard.js index bd997cb..2878dff 100644 --- a/manual_trading_hub/static/dashboard.js +++ b/manual_trading_hub/static/dashboard.js @@ -177,20 +177,30 @@ return `${esc(s)}`; } + function directionCell(side) { + const s = String(side || "").toLowerCase(); + if (s === "long" || s === "buy") { + return `做多`; + } + if (s === "short" || s === "sell") { + return `做空`; + } + return esc(side || "—"); + } + function renderUnifiedPerpTable(rows) { if (!rows.length) return ""; const body = rows .map(({ ac, ln }) => { const source = String((ln && ln.source) || "—"); 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)) : "—"; return ` ${exchangeLinkCell(ac)} ${sourceTypeCell(source)} ${symbol} - ${side} + ${directionCell(ln && ln.side)} ${priceCell(ln && ln.entry_price_fmt, ln && ln.entry_price)} ${priceCell(ln && ln.mark_price_fmt, ln && ln.mark_price)} ${contracts} @@ -226,32 +236,124 @@ return null; } + function optionsHedgePlanId(p) { + if (!p || typeof p !== "object") return ""; + const hedge = p.hedge_plan_target; + if (hedge && typeof hedge === "object" && hedge.plan_id != null && hedge.plan_id !== "") { + return String(hedge.plan_id); + } + if (p.source_plan_id != null && p.source_plan_id !== "") return String(p.source_plan_id); + const target = String(p.target_monitor_text || ""); + const m = target.match(/对冲\s*#\s*(\d+)/); + return m ? m[1] : ""; + } + + function optionsGroupKey(ac, p) { + const ex = ac && ac.id != null ? String(ac.id) : String((ac && ac.name) || ""); + const planId = optionsHedgePlanId(p); + const source = String((p && (p.source_label || p.source)) || ""); + if (planId && source.indexOf("对冲") >= 0) return "hedge:" + ex + ":" + planId; + if (planId && /对冲#/.test(String((p && p.target_monitor_text) || ""))) { + return "hedge:" + ex + ":" + planId; + } + return "solo:" + ex + ":" + String((p && p.inst_id) || Math.random()); + } + + function renderOptionsLegRow(ac, p, groupCls) { + 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 ` + ${exchangeLinkCell(ac)} + ${sourceTypeCell(source)} + ${esc(shortDashInst(p.inst_id))} + ${esc(optType)} + ${dashOptionsExpiryCd(p.exp_time_ms != null ? p.exp_time_ms : p.exp_time)} + ${p.idx_px != null ? fmt(p.idx_px, 0) : "—"} + ${esc(target)} + ${net != null ? pnlSigned(net, 2) : "—"} + `; + } + 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 ` - ${exchangeLinkCell(ac)} - ${sourceTypeCell(source)} - ${esc(shortDashInst(p.inst_id))} - ${esc(optType)} - ${dashOptionsExpiryCd(p.exp_time_ms != null ? p.exp_time_ms : p.exp_time)} - ${p.idx_px != null ? fmt(p.idx_px, 0) : "—"} - ${esc(target)} - ${net != null ? pnlSigned(net, 2) : "—"} + const groups = []; + const byKey = new Map(); + rows.forEach((item) => { + const key = optionsGroupKey(item.ac, item.p); + if (!byKey.has(key)) { + const planId = optionsHedgePlanId(item.p); + const source = String((item.p && (item.p.source_label || item.p.source)) || "期权"); + const isHedge = key.indexOf("hedge:") === 0; + const g = { + key: key, + isHedge: isHedge, + planId: planId, + source: source, + items: [], + }; + byKey.set(key, g); + groups.push(g); + } + byKey.get(key).items.push(item); + }); + + // 对冲组内 Call 在前 Put 在后 + groups.forEach((g) => { + if (!g.isHedge || g.items.length < 2) return; + g.items.sort((a, b) => { + const ta = String((a.p && a.p.opt_type) || "").toUpperCase(); + const tb = String((b.p && b.p.opt_type) || "").toUpperCase(); + if (ta === tb) return 0; + if (ta === "C") return -1; + if (tb === "C") return 1; + return ta.localeCompare(tb); + }); + }); + + let body = ""; + groups.forEach((g) => { + if (g.isHedge && g.items.length >= 1) { + let sum = 0; + let hasSum = false; + g.items.forEach(({ p }) => { + const n = optionsNetPnl(p); + if (n != null) { + sum += n; + hasSum = true; + } + }); + const title = + (g.source && g.source !== "—" ? g.source : "对冲") + + (g.planId ? " #" + g.planId : "") + + " · " + + g.items.length + + " 腿"; + body += ` + ${esc(title)} + ${hasSum ? pnlSigned(sum, 2) : "—"} `; - }) - .join(""); + g.items.forEach(({ ac, p }, idx) => { + const cls = + "dash-opt-group-row" + + (idx === 0 ? " is-first" : "") + + (idx === g.items.length - 1 ? " is-last" : ""); + body += renderOptionsLegRow(ac, p, cls); + }); + } else { + g.items.forEach(({ ac, p }) => { + body += renderOptionsLegRow(ac, p, ""); + }); + } + }); + return `
diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index ffdeb61..577423d 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -20,7 +20,7 @@ - + @@ -1385,7 +1385,7 @@ - +