Unify hub dashboard positions into one card with exchange links.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1100,6 +1100,11 @@ def format_dashboard_account_detail(ac: dict) -> dict[str, Any]:
|
|||||||
contracts = p.get("size")
|
contracts = p.get("size")
|
||||||
upnl = _position_float_pnl(p)
|
upnl = _position_float_pnl(p)
|
||||||
source = resolve_position_monitor_source(p, hub_mon)
|
source = resolve_position_monitor_source(p, hub_mon)
|
||||||
|
entry = _safe_float(p.get("entry_price"))
|
||||||
|
mark = _safe_float(p.get("mark_price"))
|
||||||
|
notional = _safe_float(p.get("notional_usdt"))
|
||||||
|
if notional is None:
|
||||||
|
notional = _safe_float(p.get("notional"))
|
||||||
position_lines.append(
|
position_lines.append(
|
||||||
{
|
{
|
||||||
"kind": "position",
|
"kind": "position",
|
||||||
@@ -1107,6 +1112,11 @@ def format_dashboard_account_detail(ac: dict) -> dict[str, Any]:
|
|||||||
"symbol": sym,
|
"symbol": sym,
|
||||||
"side": side,
|
"side": side,
|
||||||
"contracts": contracts,
|
"contracts": contracts,
|
||||||
|
"entry_price": entry,
|
||||||
|
"entry_price_fmt": p.get("entry_price_fmt"),
|
||||||
|
"mark_price": mark,
|
||||||
|
"mark_price_fmt": p.get("mark_price_fmt"),
|
||||||
|
"notional_usdt": notional,
|
||||||
"text": f"{sym} {side}",
|
"text": f"{sym} {side}",
|
||||||
"pnl": round(upnl, 4),
|
"pnl": round(upnl, 4),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -247,11 +247,43 @@ body.hub-page-dashboard .page#page-dashboard {
|
|||||||
|
|
||||||
.dash-ac-grid {
|
.dash-ac-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
|
grid-template-columns: 1fr;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dash-pos-unified-card {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dash-ex-link {
|
||||||
|
appearance: none;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--dash-accent);
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dash-ex-link:hover {
|
||||||
|
color: color-mix(in srgb, var(--dash-accent) 80%, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dash-empty-inline {
|
||||||
|
padding: 8px 0 4px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dash-pos-alert-note {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--dash-warn);
|
||||||
|
}
|
||||||
|
|
||||||
.dash-ac-card {
|
.dash-ac-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
|
|||||||
@@ -88,22 +88,6 @@
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMonitorCountChips(counts) {
|
|
||||||
const mc = counts || {};
|
|
||||||
const chips = [];
|
|
||||||
const keys = Number(mc.keys) || 0;
|
|
||||||
const orders = Number(mc.orders) || 0;
|
|
||||||
const trends = Number(mc.trends) || 0;
|
|
||||||
const rolls = Number(mc.rolls) || 0;
|
|
||||||
if (keys > 0) chips.push(`<span class="dash-monitor-chip dash-monitor-key">关键位 ${keys}</span>`);
|
|
||||||
if (orders > 0) {
|
|
||||||
chips.push(`<span class="dash-monitor-chip dash-monitor-order">下单监控 ${orders}</span>`);
|
|
||||||
}
|
|
||||||
if (trends > 0) chips.push(`<span class="dash-monitor-chip dash-monitor-trend">趋势回调 ${trends}</span>`);
|
|
||||||
if (rolls > 0) chips.push(`<span class="dash-monitor-chip dash-monitor-roll">顺势加仓 ${rolls}</span>`);
|
|
||||||
return chips;
|
|
||||||
}
|
|
||||||
|
|
||||||
function dashOptionsExpiryCd(expMs) {
|
function dashOptionsExpiryCd(expMs) {
|
||||||
const ms = expMs != null && expMs !== "" ? String(expMs) : "";
|
const ms = expMs != null && expMs !== "" ? String(expMs) : "";
|
||||||
if (!ms) return "—";
|
if (!ms) return "—";
|
||||||
@@ -130,34 +114,75 @@
|
|||||||
return perp.length > 0 || (ac && ac.options_layout && optionsPositions.length > 0);
|
return perp.length > 0 || (ac && ac.options_layout && optionsPositions.length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sourceBadgeClass(source) {
|
function exchangeLinkCell(ac) {
|
||||||
const s = String(source || "");
|
const name = esc((ac && ac.name) || "—");
|
||||||
if (s.indexOf("对冲") >= 0) return "is-hedge";
|
const exId = ac && ac.id != null ? String(ac.id) : "";
|
||||||
if (s.indexOf("纯期权") >= 0 || s === "期权") return "is-opt";
|
if (!exId) return name;
|
||||||
if (s.indexOf("顺势") >= 0) return "is-roll";
|
return (
|
||||||
if (s.indexOf("趋势") >= 0) return "is-trend";
|
`<button type="button" class="dash-ex-link" data-dash-ex-id="${esc(exId)}" ` +
|
||||||
if (s.indexOf("关键位") >= 0) return "is-key";
|
`title="打开监控区放大查看">${name}</button>`
|
||||||
if (s.indexOf("下单") >= 0) return "is-order";
|
);
|
||||||
return "is-none";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDashboardPerpTable(lines) {
|
function priceCell(fmtVal, raw) {
|
||||||
const rows = Array.isArray(lines) ? lines : [];
|
if (fmtVal != null && String(fmtVal).trim() !== "") return esc(String(fmtVal));
|
||||||
if (!rows.length) return "";
|
if (raw != null && Number.isFinite(Number(raw))) return esc(fmt(raw, 4).replace(/\.?0+$/, ""));
|
||||||
|
return "—";
|
||||||
|
}
|
||||||
|
|
||||||
|
function floatPctText(pnl, notional) {
|
||||||
|
const n = Number(pnl);
|
||||||
|
const notion = Number(notional);
|
||||||
|
if (!Number.isFinite(n) || !Number.isFinite(notion) || Math.abs(notion) < 1e-8) return "—";
|
||||||
|
const pct = (n / Math.abs(notion)) * 100;
|
||||||
|
const abs = Math.abs(pct).toFixed(2);
|
||||||
|
if (Math.abs(pct) < 1e-9) return "0.00%";
|
||||||
|
return `${pct > 0 ? "+" : "-"}${abs}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectUnifiedPositions(accounts) {
|
||||||
|
const perp = [];
|
||||||
|
const options = [];
|
||||||
|
(Array.isArray(accounts) ? accounts : []).forEach((ac) => {
|
||||||
|
if (!accountHasOpenPositions(ac)) return;
|
||||||
|
accountPerpLines(ac).forEach((ln) => {
|
||||||
|
if (!ln) return;
|
||||||
|
perp.push({ ac: ac, ln: ln });
|
||||||
|
});
|
||||||
|
if (ac && ac.options_layout) {
|
||||||
|
(Array.isArray(ac.options_positions) ? ac.options_positions : []).forEach((p) => {
|
||||||
|
if (!p) return;
|
||||||
|
options.push({ ac: ac, p: p });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { perp: perp, options: options };
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderUnifiedPerpTable(rows) {
|
||||||
|
if (!rows.length) {
|
||||||
|
return `<div class="dash-pos-block">
|
||||||
|
<div class="dash-ac-section-label">永续持仓</div>
|
||||||
|
<div class="dash-empty dash-empty-inline">当前无永续持仓</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
const body = rows
|
const body = rows
|
||||||
.map((ln) => {
|
.map(({ ac, ln }) => {
|
||||||
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 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)) : "—";
|
||||||
const pnl = ln && ln.pnl != null ? Number(ln.pnl) : NaN;
|
const pnl = ln && ln.pnl != null ? Number(ln.pnl) : NaN;
|
||||||
|
const pct = floatPctText(pnl, ln && ln.notional_usdt);
|
||||||
return `<tr>
|
return `<tr>
|
||||||
<td><span class="dash-pos-source ${sourceBadgeClass(source)}">${esc(source)}</span></td>
|
<td>${exchangeLinkCell(ac)}</td>
|
||||||
<td>${symbol}</td>
|
<td>${symbol}</td>
|
||||||
<td>${side}</td>
|
<td>${side}</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>${contracts}</td>
|
<td>${contracts}</td>
|
||||||
<td class="${pnlClass(pnl)}">${Number.isFinite(pnl) ? pnlSigned(pnl, 2) : "—"}</td>
|
<td class="${pnlClass(pnl)}">${Number.isFinite(pnl) ? pnlSigned(pnl, 2) : "—"}</td>
|
||||||
|
<td class="${pnlClass(pnl)}">${pct}</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
})
|
})
|
||||||
.join("");
|
.join("");
|
||||||
@@ -166,7 +191,7 @@
|
|||||||
<div class="dash-table-wrap">
|
<div class="dash-table-wrap">
|
||||||
<table class="dash-table dash-pos-table">
|
<table class="dash-table dash-pos-table">
|
||||||
<thead><tr>
|
<thead><tr>
|
||||||
<th>来源</th><th>合约</th><th>方向</th><th>张数</th><th>浮盈</th>
|
<th>交易所</th><th>合约</th><th>方向</th><th>开仓价</th><th>标记价</th><th>张数</th><th>盈利金额</th><th>浮盈</th>
|
||||||
</tr></thead>
|
</tr></thead>
|
||||||
<tbody>${body}</tbody>
|
<tbody>${body}</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -188,23 +213,26 @@
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDashboardOptionsTable(positions) {
|
function renderUnifiedOptionsTable(rows) {
|
||||||
const pos = Array.isArray(positions) ? positions : [];
|
if (!rows.length) {
|
||||||
if (!pos.length) return "";
|
return `<div class="dash-pos-block dash-options-block">
|
||||||
const rows = pos
|
<div class="dash-ac-section-label">期权持仓</div>
|
||||||
.map((p) => {
|
<div class="dash-empty dash-empty-inline">当前无期权持仓</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
const body = rows
|
||||||
|
.map(({ ac, p }) => {
|
||||||
const optType =
|
const optType =
|
||||||
(p.opt_type || "").toUpperCase() === "C"
|
(p.opt_type || "").toUpperCase() === "C"
|
||||||
? "Call"
|
? "Call"
|
||||||
: (p.opt_type || "").toUpperCase() === "P"
|
: (p.opt_type || "").toUpperCase() === "P"
|
||||||
? "Put"
|
? "Put"
|
||||||
: p.opt_type || "—";
|
: p.opt_type || "—";
|
||||||
const source = String(p.source_label || p.source || "纯期权");
|
|
||||||
const target = String(p.target_monitor_text || "—");
|
const target = String(p.target_monitor_text || "—");
|
||||||
const targetCls = target && target !== "—" ? "dash-target-monitor is-on" : "dash-target-monitor";
|
const targetCls = target && target !== "—" ? "dash-target-monitor is-on" : "dash-target-monitor";
|
||||||
const net = optionsNetPnl(p);
|
const net = optionsNetPnl(p);
|
||||||
return `<tr>
|
return `<tr>
|
||||||
<td><span class="dash-pos-source ${sourceBadgeClass(source)}">${esc(source)}</span></td>
|
<td>${exchangeLinkCell(ac)}</td>
|
||||||
<td title="${esc(p.inst_id || "")}">${esc(shortDashInst(p.inst_id))}</td>
|
<td title="${esc(p.inst_id || "")}">${esc(shortDashInst(p.inst_id))}</td>
|
||||||
<td>${esc(optType)}</td>
|
<td>${esc(optType)}</td>
|
||||||
<td>${dashOptionsExpiryCd(p.exp_time_ms != null ? p.exp_time_ms : p.exp_time)}</td>
|
<td>${dashOptionsExpiryCd(p.exp_time_ms != null ? p.exp_time_ms : p.exp_time)}</td>
|
||||||
@@ -219,33 +247,17 @@
|
|||||||
<div class="dash-table-wrap dash-options-table-wrap">
|
<div class="dash-table-wrap dash-options-table-wrap">
|
||||||
<table class="dash-table dash-options-table">
|
<table class="dash-table dash-options-table">
|
||||||
<thead><tr>
|
<thead><tr>
|
||||||
<th>来源</th><th>合约</th><th>类型</th><th>到期倒计时</th><th>指数</th><th>目标监控</th><th>净盈亏</th>
|
<th>交易所</th><th>合约</th><th>类型</th><th>到期倒计时</th><th>指数</th><th>目标监控</th><th>净盈亏</th>
|
||||||
</tr></thead>
|
</tr></thead>
|
||||||
<tbody>${rows}</tbody>
|
<tbody>${body}</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderAccountPositions(ac) {
|
|
||||||
const perpLines = accountPerpLines(ac);
|
|
||||||
const optionsPositions = Array.isArray(ac && ac.options_positions) ? ac.options_positions : [];
|
|
||||||
const issues = Array.isArray(ac && ac.issues) ? ac.issues : [];
|
|
||||||
const chips = renderMonitorCountChips((ac && ac.monitor_counts) || {});
|
|
||||||
const monitorRow = chips.length
|
|
||||||
? `<div class="dash-ac-monitor-row">${chips.join("")}</div>`
|
|
||||||
: "";
|
|
||||||
const perpHtml = renderDashboardPerpTable(perpLines);
|
|
||||||
const optionsHtml = ac && ac.options_layout ? renderDashboardOptionsTable(optionsPositions) : "";
|
|
||||||
const issueHtml = issues
|
|
||||||
.map((text) => `<div class="dash-ac-remark-line dash-ac-remark-issue">${esc(text)}</div>`)
|
|
||||||
.join("");
|
|
||||||
return `${monitorRow}${perpHtml}${optionsHtml}${issueHtml}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function bindDashboardExpand() {
|
function bindDashboardExpand() {
|
||||||
if (!elAccounts) return;
|
if (!elAccounts) return;
|
||||||
elAccounts.querySelectorAll(".dash-ac-expand-btn").forEach((btn) => {
|
elAccounts.querySelectorAll("[data-dash-ex-id]").forEach((btn) => {
|
||||||
btn.addEventListener("click", (ev) => {
|
btn.addEventListener("click", (ev) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
@@ -256,44 +268,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderAccounts(accounts, threshold) {
|
function renderAccounts(accounts, threshold) {
|
||||||
const rows = (Array.isArray(accounts) ? accounts : []).filter(accountHasOpenPositions);
|
const unified = collectUnifiedPositions(accounts);
|
||||||
if (!rows.length) {
|
if (!unified.perp.length && !unified.options.length) {
|
||||||
elAccounts.innerHTML = '<div class="dash-empty">当前无持仓账户</div>';
|
elAccounts.innerHTML = '<div class="dash-empty">当前无持仓账户</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
elAccounts.innerHTML = rows
|
const alertAccounts = (Array.isArray(accounts) ? accounts : []).filter((a) => a && a.loss_alert);
|
||||||
.map((ac) => {
|
const alertNote = alertAccounts.length
|
||||||
const alert = !!ac.loss_alert;
|
? `<div class="dash-pos-alert-note">风险: ${esc(
|
||||||
const unmon = !ac.monitored;
|
alertAccounts.map((a) => a.name || "").filter(Boolean).join("、")
|
||||||
const lossPct = Number(ac.daily_loss_pct);
|
)} 单日亏损 ≥${threshold}%</div>`
|
||||||
const barW =
|
: "";
|
||||||
alert && Number.isFinite(lossPct)
|
elAccounts.innerHTML = `<article class="dash-ac-card dash-pos-unified-card">
|
||||||
? Math.min(100, (lossPct / Math.max(threshold, 1)) * 100)
|
<div class="dash-ac-pos-body">
|
||||||
: 0;
|
${renderUnifiedPerpTable(unified.perp)}
|
||||||
const badge = alert
|
${renderUnifiedOptionsTable(unified.options)}
|
||||||
? `<span class="dash-ac-badge alert">单日亏损 ≥${threshold}%</span>`
|
${alertNote}
|
||||||
: `<span class="dash-ac-badge ok">${esc(ac.status || "—")}</span>`;
|
</div>
|
||||||
const exId = ac && ac.id != null ? String(ac.id) : "";
|
</article>`;
|
||||||
const expandBtn = exId
|
|
||||||
? `<button type="button" class="dash-ac-expand-btn" data-dash-ex-id="${esc(exId)}" title="放大查看监控详情" aria-label="放大查看监控详情">` +
|
|
||||||
`<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M15 3h6v6h-2V6.41l-7.29 7.3-1.42-1.42 7.3-7.29H15V3zM3 9h2v10h10v2H3V9z"/></svg>` +
|
|
||||||
`</button>`
|
|
||||||
: "";
|
|
||||||
const lossBar =
|
|
||||||
alert && barW > 0
|
|
||||||
? `<div class="dash-loss-bar" title="占资金合计 ${fmt(lossPct, 2)}%"><i style="width:${barW}%"></i></div>`
|
|
||||||
: "";
|
|
||||||
const cardCls = ac.options_layout ? " dash-ac-card-options" : "";
|
|
||||||
return `<article class="dash-ac-card dash-ac-card-pos-only${cardCls}${alert ? " is-alert" : ""}${unmon ? " is-unmon" : ""}">
|
|
||||||
<div class="dash-ac-top">
|
|
||||||
<div class="dash-ac-name">${esc(ac.name || "—")}</div>
|
|
||||||
<div class="dash-ac-top-actions">${badge}${expandBtn}</div>
|
|
||||||
</div>
|
|
||||||
${lossBar}
|
|
||||||
<div class="dash-ac-pos-body">${renderAccountPositions(ac)}</div>
|
|
||||||
</article>`;
|
|
||||||
})
|
|
||||||
.join("");
|
|
||||||
bindDashboardExpand();
|
bindDashboardExpand();
|
||||||
if (window.OptionsExpiryCountdown && OptionsExpiryCountdown.ensureTimer) {
|
if (window.OptionsExpiryCountdown && OptionsExpiryCountdown.ensureTimer) {
|
||||||
OptionsExpiryCountdown.ensureTimer();
|
OptionsExpiryCountdown.ensureTimer();
|
||||||
|
|||||||
@@ -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=20260717-dash-kpi-spread" />
|
<link rel="stylesheet" href="/assets/dashboard.css?v=20260720-dash-pos-unified" />
|
||||||
</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=20260717-dash-kpi-merge"></script>
|
<script src="/assets/dashboard.js?v=20260720-dash-pos-unified"></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