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:
@@ -1627,7 +1627,7 @@ def api_settings_meta():
|
||||
return {
|
||||
"env_disabled_ids": sorted(env_force_disabled_ids()),
|
||||
"hub_bridge_token_set": bool(HUB_BRIDGE_TOKEN),
|
||||
"capability_options": ["key", "trend"],
|
||||
"capability_options": ["key", "trend", "options"],
|
||||
"public_origin": f"{po[0]}://{po[1]}" if po else None,
|
||||
"public_origin_hint": (
|
||||
"未设置 HUB_PUBLIC_ORIGIN 时,复盘链接若为 127.0.0.1,仅服务器本机浏览器可打开"
|
||||
@@ -2306,6 +2306,9 @@ async def _fetch_exchange_flask_bundle(
|
||||
snap = results[2] if has_flask and len(results) > 2 else None
|
||||
account = results[3] if has_flask and len(results) > 3 else None
|
||||
trades_today = results[4] if has_flask and day and len(results) > 4 else None
|
||||
options_snap = None
|
||||
if has_flask and "options" in caps:
|
||||
options_snap = await _fetch_flask_json(client, ex, "/api/hub/options/snapshot")
|
||||
key_prices = None
|
||||
want_prices = HUB_BOARD_KEY_PRICES and "key" in caps
|
||||
if want_prices and isinstance(snap, dict):
|
||||
@@ -2317,6 +2320,7 @@ async def _fetch_exchange_flask_bundle(
|
||||
snap if isinstance(snap, dict) else None,
|
||||
account if isinstance(account, dict) else None,
|
||||
trades_today if isinstance(trades_today, dict) else None,
|
||||
options_snap if isinstance(options_snap, dict) else None,
|
||||
)
|
||||
|
||||
|
||||
@@ -2342,7 +2346,7 @@ def _day_stats_from_trades_body(body: dict | None) -> dict:
|
||||
async def _assemble_board_row(
|
||||
client: httpx.AsyncClient, ex: dict, agent_row: dict, *, trading_day: str
|
||||
) -> dict:
|
||||
hub_mon, meta, key_prices, snap, account, trades_today = await _fetch_exchange_flask_bundle(
|
||||
hub_mon, meta, key_prices, snap, account, trades_today, options_snap = await _fetch_exchange_flask_bundle(
|
||||
client, ex, trading_day=trading_day
|
||||
)
|
||||
if isinstance(hub_mon, dict):
|
||||
@@ -2372,6 +2376,7 @@ async def _assemble_board_row(
|
||||
"account_ok": acct_ok,
|
||||
"day_stats": _day_stats_from_trades_body(trades_today),
|
||||
"force_close": snap.get("force_close") if isinstance(snap, dict) else None,
|
||||
"options": options_snap,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ DEFAULT_EXCHANGES = [
|
||||
"flask_url": "http://127.0.0.1:5004",
|
||||
"review_url": "http://127.0.0.1:5004/records",
|
||||
"enabled": True,
|
||||
"capabilities": ["key", "trend"],
|
||||
"capabilities": ["key", "trend", "options"],
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
@@ -109,6 +109,11 @@ def load_settings() -> dict:
|
||||
ex["env_disabled"] = True
|
||||
else:
|
||||
ex.setdefault("env_disabled", False)
|
||||
if ex.get("key") == "okx":
|
||||
caps = list(ex.get("capabilities") or [])
|
||||
if "options" not in caps:
|
||||
caps.append("options")
|
||||
ex["capabilities"] = caps
|
||||
return data
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user