增加资金费率

This commit is contained in:
dekun
2026-05-22 14:00:19 +08:00
parent d7f7259ee0
commit 71ed38b32d
13 changed files with 603 additions and 14 deletions
+187
View File
@@ -0,0 +1,187 @@
/** 资金费率历史曲线 */
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();