/** 资金费率历史曲线 */ 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 = `
资金费率历史(约 90 次结算,8h/次)· 虚线为零轴