Initialize crypto_monitor_user (user edition) from monitor codebase.
Retarget git remote, install path, and deploy docs from crypto_monitor to crypto_monitor_user. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,529 @@
|
||||
/**
|
||||
* 中控资金概况:总资金曲线,分户资金与回撤(资金户+交易户,不含浮盈).
|
||||
*/
|
||||
(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,
|
||||
fontSize: 11,
|
||||
},
|
||||
grid: {
|
||||
vertLines: { color: p.border, visible: true },
|
||||
horzLines: { color: p.border, visible: true },
|
||||
},
|
||||
rightPriceScale: {
|
||||
borderColor: p.border,
|
||||
scaleMargins: { top: 0.08, bottom: 0.08 },
|
||||
},
|
||||
timeScale: {
|
||||
borderColor: p.border,
|
||||
timeVisible: true,
|
||||
fixLeftEdge: true,
|
||||
fixRightEdge: 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 },
|
||||
});
|
||||
function syncSize() {
|
||||
if (!c || !host) return;
|
||||
const w = Math.max(host.clientWidth || 0, 1);
|
||||
const h = Math.max(host.clientHeight || 0, 200);
|
||||
c.applyOptions({ width: w, height: h });
|
||||
}
|
||||
new ResizeObserver(function () {
|
||||
syncSize();
|
||||
}).observe(host);
|
||||
syncSize();
|
||||
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(/</g, "<")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
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 };
|
||||
})();
|
||||
Reference in New Issue
Block a user