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 期权只读聚合 ── */
|
/* ── 监控页:OKX 期权只读聚合 ── */
|
||||||
.hub-options-title {
|
.hub-options-title {
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
|
|||||||
@@ -3404,79 +3404,106 @@
|
|||||||
</div>`;
|
</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 "";
|
if (!showAccountPnlPref()) return "";
|
||||||
const upnl = ag.total_unrealized_pnl;
|
|
||||||
return `<div class="stat-row">
|
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(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(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(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 class="stat-box"><div class="stat-label">浮盈合计</div><div class="stat-value ${pnlCls(upnl)}">${fmt(upnl, 2)}</div></div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderAccountStatRow(row, ag) {
|
||||||
|
return renderStatRow(row.funding_usdt, row.trading_usdt, ag.total_unrealized_pnl);
|
||||||
|
}
|
||||||
|
|
||||||
function shortOptionsInst(instId) {
|
function shortOptionsInst(instId) {
|
||||||
const s = String(instId || "");
|
const s = String(instId || "");
|
||||||
if (s.length <= 22) return s;
|
if (s.length <= 22) return s;
|
||||||
return s.slice(0, 10) + "…" + s.slice(-8);
|
return s.slice(0, 10) + "…" + s.slice(-8);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderOptionsMonitorSection(row) {
|
function renderOptionsPositionsTable(pos) {
|
||||||
const caps = row.capabilities || [];
|
if (!pos.length) return '<div class="empty-hint">暂无期权持仓</div>';
|
||||||
if (!caps.includes("options")) return "";
|
let html = '<div class="table-wrap hub-options-table-wrap"><table class="hub-options-table"><thead><tr>';
|
||||||
const opt = row.options || {};
|
html += "<th>合约</th><th>类型</th><th>张数</th><th>标记</th><th>浮盈</th><th>浮盈%</th>";
|
||||||
let html = "";
|
html += "</tr></thead><tbody>";
|
||||||
if (opt.enabled === false) {
|
pos.forEach((p) => {
|
||||||
html += '<div class="section-title hub-options-title">期权 · 主账户</div>';
|
const optType = (p.opt_type || "").toUpperCase() === "C" ? "Call" : (p.opt_type || "").toUpperCase() === "P" ? "Put" : (p.opt_type || "—");
|
||||||
html += '<div class="empty-hint">期权未启用(OKX_OPTIONS_ENABLED)</div>';
|
html += `<tr>
|
||||||
return html;
|
<td><code class="hub-options-inst" title="${esc(p.inst_id || "")}">${esc(shortOptionsInst(p.inst_id))}</code></td>
|
||||||
}
|
<td>${esc(optType)}</td>
|
||||||
if (opt.ok === false) {
|
<td>${esc(p.pos)}</td>
|
||||||
html += '<div class="section-title hub-options-title">期权 · 主账户</div>';
|
<td>${fmt(p.mark_px, 4)}</td>
|
||||||
html += `<div class="err">${esc(opt.msg || "期权数据不可用")}</div>`;
|
<td class="${pnlCls(p.upl)}">${fmt(p.upl, 4)}</td>
|
||||||
return html;
|
<td class="${pnlCls(p.upl)}">${p.upl_ratio_pct != null ? esc(p.upl_ratio_pct) + "%" : "—"}</td>
|
||||||
}
|
</tr>`;
|
||||||
const pos = Array.isArray(opt.positions) ? opt.positions : [];
|
});
|
||||||
const upl = opt.upl_total_usdc;
|
html += "</tbody></table></div>";
|
||||||
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>`;
|
|
||||||
}
|
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderGridBody(row, ag, pos, hm, flaskOk, keys, orders, trends, rolls, kmap) {
|
function renderOptionsMonitorSection(row) {
|
||||||
const tickMap = buildPriceTickMap(row);
|
if (!rowHasOptionsLayout(row)) return "";
|
||||||
const intraday = isIntradayDisciplineRow(row);
|
const opt = row.options || {};
|
||||||
let inner = renderAccountStatRow(row, ag);
|
let html = '<div class="hub-monitor-block hub-monitor-options">';
|
||||||
inner += `<div class="section-title">交易所持仓 · ${pos.length} 仓</div>`;
|
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) {
|
if (pos.length) {
|
||||||
inner += renderGridPositionsTable(
|
html += renderGridPositionsTable(
|
||||||
row.id,
|
row.id,
|
||||||
row.key || row.id,
|
row.key || row.id,
|
||||||
pos,
|
pos,
|
||||||
@@ -3486,9 +3513,26 @@
|
|||||||
intraday
|
intraday
|
||||||
);
|
);
|
||||||
} else {
|
} 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 += renderCardStrategyStats(row, hm, flaskOk);
|
||||||
inner += intraday
|
inner += intraday
|
||||||
? `<div class="card-expand-hint">日内纪律:禁手动平仓/改委托 · 整点强制清仓${row.force_close && row.force_close.enabled ? " · " + esc(row.force_close.label || "强制清仓") : ""}</div>`
|
? `<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>`;
|
html += `<div class="err">${esc(row.error || ag.error || "子代理不可用")}</div>`;
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
html += renderAccountStatRow(row, ag);
|
|
||||||
const posCount = pos.length;
|
const posCount = pos.length;
|
||||||
const posListCls = hubPosListCountClass(posCount);
|
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}">`;
|
html += `<div class="hub-pos-list ${posListCls}" data-pos-count="${posCount}">`;
|
||||||
if (posCount) {
|
if (posCount) {
|
||||||
pos.forEach((p) => {
|
pos.forEach((p) => {
|
||||||
@@ -3553,6 +3601,9 @@
|
|||||||
html += '<div class="pos-empty">暂无持仓</div>';
|
html += '<div class="pos-empty">暂无持仓</div>';
|
||||||
}
|
}
|
||||||
html += "</div>";
|
html += "</div>";
|
||||||
|
if (rowHasOptionsLayout(row)) {
|
||||||
|
html += "</div>";
|
||||||
|
}
|
||||||
html += renderOptionsMonitorSection(row);
|
html += renderOptionsMonitorSection(row);
|
||||||
html += '<div class="hub-fs-sections-grid">';
|
html += '<div class="hub-fs-sections-grid">';
|
||||||
if ((row.capabilities || []).includes("key")) {
|
if ((row.capabilities || []).includes("key")) {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<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'" />
|
<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>
|
<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/trade_stats_calendar.css?v=3" />
|
||||||
<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>
|
||||||
@@ -1151,6 +1151,6 @@
|
|||||||
<script src="/assets/ai_review_render.js?v=3"></script>
|
<script src="/assets/ai_review_render.js?v=3"></script>
|
||||||
<script src="/assets/time_close_ui.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/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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user