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 @@
-
+