Hub monitor: read-only OKX options positions aggregation (Phase B).

Expose /api/hub/options/snapshot from OKX instance and render options summary on hub monitor cards when the options capability is enabled.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-07 12:55:35 +08:00
parent 11a50c02cd
commit 6cc3382526
10 changed files with 299 additions and 6 deletions
+63
View File
@@ -7896,3 +7896,66 @@ body.funds-fullscreen-open {
}
}
/* ── 监控页:OKX 期权只读聚合 ── */
.hub-options-title {
margin-top: 14px;
}
.hub-options-summary {
display: flex;
flex-wrap: wrap;
gap: 8px 14px;
font-size: 0.78rem;
color: var(--muted);
margin: 0 0 8px;
}
.hub-options-summary strong {
color: var(--text);
font-weight: 600;
}
.hub-options-table-wrap {
margin-bottom: 8px;
}
.hub-options-table {
width: 100%;
border-collapse: collapse;
font-size: 0.76rem;
}
.hub-options-table th,
.hub-options-table td {
padding: 6px 8px;
text-align: left;
border-bottom: 1px solid var(--border);
}
.hub-options-table th {
color: var(--muted);
font-weight: 600;
font-size: 0.72rem;
}
.hub-options-inst {
font-size: 0.7rem;
word-break: break-all;
}
.hub-options-actions {
margin-top: 6px;
}
html[data-theme="light"] .hub-options-table th {
color: #3a5068;
}
html[data-theme="light"] .hub-options-summary {
color: #3a5068;
}
html[data-theme="light"] .hub-options-summary strong {
color: #142232;
}
+74 -1
View File
@@ -3414,6 +3414,62 @@
</div>`;
}
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>`;
}
return html;
}
function renderGridBody(row, ag, pos, hm, flaskOk, keys, orders, trends, rolls, kmap) {
const tickMap = buildPriceTickMap(row);
const intraday = isIntradayDisciplineRow(row);
@@ -3432,6 +3488,7 @@
} else {
inner += '<div class="empty-hint">无持仓</div>';
}
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>`
@@ -3467,6 +3524,7 @@
${flaskOpen ? `<a class="btn-link btn-open-instance" href="#" data-ex-id="${esc(row.id)}" data-next="/trade">下单</a>` : ""}
${flaskOpen ? `<a class="btn-link btn-open-instance" href="#" data-ex-id="${esc(row.id)}" data-next="/key_monitor">监控位</a>` : ""}
${flaskOpen ? `<a class="btn-link btn-open-instance" href="#" data-ex-id="${esc(row.id)}" data-next="/records">复盘</a>` : ""}
${flaskOpen && (row.capabilities || []).includes("options") ? `<a class="btn-link btn-open-instance" href="#" data-ex-id="${esc(row.id)}" data-next="/options">期权</a>` : ""}
${intraday ? "" : `<button type="button" class="danger btn-close-ex" data-id="${esc(row.id)}">全平</button>`}
</div>
</div>`;
@@ -3495,6 +3553,7 @@
html += '<div class="pos-empty">暂无持仓</div>';
}
html += "</div>";
html += renderOptionsMonitorSection(row);
html += '<div class="hub-fs-sections-grid">';
if ((row.capabilities || []).includes("key")) {
if (!flaskOk) {
@@ -3739,6 +3798,13 @@
const tsShort = ts ? ts.slice(-8) : "—";
const posLine =
openCount > 0 ? `${openCount}仓 · ${alert.summary}` : alert.summary;
const opt = row.options || {};
const optCount =
opt.enabled !== false && opt.ok !== false
? Number(opt.position_count != null ? opt.position_count : (opt.positions || []).length)
: 0;
const posLineWithOpt =
optCount > 0 ? `${posLine} · 期权${optCount}` : posLine;
const hm = row.hub_monitor || {};
const flaskOk = row.flask_ok !== false && hm.ok !== false;
const strategyStats = renderCardStrategyStats(row, hm, flaskOk);
@@ -3754,7 +3820,7 @@
? `<div class="hub-tile-pnl ${pnlCls(upnl)}">${fmt(upnl, 2)} <small>U</small></div>`
: ""
}
<div class="hub-tile-meta">${esc(posLine)}</div>
<div class="hub-tile-meta">${esc(posLineWithOpt)}</div>
${strategyStats}
<div class="hub-tile-foot">UPD ${esc(tsShort)}</div>
</div>
@@ -3801,6 +3867,10 @@
const openReview = flaskOpen
? `<a class="btn-link btn-open-instance" href="#" data-ex-id="${esc(row.id)}" data-next="/records">复盘</a>`
: "";
const openOptions =
flaskOpen && (row.capabilities || []).includes("options")
? `<a class="btn-link btn-open-instance" href="#" data-ex-id="${esc(row.id)}" data-next="/options">期权</a>`
: "";
const intraday = isIntradayDisciplineRow(row);
const fcHeadBadge = intraday ? forceCloseHeadBadgeHtml(row.force_close) : "";
return `<div class="card ${cardCls}" data-ex-id="${esc(row.id)}">
@@ -3816,6 +3886,7 @@
${openFlask}
${openTrade}
${openKey}
${openOptions}
${openReview}
${intraday ? "" : `<button type="button" class="danger btn-close-ex" data-id="${esc(row.id)}">全平</button>`}
</div>
@@ -4253,6 +4324,7 @@
<div class="cap-chips">
<label><input type="checkbox" class="cap-key" ${caps.includes("key") ? "checked" : ""}/> 监控关键位</label>
<label><input type="checkbox" class="cap-trend" ${caps.includes("trend") ? "checked" : ""}/> 监控趋势计划</label>
<label><input type="checkbox" class="cap-options" ${caps.includes("options") ? "checked" : ""}/> 监控期权</label>
</div>
<div class="settings-card-foot">
<div class="field"><label>id</label><input class="ex-id" value="${esc(ex.id || "")}" /></div>
@@ -4308,6 +4380,7 @@
const caps = [];
if (card.querySelector(".cap-key").checked) caps.push("key");
if (card.querySelector(".cap-trend").checked) caps.push("trend");
if (card.querySelector(".cap-options") && card.querySelector(".cap-options").checked) caps.push("options");
const id = card.querySelector(".ex-id").value.trim();
const stableKey = (card.dataset.key || id).trim();
return {
+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=20260706-strategy-scroll" />
<link rel="stylesheet" href="/assets/app.css?v=20260707-hub-options" />
<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=20260704-monitor-stats-v2"></script>
<script src="/assets/app.js?v=20260707-hub-options"></script>
</body>
</html>