Group hedge option legs on dashboard; show long/short direction colors.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -278,6 +278,54 @@ body.hub-page-dashboard .page#page-dashboard {
|
|||||||
font-variant-numeric: tabular-nums;
|
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 {
|
.dash-empty-inline {
|
||||||
padding: 8px 0 4px;
|
padding: 8px 0 4px;
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
|
|||||||
@@ -177,20 +177,30 @@
|
|||||||
return `<span class="dash-pos-source ${sourceBadgeClass(s)}">${esc(s)}</span>`;
|
return `<span class="dash-pos-source ${sourceBadgeClass(s)}">${esc(s)}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function directionCell(side) {
|
||||||
|
const s = String(side || "").toLowerCase();
|
||||||
|
if (s === "long" || s === "buy") {
|
||||||
|
return `<span class="dash-side dash-side-long">做多</span>`;
|
||||||
|
}
|
||||||
|
if (s === "short" || s === "sell") {
|
||||||
|
return `<span class="dash-side dash-side-short">做空</span>`;
|
||||||
|
}
|
||||||
|
return esc(side || "—");
|
||||||
|
}
|
||||||
|
|
||||||
function renderUnifiedPerpTable(rows) {
|
function renderUnifiedPerpTable(rows) {
|
||||||
if (!rows.length) return "";
|
if (!rows.length) return "";
|
||||||
const body = rows
|
const body = rows
|
||||||
.map(({ ac, ln }) => {
|
.map(({ ac, ln }) => {
|
||||||
const source = String((ln && ln.source) || "—");
|
const source = String((ln && ln.source) || "—");
|
||||||
const symbol = esc((ln && (ln.symbol || ln.text)) || "—");
|
const symbol = esc((ln && (ln.symbol || ln.text)) || "—");
|
||||||
const side = esc((ln && ln.side) || "—");
|
|
||||||
const contracts =
|
const contracts =
|
||||||
ln && ln.contracts != null && ln.contracts !== "" ? esc(String(ln.contracts)) : "—";
|
ln && ln.contracts != null && ln.contracts !== "" ? esc(String(ln.contracts)) : "—";
|
||||||
return `<tr>
|
return `<tr>
|
||||||
<td>${exchangeLinkCell(ac)}</td>
|
<td>${exchangeLinkCell(ac)}</td>
|
||||||
<td>${sourceTypeCell(source)}</td>
|
<td>${sourceTypeCell(source)}</td>
|
||||||
<td>${symbol}</td>
|
<td>${symbol}</td>
|
||||||
<td>${side}</td>
|
<td>${directionCell(ln && ln.side)}</td>
|
||||||
<td>${priceCell(ln && ln.entry_price_fmt, ln && ln.entry_price)}</td>
|
<td>${priceCell(ln && ln.entry_price_fmt, ln && ln.entry_price)}</td>
|
||||||
<td>${priceCell(ln && ln.mark_price_fmt, ln && ln.mark_price)}</td>
|
<td>${priceCell(ln && ln.mark_price_fmt, ln && ln.mark_price)}</td>
|
||||||
<td>${contracts}</td>
|
<td>${contracts}</td>
|
||||||
@@ -226,32 +236,124 @@
|
|||||||
return null;
|
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 `<tr class="${groupCls || ""}">
|
||||||
|
<td>${exchangeLinkCell(ac)}</td>
|
||||||
|
<td>${sourceTypeCell(source)}</td>
|
||||||
|
<td title="${esc(p.inst_id || "")}">${esc(shortDashInst(p.inst_id))}</td>
|
||||||
|
<td>${esc(optType)}</td>
|
||||||
|
<td>${dashOptionsExpiryCd(p.exp_time_ms != null ? p.exp_time_ms : p.exp_time)}</td>
|
||||||
|
<td>${p.idx_px != null ? fmt(p.idx_px, 0) : "—"}</td>
|
||||||
|
<td><span class="${targetCls}">${esc(target)}</span></td>
|
||||||
|
<td class="${pnlClass(net)}">${net != null ? pnlSigned(net, 2) : "—"}</td>
|
||||||
|
</tr>`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderUnifiedOptionsTable(rows) {
|
function renderUnifiedOptionsTable(rows) {
|
||||||
if (!rows.length) return "";
|
if (!rows.length) return "";
|
||||||
const body = rows
|
const groups = [];
|
||||||
.map(({ ac, p }) => {
|
const byKey = new Map();
|
||||||
const optType =
|
rows.forEach((item) => {
|
||||||
(p.opt_type || "").toUpperCase() === "C"
|
const key = optionsGroupKey(item.ac, item.p);
|
||||||
? "Call"
|
if (!byKey.has(key)) {
|
||||||
: (p.opt_type || "").toUpperCase() === "P"
|
const planId = optionsHedgePlanId(item.p);
|
||||||
? "Put"
|
const source = String((item.p && (item.p.source_label || item.p.source)) || "期权");
|
||||||
: p.opt_type || "—";
|
const isHedge = key.indexOf("hedge:") === 0;
|
||||||
const source = String(p.source_label || p.source || "纯期权");
|
const g = {
|
||||||
const target = String(p.target_monitor_text || "—");
|
key: key,
|
||||||
const targetCls = target && target !== "—" ? "dash-target-monitor is-on" : "dash-target-monitor";
|
isHedge: isHedge,
|
||||||
const net = optionsNetPnl(p);
|
planId: planId,
|
||||||
return `<tr>
|
source: source,
|
||||||
<td>${exchangeLinkCell(ac)}</td>
|
items: [],
|
||||||
<td>${sourceTypeCell(source)}</td>
|
};
|
||||||
<td title="${esc(p.inst_id || "")}">${esc(shortDashInst(p.inst_id))}</td>
|
byKey.set(key, g);
|
||||||
<td>${esc(optType)}</td>
|
groups.push(g);
|
||||||
<td>${dashOptionsExpiryCd(p.exp_time_ms != null ? p.exp_time_ms : p.exp_time)}</td>
|
}
|
||||||
<td>${p.idx_px != null ? fmt(p.idx_px, 0) : "—"}</td>
|
byKey.get(key).items.push(item);
|
||||||
<td><span class="${targetCls}">${esc(target)}</span></td>
|
});
|
||||||
<td class="${pnlClass(net)}">${net != null ? pnlSigned(net, 2) : "—"}</td>
|
|
||||||
|
// 对冲组内 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 += `<tr class="dash-opt-group-head">
|
||||||
|
<td colspan="7"><span class="dash-opt-group-label">${esc(title)}</span></td>
|
||||||
|
<td class="${hasSum ? pnlClass(sum) : ""}">${hasSum ? pnlSigned(sum, 2) : "—"}</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
})
|
g.items.forEach(({ ac, p }, idx) => {
|
||||||
.join("");
|
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 `<div class="dash-pos-block dash-options-block">
|
return `<div class="dash-pos-block dash-options-block">
|
||||||
<div class="dash-ac-section-label">期权持仓</div>
|
<div class="dash-ac-section-label">期权持仓</div>
|
||||||
<div class="dash-table-wrap dash-options-table-wrap">
|
<div class="dash-table-wrap dash-options-table-wrap">
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<link rel="stylesheet" href="/assets/trade_stats_calendar.css?v=4" />
|
<link rel="stylesheet" href="/assets/trade_stats_calendar.css?v=4" />
|
||||||
<link rel="stylesheet" href="/assets/account_risk_badge.css?v=4" />
|
<link rel="stylesheet" href="/assets/account_risk_badge.css?v=4" />
|
||||||
<script src="/assets/account_risk_badge.js?v=4"></script>
|
<script src="/assets/account_risk_badge.js?v=4"></script>
|
||||||
<link rel="stylesheet" href="/assets/dashboard.css?v=20260720-dash-pnl-align" />
|
<link rel="stylesheet" href="/assets/dashboard.css?v=20260720-dash-hedge-group" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app-bg" aria-hidden="true"></div>
|
<div class="app-bg" aria-hidden="true"></div>
|
||||||
@@ -1385,7 +1385,7 @@
|
|||||||
<script src="/assets/archive.js?v=20260717-archive-cal-chart"></script>
|
<script src="/assets/archive.js?v=20260717-archive-cal-chart"></script>
|
||||||
<script src="/assets/quotes.js?v=20260717-quotes-feed"></script>
|
<script src="/assets/quotes.js?v=20260717-quotes-feed"></script>
|
||||||
<script src="/assets/funds.js?v=20260717-funds-scroll-fix"></script>
|
<script src="/assets/funds.js?v=20260717-funds-scroll-fix"></script>
|
||||||
<script src="/assets/dashboard.js?v=20260720-dash-pnl-align"></script>
|
<script src="/assets/dashboard.js?v=20260720-dash-hedge-group"></script>
|
||||||
<script src="/assets/strategy.js?v=8"></script>
|
<script src="/assets/strategy.js?v=8"></script>
|
||||||
<script src="/assets/help.js?v=1"></script>
|
<script src="/assets/help.js?v=1"></script>
|
||||||
<script src="/assets/logs.js?v=1"></script>
|
<script src="/assets/logs.js?v=1"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user