ba629ea0ee
Add HUB_FUND_HISTORY_START_DAY so curves and drawdown exclude snapshots before the baseline trading day. Co-authored-by: Cursor <cursoragent@cursor.com>
283 lines
9.3 KiB
JavaScript
283 lines
9.3 KiB
JavaScript
/**
|
|
* 中控资金概况:总资金曲线、分户资金与回撤(资金户+交易户,不含浮盈)。
|
|
*/
|
|
(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 elChartHost = document.getElementById("funds-chart-total");
|
|
const elAccounts = document.getElementById("funds-accounts");
|
|
const elBtnRefresh = document.getElementById("funds-btn-refresh");
|
|
|
|
let chart = null;
|
|
let lineSeries = null;
|
|
let inited = false;
|
|
let loading = false;
|
|
|
|
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 chartPalette() {
|
|
const light = document.documentElement.getAttribute("data-theme") === "light";
|
|
return light
|
|
? { bg: "#f0f4f9", text: "#4a6078", border: "#b8c8d8", line: "#006e9a" }
|
|
: { bg: "#0b0e18", text: "#9aa4b8", border: "#2a3348", line: "#3b82f6" };
|
|
}
|
|
|
|
function ensureChart() {
|
|
if (!elChartHost || !window.LightweightCharts) return;
|
|
if (chart) return;
|
|
const p = chartPalette();
|
|
chart = LightweightCharts.createChart(elChartHost, {
|
|
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 },
|
|
});
|
|
lineSeries = chart.addAreaSeries({
|
|
lineColor: p.line,
|
|
topColor: p.line + "44",
|
|
bottomColor: p.line + "08",
|
|
lineWidth: 2,
|
|
priceFormat: { type: "price", precision: 2, minMove: 0.01 },
|
|
});
|
|
new ResizeObserver(function () {
|
|
if (chart && elChartHost) {
|
|
chart.applyOptions({ width: elChartHost.clientWidth, height: elChartHost.clientHeight });
|
|
}
|
|
}).observe(elChartHost);
|
|
chart.applyOptions({ width: elChartHost.clientWidth, height: elChartHost.clientHeight });
|
|
}
|
|
|
|
function renderMiniChart(host, series) {
|
|
if (!host || !window.LightweightCharts) return;
|
|
host.innerHTML = "";
|
|
const data = seriesToChartData(series);
|
|
if (data.length < 2) {
|
|
host.textContent = "历史不足";
|
|
return;
|
|
}
|
|
const p = chartPalette();
|
|
const mini = LightweightCharts.createChart(host, {
|
|
layout: { background: { color: "transparent" }, textColor: p.text },
|
|
grid: { vertLines: { visible: false }, horzLines: { visible: false } },
|
|
rightPriceScale: { visible: false },
|
|
timeScale: { visible: false },
|
|
crosshair: { mode: LightweightCharts.CrosshairMode.Hidden },
|
|
handleScroll: false,
|
|
handleScale: false,
|
|
});
|
|
const s = mini.addLineSeries({ color: p.line, lineWidth: 1.5 });
|
|
s.setData(data);
|
|
mini.timeScale().fitContent();
|
|
const w = host.clientWidth;
|
|
const h = host.clientHeight;
|
|
if (w > 0 && h > 0) mini.applyOptions({ width: w, height: h });
|
|
}
|
|
|
|
function renderAccounts(accounts) {
|
|
if (!elAccounts) return;
|
|
if (!accounts || !accounts.length) {
|
|
elAccounts.innerHTML = '<p class="funds-empty">暂无账户配置</p>';
|
|
return;
|
|
}
|
|
elAccounts.innerHTML = accounts
|
|
.map(function (ac) {
|
|
const monitored = !!ac.monitored;
|
|
const cls = monitored ? "" : " is-off";
|
|
const total = monitored && ac.data_ok ? fmt(ac.total_usdt, 2) + " U" : "—";
|
|
const funding = monitored && ac.funding_usdt != null ? fmt(ac.funding_usdt, 2) : "—";
|
|
const trading = monitored && ac.trading_usdt != null ? fmt(ac.trading_usdt, 2) : "—";
|
|
const dd = ac.drawdown || {};
|
|
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) + "%" : "—";
|
|
const status = monitored ? (ac.data_ok ? "已监控" : "余额未齐") : "未监控";
|
|
return (
|
|
'<article class="funds-ac-card' +
|
|
cls +
|
|
'" data-key="' +
|
|
(ac.key || "") +
|
|
'">' +
|
|
'<div class="funds-ac-head">' +
|
|
'<h3>' +
|
|
(ac.name || ac.key || "—") +
|
|
"</h3>" +
|
|
'<span class="funds-ac-badge">' +
|
|
status +
|
|
"</span>" +
|
|
"</div>" +
|
|
'<div class="funds-ac-stats">' +
|
|
'<div><span class="k">总资金</span><span class="v">' +
|
|
total +
|
|
"</span></div>" +
|
|
'<div><span class="k">资金户</span><span class="v">' +
|
|
funding +
|
|
"</span></div>" +
|
|
'<div><span class="k">交易户</span><span class="v">' +
|
|
trading +
|
|
"</span></div>" +
|
|
'<div><span class="k">最大回撤</span><span class="v">' +
|
|
ddU +
|
|
" / " +
|
|
ddPct +
|
|
"</span></div>" +
|
|
"</div>" +
|
|
'<div class="funds-ac-chart" aria-hidden="true"></div>' +
|
|
"</article>"
|
|
);
|
|
})
|
|
.join("");
|
|
|
|
elAccounts.querySelectorAll(".funds-ac-card").forEach(function (card, idx) {
|
|
const ac = accounts[idx];
|
|
const host = card.querySelector(".funds-ac-chart");
|
|
if (ac && ac.monitored && host) {
|
|
renderMiniChart(host, ac.series || []);
|
|
} else if (host) {
|
|
host.textContent = monitoredLabel(ac);
|
|
}
|
|
});
|
|
}
|
|
|
|
function monitoredLabel(ac) {
|
|
return ac && ac.monitored ? "暂无曲线" : "未参与合计";
|
|
}
|
|
|
|
function renderOverview(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 || "2026-06-09") + " 起",
|
|
"最多 " + (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 || []);
|
|
}
|
|
|
|
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);
|
|
document.addEventListener("hub-theme-change", function () {
|
|
destroyChart();
|
|
load();
|
|
});
|
|
}
|
|
|
|
function init() {
|
|
if (!page || page.classList.contains("hidden")) return;
|
|
if (!inited) {
|
|
bind();
|
|
inited = true;
|
|
}
|
|
load();
|
|
}
|
|
|
|
function destroy() {
|
|
destroyChart();
|
|
}
|
|
|
|
window.hubFundsPage = { init: init, destroy: destroy, reload: load };
|
|
})();
|