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:
@@ -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;
|
||||
|
||||
@@ -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")) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user