/** * 中控数据看板:后端 SSE 推送版本号,前端拉快照刷新(无轮询闪烁). */ (function () { const page = document.getElementById("page-dashboard"); if (!page) return; let dashEventSource = null; let dashReconnectTimer = null; let localDashVersion = 0; let inited = false; let loading = false; const elStatus = document.getElementById("dash-status"); const elBanner = document.getElementById("dash-alert-banner"); const elBannerText = document.getElementById("dash-alert-banner-text"); const elKpi = document.getElementById("dash-kpi-row"); const elAccounts = document.getElementById("dash-accounts"); const elTrades = document.getElementById("dash-trades-body"); const elUpdated = document.getElementById("dash-updated-at"); const elDay = document.getElementById("dash-trading-day"); const btnRefresh = document.getElementById("dash-btn-refresh"); function fmt(n, d) { if (n == null || n === "" || !Number.isFinite(Number(n))) return "—"; return Number(n).toFixed(d == null ? 2 : d); } function pnlClass(v) { const n = Number(v); if (!Number.isFinite(n) || Math.abs(n) < 1e-9) return ""; return n > 0 ? "pos" : "neg"; } function pnlSigned(v, digits) { const n = Number(v); if (!Number.isFinite(n)) return "—"; const abs = fmt(Math.abs(n), digits); if (Math.abs(n) < 1e-9) return `${abs}U`; return `${n > 0 ? "+" : "-"}${abs}U`; } function esc(s) { return String(s == null ? "" : s) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function setStatus(msg, isErr) { if (!elStatus) return; elStatus.textContent = msg || ""; elStatus.className = "dash-status" + (isErr ? " err" : ""); } function renderKpi(totals) { if (!elKpi || !totals) return; const closed = Number(totals.total_pnl_u); const floating = Number(totals.float_pnl_u); const funding = totals.total_funding_usdt; const trading = totals.total_trading_usdt; const funds = funding != null && trading != null ? Number(funding) + Number(trading) : NaN; const totalPos = Number(totals.open_position_count) || 0; const optPos = Number(totals.options_open_position_count) || 0; const perpPos = totals.perpetual_open_position_count != null ? Number(totals.perpetual_open_position_count) || 0 : Math.max(0, totalPos - optPos); const items = [ kpiItem("交易日", esc(totals.trading_day || "—")), kpiItem("资金合计", Number.isFinite(funds) ? `${fmt(funds, 2)}U` : "—"), kpiItem("总持仓数量", `${totalPos}`), kpiItem("期权持仓", `${optPos}`), kpiItem("永续持仓", `${perpPos}`), kpiItem("平仓数量", `${totals.closed_count || 0}`), kpiItem("平仓盈亏", pnlSigned(closed, 2), pnlClass(closed)), kpiItem("浮盈亏", pnlSigned(floating, 2), pnlClass(floating)), ]; elKpi.innerHTML = `
${items.join("")}
`; } function kpiItem(label, value, valCls) { return `
${esc(label)}
${value}
`; } function renderMonitorCountChips(counts) { const mc = counts || {}; const chips = []; const keys = Number(mc.keys) || 0; const orders = Number(mc.orders) || 0; const trends = Number(mc.trends) || 0; const rolls = Number(mc.rolls) || 0; if (keys > 0) chips.push(`关键位 ${keys}`); if (orders > 0) { chips.push(`下单监控 ${orders}`); } if (trends > 0) chips.push(`趋势回调 ${trends}`); if (rolls > 0) chips.push(`顺势加仓 ${rolls}`); return chips; } function dashOptionsExpiryCd(expMs) { const ms = expMs != null && expMs !== "" ? String(expMs) : ""; if (!ms) return "—"; return ``; } function shortDashInst(instId) { const s = String(instId || ""); if (s.length <= 18) return s; return s.slice(0, 8) + "…" + s.slice(-6); } function accountPerpLines(ac) { const positions = Array.isArray(ac && ac.position_lines) ? ac.position_lines : []; if (ac && ac.options_layout) { return positions.filter((ln) => (ln && ln.kind) !== "options"); } return positions; } function accountHasOpenPositions(ac) { const perp = accountPerpLines(ac); const optionsPositions = Array.isArray(ac && ac.options_positions) ? ac.options_positions : []; return perp.length > 0 || (ac && ac.options_layout && optionsPositions.length > 0); } function sourceBadgeClass(source) { const s = String(source || ""); if (s.indexOf("对冲") >= 0) return "is-hedge"; if (s.indexOf("纯期权") >= 0 || s === "期权") return "is-opt"; if (s.indexOf("顺势") >= 0) return "is-roll"; if (s.indexOf("趋势") >= 0) return "is-trend"; if (s.indexOf("关键位") >= 0) return "is-key"; if (s.indexOf("下单") >= 0) return "is-order"; return "is-none"; } function renderDashboardPerpTable(lines) { const rows = Array.isArray(lines) ? lines : []; if (!rows.length) return ""; const body = rows .map((ln) => { const source = String((ln && ln.source) || "—"); const symbol = esc((ln && (ln.symbol || ln.text)) || "—"); const side = esc((ln && ln.side) || "—"); const contracts = ln && ln.contracts != null && ln.contracts !== "" ? esc(String(ln.contracts)) : "—"; const pnl = ln && ln.pnl != null ? Number(ln.pnl) : NaN; return ` ${esc(source)} ${symbol} ${side} ${contracts} ${Number.isFinite(pnl) ? pnlSigned(pnl, 2) : "—"} `; }) .join(""); return `
永续持仓
${body}
来源合约方向张数浮盈
`; } function optionsNetPnl(p) { if (!p || typeof p !== "object") return null; if (p.net_pnl != null && Number.isFinite(Number(p.net_pnl))) return Number(p.net_pnl); const preview = p.close_preview || {}; if (preview.estimated_pnl != null && Number.isFinite(Number(preview.estimated_pnl))) { return Number(preview.estimated_pnl); } if (preview.total_received != null && p.premium_paid != null) { const n = Number(preview.total_received) - Number(p.premium_paid); return Number.isFinite(n) ? n : null; } return null; } function renderDashboardOptionsTable(positions) { const pos = Array.isArray(positions) ? positions : []; if (!pos.length) return ""; const rows = pos .map((p) => { const optType = (p.opt_type || "").toUpperCase() === "C" ? "Call" : (p.opt_type || "").toUpperCase() === "P" ? "Put" : p.opt_type || "—"; const source = String(p.source_label || p.source || "纯期权"); const target = String(p.target_monitor_text || "—"); const targetCls = target && target !== "—" ? "dash-target-monitor is-on" : "dash-target-monitor"; const net = optionsNetPnl(p); return ` ${esc(source)} ${esc(shortDashInst(p.inst_id))} ${esc(optType)} ${dashOptionsExpiryCd(p.exp_time_ms != null ? p.exp_time_ms : p.exp_time)} ${p.idx_px != null ? fmt(p.idx_px, 0) : "—"} ${esc(target)} ${net != null ? pnlSigned(net, 2) : "—"} `; }) .join(""); return `
期权持仓
${rows}
来源合约类型到期倒计时指数目标监控净盈亏
`; } function renderAccountPositions(ac) { const perpLines = accountPerpLines(ac); const optionsPositions = Array.isArray(ac && ac.options_positions) ? ac.options_positions : []; const issues = Array.isArray(ac && ac.issues) ? ac.issues : []; const chips = renderMonitorCountChips((ac && ac.monitor_counts) || {}); const monitorRow = chips.length ? `
${chips.join("")}
` : ""; const perpHtml = renderDashboardPerpTable(perpLines); const optionsHtml = ac && ac.options_layout ? renderDashboardOptionsTable(optionsPositions) : ""; const issueHtml = issues .map((text) => `
${esc(text)}
`) .join(""); return `${monitorRow}${perpHtml}${optionsHtml}${issueHtml}`; } function bindDashboardExpand() { if (!elAccounts) return; elAccounts.querySelectorAll(".dash-ac-expand-btn").forEach((btn) => { btn.addEventListener("click", (ev) => { ev.preventDefault(); ev.stopPropagation(); const id = btn.getAttribute("data-dash-ex-id"); if (id && window.hubOpenMonitorExpand) window.hubOpenMonitorExpand(id); }); }); } function renderAccounts(accounts, threshold) { const rows = (Array.isArray(accounts) ? accounts : []).filter(accountHasOpenPositions); if (!rows.length) { elAccounts.innerHTML = '
当前无持仓账户
'; return; } elAccounts.innerHTML = rows .map((ac) => { const alert = !!ac.loss_alert; const unmon = !ac.monitored; const lossPct = Number(ac.daily_loss_pct); const barW = alert && Number.isFinite(lossPct) ? Math.min(100, (lossPct / Math.max(threshold, 1)) * 100) : 0; const badge = alert ? `单日亏损 ≥${threshold}%` : `${esc(ac.status || "—")}`; const exId = ac && ac.id != null ? String(ac.id) : ""; const expandBtn = exId ? `` : ""; const lossBar = alert && barW > 0 ? `
` : ""; const cardCls = ac.options_layout ? " dash-ac-card-options" : ""; return `
${esc(ac.name || "—")}
${badge}${expandBtn}
${lossBar}
${renderAccountPositions(ac)}
`; }) .join(""); bindDashboardExpand(); if (window.OptionsExpiryCountdown && OptionsExpiryCountdown.ensureTimer) { OptionsExpiryCountdown.ensureTimer(); } } function renderTrades(trades, accounts) { if (!elTrades) return; const rows = Array.isArray(trades) ? trades : []; if (!rows.length) { elTrades.innerHTML = '
今日暂无平仓
'; return; } const alertNames = new Set( (accounts || []).filter((a) => a.loss_alert).map((a) => String(a.name || "")) ); const body = rows .map((t) => { const pnl = Number(t.pnl_amount); const rowAlert = alertNames.has(String(t.account_name || "")); return ` ${esc(t.trading_day || "—")} ${esc(t.account_name || "—")} ${esc(t.symbol || "—")} ${esc(t.direction || "—")} ${esc(t.result || "—")} ${pnlSigned(pnl, 2)} ${esc(t.closed_at || "—")} `; }) .join(""); elTrades.innerHTML = `
${body}
交易日账户合约方向结果盈亏时间
`; } function renderPayload(data) { const totals = data.totals || {}; const threshold = Number(data.loss_alert_pct_threshold) || 5; const alertCount = Number(data.loss_alert_count) || 0; if (elDay) elDay.textContent = totals.trading_day || data.trading_day || "—"; if (elUpdated) elUpdated.textContent = data.updated_at || "—"; renderKpi(totals); renderAccounts(data.accounts, threshold); renderTrades(data.closed_trades, data.accounts); if (elBanner && elBannerText) { if (alertCount > 0) { const names = (data.accounts || []) .filter((a) => a.loss_alert) .map((a) => a.name) .join(","); elBanner.classList.add("is-on"); elBannerText.textContent = `${alertCount} 户单日平仓亏损超过资金合计 ${threshold}%:${names}`; } else { elBanner.classList.remove("is-on"); elBannerText.textContent = ""; } } } async function fetchDashboardSnapshot(opts) { const options = opts || {}; if (loading && !options.force) return; loading = true; if (!options.silent) setStatus("同步中…"); try { const r = await fetch("/api/dashboard/daily", { credentials: "same-origin" }); if (r.status === 401) { location.href = "/login?next=" + encodeURIComponent(location.pathname); return; } const data = await r.json(); if (!data.ok) throw new Error(data.detail || data.msg || data.error || "加载失败"); const ver = Number(data.dashboard_version) || 0; if (ver) localDashVersion = ver; renderPayload(data); const sec = Number(data.poll_interval_sec) || 5; setStatus(options.silent ? `SSE 已连接 · 后台每 ${sec}s 聚合` : `已更新 · 后台每 ${sec}s 聚合`); } catch (e) { setStatus(String(e.message || e), true); } finally { loading = false; } } function closeDashboardStream() { if (dashEventSource) { dashEventSource.close(); dashEventSource = null; } if (dashReconnectTimer) { clearTimeout(dashReconnectTimer); dashReconnectTimer = null; } } function connectDashboardStream() { closeDashboardStream(); dashEventSource = new EventSource("/api/dashboard/stream"); dashEventSource.addEventListener("dashboard", (ev) => { try { const st = JSON.parse(ev.data || "{}"); const ver = Number(st.dashboard_version) || 0; if (ver && ver !== localDashVersion) { void fetchDashboardSnapshot({ silent: true }); } else if (st.aggregating) { setStatus("后台聚合中…"); } } catch (_) { /* ignore */ } }); dashEventSource.onerror = () => { closeDashboardStream(); setStatus("SSE 断开,8s 后重连…", true); dashReconnectTimer = setTimeout(() => { if (inited) { connectDashboardStream(); void fetchDashboardSnapshot({ silent: true }); } }, 8000); }; } async function requestDashboardRefresh() { try { await fetch("/api/dashboard/refresh", { method: "POST", credentials: "same-origin" }); } catch (_) { /* ignore */ } } function startLive() { void fetchDashboardSnapshot(); connectDashboardStream(); } function stopLive() { closeDashboardStream(); setStatus(""); } if (btnRefresh) { btnRefresh.addEventListener("click", () => { void requestDashboardRefresh(); void fetchDashboardSnapshot({ force: true }); }); } window.hubDashboardPage = { init() { inited = true; startLive(); }, destroy() { inited = false; stopLive(); }, }; })();