/** * 中控资金概况:总资金曲线、分户资金与回撤(资金户+交易户,不含浮盈)。 */ (function () { const page = document.getElementById("page-funds"); if (!page) return; const elStatus = document.getElementById("funds-status"); const elTotal = document.getElementById("funds-total-usdt"); const elDdU = document.getElementById("funds-total-dd-u"); const elDdPct = document.getElementById("funds-total-dd-pct"); const elDelta = document.getElementById("funds-total-delta"); const elMeta = document.getElementById("funds-meta"); const elDescBody = document.getElementById("funds-desc-body"); const elChartSub = document.getElementById("funds-chart-sub"); const elChartHost = document.getElementById("funds-chart-total"); const elAccounts = document.getElementById("funds-accounts"); const elBtnRefresh = document.getElementById("funds-btn-refresh"); const elFs = document.getElementById("funds-fullscreen"); const elFsBackdrop = document.getElementById("funds-fs-backdrop"); const elFsClose = document.getElementById("funds-fs-close"); const elFsTitle = document.getElementById("funds-fs-title"); const elFsSub = document.getElementById("funds-fs-sub"); const elFsTotal = document.getElementById("funds-fs-total"); const elFsFunding = document.getElementById("funds-fs-funding"); const elFsTrading = document.getElementById("funds-fs-trading"); const elFsDelta = document.getElementById("funds-fs-delta"); const elFsDd = document.getElementById("funds-fs-dd"); const elFsChartHost = document.getElementById("funds-fs-chart"); let chart = null; let lineSeries = null; let fsChart = null; let fsLineSeries = null; let inited = false; let loading = false; let lastOverview = null; let fsAccountKey = ""; function fmt(n, d) { if (n == null || n === "" || !Number.isFinite(Number(n))) return "—"; return Number(n).toFixed(d == null ? 2 : d); } function fmtDelta(n) { if (n == null || !Number.isFinite(Number(n))) return "—"; const v = Number(n); const sign = v > 0 ? "+" : ""; return sign + v.toFixed(2) + " U"; } function deltaClass(n) { if (!Number.isFinite(Number(n))) return ""; if (Number(n) > 0) return "pos"; if (Number(n) < 0) return "neg"; return ""; } function setStatus(msg, isErr) { if (!elStatus) return; elStatus.textContent = msg || ""; elStatus.className = "funds-status" + (isErr ? " err" : ""); } function seriesToChartData(series) { return (series || []) .filter(function (p) { return p && p.day && Number.isFinite(Number(p.total_usdt)); }) .map(function (p) { return { time: String(p.day), value: Number(p.total_usdt) }; }); } function destroyChart() { if (chart) { chart.remove(); chart = null; lineSeries = null; } if (elChartHost) elChartHost.innerHTML = ""; } function destroyFsChart() { if (fsChart) { fsChart.remove(); fsChart = null; fsLineSeries = null; } if (elFsChartHost) elFsChartHost.innerHTML = ""; } function chartPalette() { const light = document.documentElement.getAttribute("data-theme") === "light"; return light ? { bg: "#eef4fa", text: "#4a6078", border: "#c5d4e4", line: "#006e9a", top: "#006e9a44" } : { bg: "#060a14", text: "#6b8aa8", border: "#1a2840", line: "#00d4ff", top: "#00d4ff55" }; } function createAreaChart(host) { const p = chartPalette(); const c = LightweightCharts.createChart(host, { layout: { background: { color: p.bg }, textColor: p.text }, grid: { vertLines: { color: p.border, visible: true }, horzLines: { color: p.border, visible: true }, }, rightPriceScale: { borderColor: p.border }, timeScale: { borderColor: p.border, timeVisible: true }, crosshair: { mode: LightweightCharts.CrosshairMode.Normal }, handleScroll: { mouseWheel: true, pressedMouseMove: true }, handleScale: { axisPressedMouseMove: true, mouseWheel: true, pinch: true }, }); const s = c.addAreaSeries({ lineColor: p.line, topColor: p.top || p.line + "44", bottomColor: p.line + "08", lineWidth: 2, priceFormat: { type: "price", precision: 2, minMove: 0.01 }, }); new ResizeObserver(function () { if (c && host) { c.applyOptions({ width: host.clientWidth, height: host.clientHeight }); } }).observe(host); c.applyOptions({ width: host.clientWidth, height: host.clientHeight }); return { chart: c, series: s }; } function ensureChart() { if (!elChartHost || !window.LightweightCharts) return; if (chart) return; const built = createAreaChart(elChartHost); chart = built.chart; lineSeries = built.series; } function ensureFsChart() { if (!elFsChartHost || !window.LightweightCharts) return; if (fsChart) return; const built = createAreaChart(elFsChartHost); fsChart = built.chart; fsLineSeries = built.series; } function esc(s) { return String(s || "") .replace(/&/g, "&") .replace(/' + '
' + '

' + esc(name) + "

" + '' + st.text + "" + "
" + '
' + '总资金' + '' + total + "" + "
" + '
' + '
资金户' + funding + "
" + '
交易户' + trading + "
" + '
较昨日' + deltaText + "
" + '
最大回撤' + ddU + " / " + ddPct + "
" + "
" + (monitored ? '
点击查看资金曲线
' : "") + "" ); }) .join(""); elAccounts.querySelectorAll(".funds-ac-card:not(.is-off)").forEach(function (btn) { btn.addEventListener("click", function () { openAccountFullscreen(btn.getAttribute("data-key")); }); }); } function findAccount(key) { const accounts = (lastOverview && lastOverview.accounts) || []; return accounts.find(function (ac) { return String(ac.key || "") === String(key || ""); }); } function closeAccountFullscreen() { fsAccountKey = ""; destroyFsChart(); if (elFs) { elFs.classList.add("hidden"); elFs.setAttribute("aria-hidden", "true"); } document.body.classList.remove("funds-fullscreen-open"); } function openAccountFullscreen(key) { const ac = findAccount(key); if (!ac || !ac.monitored) return; fsAccountKey = String(key || ""); const dd = ac.drawdown || {}; const meta = lastOverview || {}; if (elFsTitle) elFsTitle.textContent = ac.name || ac.key || "—"; if (elFsSub) { const parts = [ "资金户 + 交易户(不含浮盈)", "交易日 " + (meta.trading_day || "—"), "自 " + (meta.history_start_day || "—") + " 起", ]; elFsSub.textContent = parts.join(" · "); } if (elFsTotal) { elFsTotal.textContent = ac.data_ok && ac.total_usdt != null ? fmt(ac.total_usdt, 2) + " U" : "—"; } if (elFsFunding) { elFsFunding.textContent = ac.funding_usdt != null ? fmt(ac.funding_usdt, 2) + " U" : "—"; } if (elFsTrading) { elFsTrading.textContent = ac.trading_usdt != null ? fmt(ac.trading_usdt, 2) + " U" : "—"; } if (elFsDelta) { elFsDelta.textContent = fmtDelta(ac.day_delta_usdt); elFsDelta.className = "v " + deltaClass(ac.day_delta_usdt); } if (elFsDd) { const ddU = dd.max_drawdown_u != null ? fmt(dd.max_drawdown_u, 2) + " U" : "—"; const ddPct = dd.max_drawdown_pct != null ? fmt(dd.max_drawdown_pct, 2) + "%" : "—"; elFsDd.textContent = ddU + " / " + ddPct; } if (elFs) { elFs.classList.remove("hidden"); elFs.setAttribute("aria-hidden", "false"); document.body.classList.add("funds-fullscreen-open"); } destroyFsChart(); const pts = seriesToChartData(ac.series || []); if (pts.length) { ensureFsChart(); if (fsLineSeries) { fsLineSeries.setData(pts); fsChart.timeScale().fitContent(); } requestAnimationFrame(function () { if (fsChart && elFsChartHost) { fsChart.applyOptions({ width: elFsChartHost.clientWidth, height: elFsChartHost.clientHeight, }); fsChart.timeScale().fitContent(); } }); } else if (elFsChartHost) { elFsChartHost.innerHTML = '

暂无历史曲线,请保持监控板运行以积累快照

'; } } function renderDesc(data) { const start = (data && data.history_start_day) || "—"; const keep = (data && data.keep_days) || 180; const hour = data && data.reset_hour != null ? data.reset_hour : 8; if (elDescBody) { elDescBody.textContent = "总资金 = 各监控户(资金账户 + 交易账户);自 " + start + " 起按北京时间 " + hour + ":00 交易日切日快照,最多保留 " + keep + " 天。起算日由环境变量 HUB_FUND_HISTORY_START_DAY 配置。"; } if (elChartSub) { elChartSub.textContent = keep + " TRADING DAYS"; } } function renderOverview(data) { lastOverview = data; renderDesc(data); const totals = data.totals || {}; const dd = totals.drawdown || {}; if (elTotal) { elTotal.textContent = totals.total_usdt != null ? fmt(totals.total_usdt, 2) + " U" : "—"; } if (elDdU) elDdU.textContent = dd.max_drawdown_u != null ? fmt(dd.max_drawdown_u, 2) + " U" : "—"; if (elDdPct) { elDdPct.textContent = dd.max_drawdown_pct != null ? fmt(dd.max_drawdown_pct, 2) + "%" : "—"; } if (elDelta) { elDelta.textContent = fmtDelta(totals.day_delta_usdt); elDelta.className = "funds-stat-val " + deltaClass(totals.day_delta_usdt); } if (elMeta) { const parts = [ "交易日 " + (data.trading_day || "—"), "切日 " + (data.reset_hour != null ? data.reset_hour : 8) + ":00 北京", "自 " + (data.history_start_day || "—") + " 起", "最多 " + (data.keep_days || 180) + " 交易日", ]; if (data.updated_at) parts.push("刷新 " + data.updated_at); if (totals.live_known_count != null) { parts.push("合计含 " + totals.live_known_count + " 户"); } elMeta.textContent = parts.join(" · "); } ensureChart(); if (lineSeries) { const pts = seriesToChartData(totals.series || []); if (pts.length) { lineSeries.setData(pts); chart.timeScale().fitContent(); } else { lineSeries.setData([]); } } renderAccounts(data.accounts || []); if (fsAccountKey) { const ac = findAccount(fsAccountKey); if (ac && ac.monitored) openAccountFullscreen(fsAccountKey); else closeAccountFullscreen(); } } async function load() { if (loading) return; loading = true; setStatus("加载中…"); try { const r = await fetch("/api/hub/fund-overview", { credentials: "same-origin" }); const j = await r.json(); if (!r.ok) { setStatus(j.detail || j.msg || "加载失败", true); return; } renderOverview(j); setStatus(""); } catch (e) { setStatus(String(e.message || e), true); } finally { loading = false; } } function bind() { if (elBtnRefresh) elBtnRefresh.addEventListener("click", load); if (elFsBackdrop) elFsBackdrop.addEventListener("click", closeAccountFullscreen); if (elFsClose) elFsClose.addEventListener("click", closeAccountFullscreen); document.addEventListener("keydown", function (ev) { if (ev.key === "Escape" && fsAccountKey) closeAccountFullscreen(); }); document.addEventListener("hub-theme-change", function () { destroyChart(); destroyFsChart(); load(); }); } function init() { if (!page || page.classList.contains("hidden")) return; if (!inited) { bind(); inited = true; } load(); } function destroy() { closeAccountFullscreen(); destroyChart(); } window.hubFundsPage = { init: init, destroy: destroy, reload: load }; })();