/** * 中控数据看板:总览 / 分户 / 平仓明细,60s 自动刷新。 */ (function () { const page = document.getElementById("page-dashboard"); if (!page) return; const POLL_MS = 60 * 1000; let timer = null; 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; elKpi.innerHTML = [ kpiCard("交易日", esc(totals.trading_day || "—"), ""), kpiCard("平仓盈亏", pnlSigned(closed, 2), pnlClass(closed)), kpiCard( "平仓笔数", `${totals.closed_count || 0}`, "", `胜 ${totals.win_count || 0} / 负 ${totals.loss_count || 0}` ), kpiCard("浮盈亏", pnlSigned(floating, 2), pnlClass(floating)), kpiCard( "资金合计", funding != null && trading != null ? `${fmt(Number(funding) + Number(trading), 2)}U` : "—", "", `资金 ${fmt(funding, 2)} + 交易 ${fmt(trading, 2)}` ), kpiCard("实盘持仓", `${totals.open_position_count || 0} 仓`, ""), ].join(""); } function kpiCard(label, value, valCls, sub) { return `
${esc(label)}
${value}
${sub ? `
${esc(sub)}
` : ""}
`; } function renderAccounts(accounts, threshold) { if (!elAccounts) return; const rows = Array.isArray(accounts) ? accounts : []; if (!rows.length) { elAccounts.innerHTML = '
暂无账户数据
'; return; } elAccounts.innerHTML = rows .map((ac) => { const alert = !!ac.loss_alert; const unmon = !ac.monitored; const pnl = Number(ac.pnl_u); const floatPnl = Number(ac.float_pnl_u); 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 lossBar = alert && barW > 0 ? `
` : ""; return `
${esc(ac.name || "—")}
${badge}
资金账户${fmt(ac.funding_usdt, 2)}U
交易账户${fmt(ac.trading_usdt, 2)}U
资金合计${fmt(ac.capital_total_usdt, 2)}U
今日盈亏${pnlSigned(pnl, 2)}
平仓笔数${Number(ac.closed_count) || 0}
浮盈亏${pnlSigned(floatPnl, 2)}
${lossBar}
${esc(ac.remark || "—")}
`; }) .join(""); } 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 fetchDashboard() { if (loading) return; loading = true; 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 || "加载失败"); renderPayload(data); setStatus(`每 ${(data.poll_interval_sec || 60)}s 自动刷新`); } catch (e) { setStatus(String(e.message || e), true); } finally { loading = false; } } function startPoll() { stopPoll(); void fetchDashboard(); timer = setInterval(fetchDashboard, POLL_MS); } function stopPoll() { if (timer) { clearInterval(timer); timer = null; } } if (btnRefresh) { btnRefresh.addEventListener("click", () => void fetchDashboard()); } window.hubDashboardPage = { init() { if (!inited) inited = true; startPoll(); }, destroy() { stopPoll(); setStatus(""); }, }; })();