Files
crypto_monitor/manual_trading_hub/static/funds.js
T
dekun cb2fcd283a Give funds equity curve more room on 1920x1080.
Drop the 42vh chart cap, let the curve fill remaining height, and scroll account cards so the curve is not clipped.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 11:19:58 +08:00

519 lines
18 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 elPeriod = document.getElementById("funds-total-period");
const elPeriodSub = document.getElementById("funds-total-period-sub");
const elPeriodBanner = document.getElementById("funds-period-delta");
const elPeriodPct = document.getElementById("funds-period-pct");
const elDayChip = document.getElementById("funds-day-chip");
const elPnlBanner = document.getElementById("funds-pnl-banner");
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 fmtPct(n) {
if (n == null || !Number.isFinite(Number(n))) return "—";
const v = Number(n);
const sign = v > 0 ? "+" : "";
return sign + v.toFixed(2) + "%";
}
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) {
const w = Math.max(host.clientWidth || 0, 1);
const h = Math.max(host.clientHeight || 0, 1);
c.applyOptions({ width: w, height: h });
}
}).observe(host);
c.applyOptions({
width: Math.max(host.clientWidth || 0, 1),
height: Math.max(host.clientHeight || 0, 200),
});
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, "&amp;")
.replace(/</g, "&lt;")
.replace(/"/g, "&quot;");
}
function accountStatus(ac) {
if (!ac || !ac.monitored) return { text: "未监控", cls: "" };
if (ac.data_ok) return { text: "已监控", cls: "is-ok" };
return { text: "余额未齐", cls: "" };
}
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 offCls = monitored ? "" : " is-off";
const st = accountStatus(ac);
const clickable = monitored ? "" : ' disabled aria-disabled="true"';
const name = ac.name || ac.key || "—";
const total =
monitored && ac.data_ok ? fmt(ac.total_usdt, 2) + " U" : "—";
const funding =
monitored && ac.funding_usdt != null ? fmt(ac.funding_usdt, 2) + " U" : "—";
const trading =
monitored && ac.trading_usdt != null ? fmt(ac.trading_usdt, 2) + " U" : "—";
const optFunding =
monitored && ac.options_funding_usdt != null ? fmt(ac.options_funding_usdt, 2) + " U" : "";
const optTrading =
monitored && ac.options_trading_usdt != null ? fmt(ac.options_trading_usdt, 2) + " U" : "";
const optLine =
optFunding || optTrading
? '<div><span class="k">期权户</span><span class="v">' +
(optFunding || "—") +
" / " +
(optTrading || "—") +
"</span></div>"
: "";
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 deltaCls = deltaClass(ac.day_delta_usdt);
const deltaText = monitored ? fmtDelta(ac.day_delta_usdt) : "—";
const periodCls = deltaClass(ac.period_delta_usdt);
const periodText = monitored ? fmtDelta(ac.period_delta_usdt) : "—";
return (
'<button type="button" class="funds-ac-card' +
offCls +
'" data-key="' +
esc(ac.key || "") +
'"' +
clickable +
' title="' +
esc(monitored ? "点击查看 " + name + " 资金曲线" : "未监控,不参与合计") +
'">' +
'<div class="funds-ac-head">' +
'<h3 class="funds-ac-name">' +
esc(name) +
"</h3>" +
'<span class="funds-ac-badge ' +
st.cls +
'">' +
st.text +
"</span>" +
"</div>" +
'<div class="funds-ac-total">' +
'<span class="k">总资金</span>' +
'<span class="v">' +
total +
"</span>" +
"</div>" +
'<div class="funds-ac-stats">' +
'<div><span class="k">资金户</span><span class="v">' +
funding +
"</span></div>" +
'<div><span class="k">交易户</span><span class="v">' +
trading +
"</span></div>" +
optLine +
'<div><span class="k">累计盈亏</span><span class="v ' +
periodCls +
'">' +
periodText +
"</span></div>" +
'<div><span class="k">较昨日</span><span class="v ' +
deltaCls +
'">' +
deltaText +
"</span></div>" +
'<div><span class="k">最大回撤</span><span class="v">' +
ddU +
" / " +
ddPct +
"</span></div>" +
"</div>" +
(monitored
? '<div class="funds-ac-foot">点击查看资金曲线</div>'
: "") +
"</button>"
);
})
.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 = [
"资金户 + 交易户 + 期权户(USDC≈USDT,不含浮盈)",
"交易日 " + (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 =
'<p class="funds-empty">暂无历史曲线,请保持监控板运行以积累快照</p>';
}
}
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 =
"总资金 = 各监控户(永续资金账户 + 交易账户 + 期权账户,USDC 按 1:1 计入 USDT);自 " +
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);
}
const periodCls = deltaClass(totals.period_delta_usdt);
if (elPeriod) {
elPeriod.textContent = fmtDelta(totals.period_delta_usdt);
elPeriod.className = "funds-stat-val " + periodCls;
}
if (elPeriodSub) {
const startDay = data.history_start_day || "—";
const pct = fmtPct(totals.period_delta_pct);
elPeriodSub.textContent =
pct !== "—"
? "自 " + startDay + " · " + pct
: "自 " + startDay + " 起相对起点";
}
if (elPeriodBanner) {
elPeriodBanner.textContent = fmtDelta(totals.period_delta_usdt);
elPeriodBanner.className = "funds-pnl-value " + periodCls;
}
if (elPeriodPct) {
elPeriodPct.textContent = fmtPct(totals.period_delta_pct);
elPeriodPct.className = "funds-pnl-pct " + periodCls;
}
if (elDayChip) {
elDayChip.textContent = fmtDelta(totals.day_delta_usdt);
elDayChip.className = "funds-pnl-side-val " + deltaClass(totals.day_delta_usdt);
}
if (elPnlBanner) {
elPnlBanner.className =
"funds-pnl-banner" + (periodCls ? " is-" + periodCls : "");
}
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 || []);
// 分户卡片渲染后高度会变,补一次尺寸,避免 1080p 一屏布局下曲线被裁切
if (chart && elChartHost) {
requestAnimationFrame(function () {
if (!chart || !elChartHost) return;
chart.applyOptions({
width: Math.max(elChartHost.clientWidth || 0, 1),
height: Math.max(elChartHost.clientHeight || 0, 200),
});
chart.timeScale().fitContent();
});
}
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 };
})();