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