feat: add hub fund overview tab with 180-day equity curves
Add /funds page for total and per-account balance (funding+trading), drawdown, and daily snapshots from monitor board aggregation. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4428,6 +4428,135 @@ body.hub-page-ai #page-ai {
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
/* —— 资金概况 —— */
|
||||
.funds-toolbar {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.funds-status.err {
|
||||
color: var(--red);
|
||||
}
|
||||
.funds-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.funds-stat-card {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border-soft);
|
||||
border-radius: var(--radius);
|
||||
padding: 12px 14px;
|
||||
}
|
||||
.funds-stat-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--muted);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.funds-stat-value {
|
||||
font-size: 1.35rem;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.funds-stat-val {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.funds-stat-val.pos {
|
||||
color: var(--green);
|
||||
}
|
||||
.funds-stat-val.neg {
|
||||
color: var(--red);
|
||||
}
|
||||
.funds-dd-pct {
|
||||
font-size: 0.82rem;
|
||||
color: var(--muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
.funds-meta {
|
||||
font-size: 0.78rem;
|
||||
color: var(--muted);
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
.funds-chart-host {
|
||||
height: 280px;
|
||||
min-height: 220px;
|
||||
border: 1px solid var(--border-soft);
|
||||
border-radius: var(--radius);
|
||||
background: var(--chart-surface, var(--panel));
|
||||
margin-bottom: 18px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.funds-section-title {
|
||||
margin: 0 0 10px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.funds-accounts {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.funds-ac-card {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border-soft);
|
||||
border-radius: var(--radius);
|
||||
padding: 12px 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.funds-ac-card.is-off {
|
||||
opacity: 0.72;
|
||||
}
|
||||
.funds-ac-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
.funds-ac-head h3 {
|
||||
margin: 0;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
.funds-ac-badge {
|
||||
font-size: 0.68rem;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border-soft);
|
||||
color: var(--muted);
|
||||
}
|
||||
.funds-ac-stats {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6px 10px;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
.funds-ac-stats .k {
|
||||
color: var(--muted);
|
||||
margin-right: 6px;
|
||||
}
|
||||
.funds-ac-stats .v {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.funds-ac-chart {
|
||||
height: 72px;
|
||||
min-height: 72px;
|
||||
border-radius: 6px;
|
||||
background: var(--inset-surface);
|
||||
font-size: 0.72rem;
|
||||
color: var(--muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
.funds-empty {
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
/* —— 币种档案 —— */
|
||||
.archive-toolbar {
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -625,6 +625,7 @@
|
||||
const p = window.location.pathname.replace(/\/$/, "") || "/monitor";
|
||||
if (p.includes("settings")) return "settings";
|
||||
if (p.includes("archive")) return "archive";
|
||||
if (p.includes("funds")) return "funds";
|
||||
if (p.includes("market")) return "market";
|
||||
if (p.includes("/ai")) return "ai";
|
||||
return "monitor";
|
||||
@@ -633,6 +634,7 @@
|
||||
function pageElementId(page) {
|
||||
if (page === "settings") return "page-settings";
|
||||
if (page === "archive") return "page-archive";
|
||||
if (page === "funds") return "page-funds";
|
||||
if (page === "market") return "page-market";
|
||||
if (page === "ai") return "page-ai";
|
||||
return "page-monitor";
|
||||
@@ -662,6 +664,11 @@
|
||||
} else if (window.hubArchivePage && window.hubArchivePage.destroy) {
|
||||
window.hubArchivePage.destroy();
|
||||
}
|
||||
if (page === "funds" && window.hubFundsPage) {
|
||||
window.hubFundsPage.init();
|
||||
} else if (window.hubFundsPage && window.hubFundsPage.destroy) {
|
||||
window.hubFundsPage.destroy();
|
||||
}
|
||||
if (page === "market" && window.hubMarketChart) {
|
||||
window.hubMarketChart.init();
|
||||
} else if (window.hubMarketChart) {
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
/**
|
||||
* 中控资金概况:总资金曲线、分户资金与回撤(资金户+交易户,不含浮盈)。
|
||||
*/
|
||||
(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.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 };
|
||||
})();
|
||||
@@ -15,7 +15,7 @@
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
|
||||
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
|
||||
<link rel="stylesheet" href="/assets/app.css?v=20260609-market-day-split" />
|
||||
<link rel="stylesheet" href="/assets/app.css?v=20260609-hub-funds" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-bg" aria-hidden="true"></div>
|
||||
@@ -47,6 +47,7 @@
|
||||
<a href="/monitor" id="nav-monitor">监控区</a>
|
||||
<a href="/market" id="nav-market">行情区</a>
|
||||
<a href="/archive" id="nav-archive">币种档案</a>
|
||||
<a href="/funds" id="nav-funds">资金概况</a>
|
||||
<a href="/ai" id="nav-ai">AI 教练</a>
|
||||
<a href="/settings" id="nav-settings">系统设置</a>
|
||||
</nav>
|
||||
@@ -330,6 +331,35 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="page-funds" class="page hidden">
|
||||
<div class="page-head">
|
||||
<h1><span class="head-tag">FND</span> 资金概况</h1>
|
||||
<p class="page-desc">总资金 = 各监控户(资金账户 + 交易账户);按北京时间交易日切日快照,保留 180 天</p>
|
||||
</div>
|
||||
<div class="funds-toolbar toolbar">
|
||||
<button type="button" id="funds-btn-refresh" class="primary">刷新</button>
|
||||
<span id="funds-status" class="toolbar-meta funds-status"></span>
|
||||
</div>
|
||||
<section class="funds-summary">
|
||||
<div class="funds-stat-card">
|
||||
<div class="funds-stat-label">总资金</div>
|
||||
<div id="funds-total-usdt" class="funds-stat-value">—</div>
|
||||
</div>
|
||||
<div class="funds-stat-card">
|
||||
<div class="funds-stat-label">较昨日</div>
|
||||
<div id="funds-total-delta" class="funds-stat-val">—</div>
|
||||
</div>
|
||||
<div class="funds-stat-card">
|
||||
<div class="funds-stat-label">最大回撤</div>
|
||||
<div class="funds-stat-value"><span id="funds-total-dd-u">—</span> <small id="funds-total-dd-pct" class="funds-dd-pct">—</small></div>
|
||||
</div>
|
||||
</section>
|
||||
<p id="funds-meta" class="funds-meta"></p>
|
||||
<div id="funds-chart-total" class="funds-chart-host"></div>
|
||||
<h2 class="funds-section-title">分户资金</h2>
|
||||
<div id="funds-accounts" class="funds-accounts"></div>
|
||||
</div>
|
||||
|
||||
<div id="page-ai" class="page hidden">
|
||||
<div class="page-head">
|
||||
<h1><span class="head-tag">AI</span> 教练</h1>
|
||||
@@ -426,7 +456,8 @@
|
||||
<script src="/assets/chart_draw.js?v=20260609-market-day-split"></script>
|
||||
<script src="/assets/chart.js?v=20260609-market-day-split"></script>
|
||||
<script src="/assets/archive.js?v=20260608-hub-archive-history"></script>
|
||||
<script src="/assets/funds.js?v=20260609-hub-funds"></script>
|
||||
<script src="/assets/ai_review_render.js?v=2"></script>
|
||||
<script src="/assets/app.js?v=20260609-hub-mobile-ai-v3"></script>
|
||||
<script src="/assets/app.js?v=20260609-hub-funds"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user