Files
Binance_Altcoin_Monitor/web/funding.js
T
2026-05-22 14:00:19 +08:00

188 lines
5.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** 资金费率历史曲线 */
const fundingCache = new Map();
const fundingQueue = [];
let fundingQueueRunning = false;
const FUNDING_FETCH_GAP_MS = 200;
const FUNDING_MINI = { w: 200, h: 56 };
const FUNDING_MODAL = { w: 960, h: 320 };
function enqueueFundingCharts(root) {
root.querySelectorAll(".mini-funding-chart[data-symbol]").forEach((box) => {
if (box.dataset.loaded === "1" || box.dataset.loading === "1") return;
fundingQueue.push(box);
});
runFundingQueue();
}
async function runFundingQueue() {
if (fundingQueueRunning) return;
fundingQueueRunning = true;
while (fundingQueue.length) {
const box = fundingQueue.shift();
if (!box?.isConnected) continue;
await loadMiniFundingChart(box);
await new Promise((r) => setTimeout(r, FUNDING_FETCH_GAP_MS));
}
fundingQueueRunning = false;
}
function setupCanvas(canvas, w, h) {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.style.width = `${w}px`;
canvas.style.height = `${h}px`;
canvas.width = Math.floor(w * dpr);
canvas.height = Math.floor(h * dpr);
const ctx = canvas.getContext("2d");
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
return { ctx, w, h };
}
function drawFundingChart(canvas, history, current, large = false) {
if (!canvas || !history?.length) return;
const size = large ? FUNDING_MODAL : FUNDING_MINI;
const { ctx, w, h } = setupCanvas(canvas, size.w, size.h);
const pad = large ? { t: 20, r: 16, b: 24, l: 48 } : { t: 6, r: 4, b: 8, l: 4 };
const plotW = w - pad.l - pad.r;
const plotH = h - pad.t - pad.b;
const rates = history.map((p) => p.rate_pct);
let min = Math.min(...rates, 0);
let max = Math.max(...rates, 0);
const margin = Math.max(0.005, (max - min) * 0.15);
min -= margin;
max += margin;
const range = max - min || 1;
const n = history.length;
const step = plotW / Math.max(n - 1, 1);
ctx.clearRect(0, 0, w, h);
ctx.fillStyle = "#0d1118";
ctx.fillRect(0, 0, w, h);
const y0 = pad.t + plotH * (1 - (0 - min) / range);
ctx.strokeStyle = "#3a4558";
ctx.lineWidth = 1;
ctx.setLineDash([4, 4]);
ctx.beginPath();
ctx.moveTo(pad.l, y0);
ctx.lineTo(w - pad.r, y0);
ctx.stroke();
ctx.setLineDash([]);
if (large) {
ctx.fillStyle = "#8b9cb3";
ctx.font = "11px system-ui,sans-serif";
ctx.textAlign = "right";
for (let i = 0; i <= 4; i++) {
const v = max - (range * i) / 4;
const y = pad.t + (plotH * i) / 4;
ctx.fillText(`${v.toFixed(3)}%`, pad.l - 6, y + 4);
}
}
ctx.lineWidth = large ? 2 : 1.2;
ctx.beginPath();
history.forEach((p, i) => {
const x = pad.l + i * step;
const y = pad.t + plotH * (1 - (p.rate_pct - min) / range);
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
});
ctx.strokeStyle = "#5c7cfa";
ctx.stroke();
history.forEach((p, i) => {
const x = pad.l + i * step;
const y = pad.t + plotH * (1 - (p.rate_pct - min) / range);
ctx.fillStyle = p.rate_pct >= 0 ? "#0ecb81" : "#f6465d";
ctx.beginPath();
ctx.arc(x, y, large ? 3 : 1.5, 0, Math.PI * 2);
ctx.fill();
});
if (current != null) {
const lx = w - pad.r - 2;
const ly = pad.t + 2;
ctx.fillStyle = current >= 0 ? "#0ecb81" : "#f6465d";
ctx.font = `${large ? 13 : 10}px system-ui,sans-serif`;
ctx.textAlign = "right";
ctx.fillText(`当前 ${current.toFixed(4)}%`, lx, ly + (large ? 12 : 10));
}
}
async function loadMiniFundingChart(box) {
const symbol = box.dataset.symbol;
if (!symbol) return;
box.dataset.loading = "1";
const canvas = box.querySelector("canvas");
const label = box.querySelector(".funding-rate-label");
try {
let bundle = fundingCache.get(symbol);
if (!bundle) {
const res = await fetch(`/api/funding/${symbol}/history?limit=90`);
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).detail || res.statusText);
bundle = await res.json();
fundingCache.set(symbol, bundle);
}
const history = bundle.history || [];
const curPct = bundle.current?.rate_pct ?? 0;
if (label) {
label.textContent = `${curPct >= 0 ? "+" : ""}${curPct.toFixed(4)}%`;
label.className = `funding-rate-label ${curPct >= 0 ? "pct-up" : "pct-down"}`;
}
drawFundingChart(canvas, history, curPct, false);
box.dataset.loaded = "1";
box.title = `${symbol} 资金费率历史 ${history.length} 点,点击放大`;
} catch (e) {
if (label) label.textContent = "—";
box.title = e.message;
} finally {
box.dataset.loading = "0";
}
}
function setupFundingModal() {
let modal = document.getElementById("funding-modal");
if (!modal) {
modal = document.createElement("div");
modal.id = "funding-modal";
modal.className = "chart-modal hidden";
modal.innerHTML = `
<div class="chart-modal-inner">
<button type="button" class="chart-modal-close funding-modal-close">×</button>
<h3 id="funding-modal-title"></h3>
<p class="chart-modal-hint">资金费率历史(约 90 次结算,8h/次)· 虚线为零轴</p>
<div class="chart-modal-canvas-wrap">
<canvas id="funding-modal-canvas"></canvas>
</div>
</div>`;
document.body.appendChild(modal);
modal.querySelector(".funding-modal-close").onclick = () => modal.classList.add("hidden");
modal.addEventListener("click", (e) => {
if (e.target === modal) modal.classList.add("hidden");
});
}
document.body.addEventListener("click", (e) => {
const box = e.target.closest(".mini-funding-chart[data-symbol]");
if (!box || box.dataset.loaded !== "1") return;
const symbol = box.dataset.symbol;
const bundle = fundingCache.get(symbol);
if (!bundle) return;
modal.classList.remove("hidden");
document.getElementById("funding-modal-title").textContent =
`${symbol} · 资金费率`;
drawFundingChart(
document.getElementById("funding-modal-canvas"),
bundle.history,
bundle.current?.rate_pct,
true
);
});
}
setupFundingModal();