Hub monitor card: split perpetual and options blocks when options enabled.

Show matching stat rows and position sections for each account type; keep the standard single-block layout for exchanges without options.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-07 13:17:48 +08:00
parent 4a3afdfb48
commit 5e66600727
3 changed files with 153 additions and 63 deletions
+39
View File
@@ -7896,6 +7896,45 @@ body.funds-fullscreen-open {
}
}
/* ── 监控页:永续 / 期权分块 ── */
.hub-monitor-block {
margin-bottom: 4px;
}
.hub-monitor-block-label {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.06em;
color: var(--text);
margin: 0 0 8px;
}
.hub-monitor-perp .section-title:first-of-type {
margin-top: 0;
}
.hub-monitor-options {
margin-top: 14px;
padding-top: 12px;
border-top: 1px dashed var(--border-soft);
}
.hub-monitor-options .hub-monitor-block-label {
color: var(--accent);
}
.hub-monitor-options .section-title.hub-options-title {
margin-top: 0;
}
html[data-theme="light"] .hub-monitor-block-label {
color: #142232;
}
html[data-theme="light"] .hub-monitor-options .hub-monitor-block-label {
color: #006e9a;
}
/* ── 监控页:OKX 期权只读聚合 ── */
.hub-options-title {
margin-top: 14px;
+112 -61
View File
@@ -3404,79 +3404,106 @@
</div>`;
}
function renderAccountStatRow(row, ag) {
function rowHasOptionsLayout(row) {
if (!(row.capabilities || []).includes("options")) return false;
const opt = row.options || {};
return opt.enabled !== false;
}
function sumUsdtEquiv(a, b) {
const vals = [a, b]
.map((v) => (v == null || v === "" ? null : Number(v)))
.filter((v) => v != null && Number.isFinite(v));
if (!vals.length) return null;
return vals.reduce((s, v) => s + v, 0);
}
function optionsBalanceFields(opt) {
const bal = (opt && opt.balances) || opt || {};
return {
funding: sumUsdtEquiv(bal.funding_usdt, bal.funding_usdc),
trading: sumUsdtEquiv(bal.trading_usdt, bal.trading_usdc),
upl: opt && opt.upl_total_usdc != null && Number.isFinite(Number(opt.upl_total_usdc))
? Number(opt.upl_total_usdc)
: null,
};
}
function renderStatRow(funding, trading, upnl) {
if (!showAccountPnlPref()) return "";
const upnl = ag.total_unrealized_pnl;
return `<div class="stat-row">
<div class="stat-box"><div class="stat-label">资金账户</div><div class="stat-value">${fmt(row.funding_usdt, 2)} <small style="font-size:12px;color:var(--muted)">U</small></div></div>
<div class="stat-box"><div class="stat-label">交易账户</div><div class="stat-value">${fmt(row.trading_usdt, 2)} <small style="font-size:12px;color:var(--muted)">U</small></div></div>
<div class="stat-box"><div class="stat-label">资金账户</div><div class="stat-value">${fmt(funding, 2)} <small style="font-size:12px;color:var(--muted)">U</small></div></div>
<div class="stat-box"><div class="stat-label">交易账户</div><div class="stat-value">${fmt(trading, 2)} <small style="font-size:12px;color:var(--muted)">U</small></div></div>
<div class="stat-box"><div class="stat-label">浮盈合计</div><div class="stat-value ${pnlCls(upnl)}">${fmt(upnl, 2)}</div></div>
</div>`;
}
function renderAccountStatRow(row, ag) {
return renderStatRow(row.funding_usdt, row.trading_usdt, ag.total_unrealized_pnl);
}
function shortOptionsInst(instId) {
const s = String(instId || "");
if (s.length <= 22) return s;
return s.slice(0, 10) + "…" + s.slice(-8);
}
function renderOptionsMonitorSection(row) {
const caps = row.capabilities || [];
if (!caps.includes("options")) return "";
const opt = row.options || {};
let html = "";
if (opt.enabled === false) {
html += '<div class="section-title hub-options-title">期权 · 主账户</div>';
html += '<div class="empty-hint">期权未启用(OKX_OPTIONS_ENABLED</div>';
return html;
}
if (opt.ok === false) {
html += '<div class="section-title hub-options-title">期权 · 主账户</div>';
html += `<div class="err">${esc(opt.msg || "期权数据不可用")}</div>`;
return html;
}
const pos = Array.isArray(opt.positions) ? opt.positions : [];
const upl = opt.upl_total_usdc;
const tradingUsdc = opt.trading_usdc != null ? opt.trading_usdc : (opt.balances && opt.balances.trading_usdc);
const stats = opt.stats || {};
html += `<div class="section-title hub-options-title">期权 · 主账户 · ${pos.length} 仓</div>`;
html += `<div class="hub-options-summary">
<span>交易户 <strong>${fmt(tradingUsdc, 2)}</strong> USDC</span>
<span>浮盈 <strong class="${pnlCls(upl)}">${fmt(upl, 4)}</strong> USDC</span>
<span>已平 ${esc(stats.total_closed != null ? stats.total_closed : "—")} · 胜率 ${esc(stats.win_rate != null ? stats.win_rate + "%" : "—")}</span>
</div>`;
if (pos.length) {
html += '<div class="table-wrap hub-options-table-wrap"><table class="hub-options-table"><thead><tr>';
html += "<th>合约</th><th>类型</th><th>张数</th><th>标记</th><th>浮盈</th><th>浮盈%</th>";
html += "</tr></thead><tbody>";
pos.forEach((p) => {
const optType = (p.opt_type || "").toUpperCase() === "C" ? "Call" : (p.opt_type || "").toUpperCase() === "P" ? "Put" : (p.opt_type || "—");
html += `<tr>
<td><code class="hub-options-inst" title="${esc(p.inst_id || "")}">${esc(shortOptionsInst(p.inst_id))}</code></td>
<td>${esc(optType)}</td>
<td>${esc(p.pos)}</td>
<td>${fmt(p.mark_px, 4)}</td>
<td class="${pnlCls(p.upl)}">${fmt(p.upl, 4)}</td>
<td class="${pnlCls(p.upl)}">${p.upl_ratio_pct != null ? esc(p.upl_ratio_pct) + "%" : "—"}</td>
</tr>`;
});
html += "</tbody></table></div>";
} else {
html += '<div class="empty-hint">暂无期权持仓</div>';
}
if (row.flask_url_browser || row.flask_url) {
html += `<div class="hub-options-actions"><a class="btn-link btn-open-instance" href="#" data-ex-id="${esc(row.id)}" data-next="/options">打开期权页</a></div>`;
}
function renderOptionsPositionsTable(pos) {
if (!pos.length) return '<div class="empty-hint">暂无期权持仓</div>';
let html = '<div class="table-wrap hub-options-table-wrap"><table class="hub-options-table"><thead><tr>';
html += "<th>合约</th><th>类型</th><th>张数</th><th>标记</th><th>浮盈</th><th>浮盈%</th>";
html += "</tr></thead><tbody>";
pos.forEach((p) => {
const optType = (p.opt_type || "").toUpperCase() === "C" ? "Call" : (p.opt_type || "").toUpperCase() === "P" ? "Put" : (p.opt_type || "—");
html += `<tr>
<td><code class="hub-options-inst" title="${esc(p.inst_id || "")}">${esc(shortOptionsInst(p.inst_id))}</code></td>
<td>${esc(optType)}</td>
<td>${esc(p.pos)}</td>
<td>${fmt(p.mark_px, 4)}</td>
<td class="${pnlCls(p.upl)}">${fmt(p.upl, 4)}</td>
<td class="${pnlCls(p.upl)}">${p.upl_ratio_pct != null ? esc(p.upl_ratio_pct) + "%" : "—"}</td>
</tr>`;
});
html += "</tbody></table></div>";
return html;
}
function renderGridBody(row, ag, pos, hm, flaskOk, keys, orders, trends, rolls, kmap) {
const tickMap = buildPriceTickMap(row);
const intraday = isIntradayDisciplineRow(row);
let inner = renderAccountStatRow(row, ag);
inner += `<div class="section-title">交易所持仓 · ${pos.length}</div>`;
function renderOptionsMonitorSection(row) {
if (!rowHasOptionsLayout(row)) return "";
const opt = row.options || {};
let html = '<div class="hub-monitor-block hub-monitor-options">';
html += '<div class="hub-monitor-block-label">期权账户</div>';
if (opt.enabled === false) {
html += renderStatRow(null, null, null);
html += '<div class="section-title hub-options-title">期权持仓</div>';
html += '<div class="empty-hint">期权未启用(OKX_OPTIONS_ENABLED</div>';
html += "</div>";
return html;
}
if (opt.ok === false) {
html += renderStatRow(null, null, null);
html += '<div class="section-title hub-options-title">期权持仓</div>';
html += `<div class="err">${esc(opt.msg || "期权数据不可用")}</div>`;
html += "</div>";
return html;
}
const pos = Array.isArray(opt.positions) ? opt.positions : [];
const bal = optionsBalanceFields(opt);
html += renderStatRow(bal.funding, bal.trading, bal.upl);
html += `<div class="section-title hub-options-title">期权持仓 · ${pos.length} 仓</div>`;
html += renderOptionsPositionsTable(pos);
if (row.flask_url_browser || row.flask_url) {
html += `<div class="hub-options-actions"><a class="btn-link btn-open-instance" href="#" data-ex-id="${esc(row.id)}" data-next="/options">打开期权页</a></div>`;
}
html += "</div>";
return html;
}
function renderPerpetualPositionsSection(row, ag, pos, orders, trends, tickMap, intraday) {
let html = "";
html += `<div class="section-title">交易所持仓 · ${pos.length} 仓</div>`;
if (pos.length) {
inner += renderGridPositionsTable(
html += renderGridPositionsTable(
row.id,
row.key || row.id,
pos,
@@ -3486,9 +3513,26 @@
intraday
);
} else {
inner += '<div class="empty-hint">无持仓</div>';
html += '<div class="empty-hint">无持仓</div>';
}
return html;
}
function renderGridBody(row, ag, pos, hm, flaskOk, keys, orders, trends, rolls, kmap) {
const tickMap = buildPriceTickMap(row);
const intraday = isIntradayDisciplineRow(row);
let inner = "";
if (rowHasOptionsLayout(row)) {
inner += '<div class="hub-monitor-block hub-monitor-perp">';
inner += '<div class="hub-monitor-block-label">永续账户</div>';
inner += renderAccountStatRow(row, ag);
inner += renderPerpetualPositionsSection(row, ag, pos, orders, trends, tickMap, intraday);
inner += "</div>";
inner += renderOptionsMonitorSection(row);
} else {
inner += renderAccountStatRow(row, ag);
inner += renderPerpetualPositionsSection(row, ag, pos, orders, trends, tickMap, intraday);
}
inner += renderOptionsMonitorSection(row);
inner += renderCardStrategyStats(row, hm, flaskOk);
inner += intraday
? `<div class="card-expand-hint">日内纪律:禁手动平仓/改委托 · 整点强制清仓${row.force_close && row.force_close.enabled ? " · " + esc(row.force_close.label || "强制清仓") : ""}</div>`
@@ -3532,10 +3576,14 @@
html += `<div class="err">${esc(row.error || ag.error || "子代理不可用")}</div>`;
return html;
}
html += renderAccountStatRow(row, ag);
const posCount = pos.length;
const posListCls = hubPosListCountClass(posCount);
html += `<div class="section-title">持仓(${posCount} 仓 · 每币种一卡)</div>`;
if (rowHasOptionsLayout(row)) {
html += '<div class="hub-monitor-block hub-monitor-perp">';
html += '<div class="hub-monitor-block-label">永续账户</div>';
}
html += renderAccountStatRow(row, ag);
html += `<div class="section-title">交易所持仓(${posCount} 仓 · 每币种一卡)</div>`;
html += `<div class="hub-pos-list ${posListCls}" data-pos-count="${posCount}">`;
if (posCount) {
pos.forEach((p) => {
@@ -3553,6 +3601,9 @@
html += '<div class="pos-empty">暂无持仓</div>';
}
html += "</div>";
if (rowHasOptionsLayout(row)) {
html += "</div>";
}
html += renderOptionsMonitorSection(row);
html += '<div class="hub-fs-sections-grid">';
if ((row.capabilities || []).includes("key")) {
+2 -2
View File
@@ -15,7 +15,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
<link rel="stylesheet" href="/assets/app.css?v=20260707-hub-options" />
<link rel="stylesheet" href="/assets/app.css?v=20260707-hub-options-split" />
<link rel="stylesheet" href="/assets/trade_stats_calendar.css?v=3" />
<link rel="stylesheet" href="/assets/account_risk_badge.css?v=4" />
<script src="/assets/account_risk_badge.js?v=4"></script>
@@ -1151,6 +1151,6 @@
<script src="/assets/ai_review_render.js?v=3"></script>
<script src="/assets/time_close_ui.js?v=3"></script>
<script src="/assets/backup.js?v=1"></script>
<script src="/assets/app.js?v=20260707-hub-options"></script>
<script src="/assets/app.js?v=20260707-hub-options-split"></script>
</body>
</html>