diff --git a/manual_trading_hub/static/dashboard.css b/manual_trading_hub/static/dashboard.css index 128cf30..978cc25 100644 --- a/manual_trading_hub/static/dashboard.css +++ b/manual_trading_hub/static/dashboard.css @@ -290,42 +290,6 @@ body.hub-page-dashboard .page#page-dashboard { 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 aaac17d..a8be8af 100644 --- a/manual_trading_hub/static/dashboard.js +++ b/manual_trading_hub/static/dashboard.js @@ -259,6 +259,16 @@ return "solo:" + ex + ":" + String((p && p.inst_id) || Math.random()); } + function optionsTypeLabel(p) { + const source = String((p && (p.source_label || p.source)) || "纯期权").trim() || "纯期权"; + const planId = optionsHedgePlanId(p); + if (planId && (source.indexOf("对冲") >= 0 || /对冲#/.test(String((p && p.target_monitor_text) || "")))) { + const base = source.indexOf("对冲") >= 0 ? source.replace(/\s*#\s*\d+\s*$/, "") : "对冲"; + return base + "#" + planId; + } + return source; + } + function optionsRoiPct(p) { if (!p || typeof p !== "object") return null; const preview = p.close_preview || {}; @@ -273,19 +283,19 @@ return null; } - function renderOptionsLegRow(ac, p, groupCls) { + function renderOptionsLegRow(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 source = optionsTypeLabel(p); const target = String(p.target_monitor_text || "—"); const targetCls = target && target !== "—" ? "dash-target-monitor is-on" : "dash-target-monitor"; const net = optionsNetPnl(p); const roi = optionsRoiPct(p); - return ` + return ` ${exchangeLinkCell(ac)} ${sourceTypeCell(source)} ${esc(shortDashInst(p.inst_id))} @@ -300,86 +310,19 @@ function renderUnifiedOptionsTable(rows) { if (!rows.length) return ""; - 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 paidSum = 0; - let hasSum = false; - let hasPaid = false; - g.items.forEach(({ p }) => { - const n = optionsNetPnl(p); - if (n != null) { - sum += n; - hasSum = true; - } - const paid = Number(p && p.premium_paid); - if (Number.isFinite(paid) && paid > 0) { - paidSum += paid; - hasPaid = true; - } - }); - const groupRoi = hasSum && hasPaid && paidSum > 0 ? (sum / paidSum) * 100 : null; - const title = - (g.source && g.source !== "—" ? g.source : "对冲") + - (g.planId ? " #" + g.planId : "") + - " · " + - g.items.length + - " 腿"; - body += ` - ${esc(title)} - ${hasSum ? pnlSigned(sum, 2) : "—"} - ${ - groupRoi != null ? esc(Number(groupRoi).toFixed(2)) + "%" : "—" - } - `; - 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, ""); - }); - } + // 同所同计划相邻,Call 在前 Put 在后;不额外画分组框 + const sorted = rows.slice().sort((a, b) => { + const ka = optionsGroupKey(a.ac, a.p); + const kb = optionsGroupKey(b.ac, b.p); + if (ka !== kb) return ka.localeCompare(kb); + 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); }); + const body = sorted.map(({ ac, p }) => renderOptionsLegRow(ac, p)).join(""); return `
期权持仓
diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index ab1a462..b9dc9cc 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -20,7 +20,7 @@ - + @@ -1385,7 +1385,7 @@ - +