增加k线图
This commit is contained in:
@@ -227,6 +227,7 @@ class BinanceFuturesClient:
|
|||||||
"low": float(k[3]),
|
"low": float(k[3]),
|
||||||
"close": float(k[4]),
|
"close": float(k[4]),
|
||||||
"volume": float(k[5]),
|
"volume": float(k[5]),
|
||||||
|
"quote_volume": float(k[7]),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return candles
|
return candles
|
||||||
|
|||||||
+1
-1
@@ -107,7 +107,7 @@ function renderTable(tableId, tbody) {
|
|||||||
<td class="symbol-cell"><strong>${row.symbol}</strong></td>
|
<td class="symbol-cell"><strong>${row.symbol}</strong></td>
|
||||||
<td class="chart-cell">
|
<td class="chart-cell">
|
||||||
<div class="mini-chart" data-symbol="${row.symbol}">
|
<div class="mini-chart" data-symbol="${row.symbol}">
|
||||||
<canvas width="300" height="64"></canvas>
|
<canvas></canvas>
|
||||||
<span class="chart-status"></span>
|
<span class="chart-status"></span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
+157
-74
@@ -1,10 +1,23 @@
|
|||||||
/** 迷你日 K 线图(Canvas) + 限速队列 */
|
/** 日 K + 成交量(Canvas 高清) */
|
||||||
|
|
||||||
const chartDataCache = new Map();
|
const chartDataCache = new Map();
|
||||||
const chartQueue = [];
|
const chartQueue = [];
|
||||||
let chartQueueRunning = false;
|
let chartQueueRunning = false;
|
||||||
const CHART_FETCH_GAP_MS = 120;
|
const CHART_FETCH_GAP_MS = 120;
|
||||||
|
|
||||||
|
const COLORS = {
|
||||||
|
bg: "#0d1118",
|
||||||
|
grid: "#2a3548",
|
||||||
|
up: "#0ecb81",
|
||||||
|
down: "#f6465d",
|
||||||
|
volUp: "rgba(14, 203, 129, 0.55)",
|
||||||
|
volDown: "rgba(246, 70, 93, 0.55)",
|
||||||
|
text: "#8b9cb3",
|
||||||
|
};
|
||||||
|
|
||||||
|
const MINI_SIZE = { w: 380, h: 100 };
|
||||||
|
const MODAL_SIZE = { w: 1280, h: 720 };
|
||||||
|
|
||||||
function enqueueCharts(root) {
|
function enqueueCharts(root) {
|
||||||
root.querySelectorAll(".mini-chart[data-symbol]").forEach((box) => {
|
root.querySelectorAll(".mini-chart[data-symbol]").forEach((box) => {
|
||||||
const symbol = box.dataset.symbol;
|
const symbol = box.dataset.symbol;
|
||||||
@@ -30,6 +43,136 @@ function sleep(ms) {
|
|||||||
return new Promise((r) => setTimeout(r, ms));
|
return new Promise((r) => setTimeout(r, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function volOf(c) {
|
||||||
|
return Number(c.quote_volume || c.volume || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupCanvas(canvas, displayW, displayH) {
|
||||||
|
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||||
|
canvas.style.width = `${displayW}px`;
|
||||||
|
canvas.style.height = `${displayH}px`;
|
||||||
|
const pw = Math.floor(displayW * dpr);
|
||||||
|
const ph = Math.floor(displayH * dpr);
|
||||||
|
if (canvas.width !== pw || canvas.height !== ph) {
|
||||||
|
canvas.width = pw;
|
||||||
|
canvas.height = ph;
|
||||||
|
}
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
|
return { ctx, w: displayW, h: displayH };
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawCandlestickChart(canvas, candles, options = {}) {
|
||||||
|
if (!canvas || !candles.length) return;
|
||||||
|
|
||||||
|
const large = options.large === true;
|
||||||
|
const size = large ? MODAL_SIZE : MINI_SIZE;
|
||||||
|
const volRatio = large ? 0.22 : 0.32;
|
||||||
|
const pad = large
|
||||||
|
? { t: 16, r: 16, b: 28, l: 56 }
|
||||||
|
: { t: 6, r: 6, b: 14, l: 6 };
|
||||||
|
|
||||||
|
const { ctx, w, h } = setupCanvas(canvas, size.w, size.h);
|
||||||
|
const priceH = (h - pad.t - pad.b) * (1 - volRatio);
|
||||||
|
const volH = (h - pad.t - pad.b) * volRatio;
|
||||||
|
const volTop = pad.t + priceH + (large ? 8 : 4);
|
||||||
|
const plotW = w - pad.l - pad.r;
|
||||||
|
const n = candles.length;
|
||||||
|
const step = plotW / n;
|
||||||
|
|
||||||
|
let pMin = Infinity;
|
||||||
|
let pMax = -Infinity;
|
||||||
|
let vMax = 0;
|
||||||
|
for (const c of candles) {
|
||||||
|
pMin = Math.min(pMin, c.low);
|
||||||
|
pMax = Math.max(pMax, c.high);
|
||||||
|
vMax = Math.max(vMax, volOf(c));
|
||||||
|
}
|
||||||
|
const pRange = pMax - pMin || 1;
|
||||||
|
vMax = vMax || 1;
|
||||||
|
|
||||||
|
const yPrice = (p) => pad.t + priceH * (1 - (p - pMin) / pRange);
|
||||||
|
const yVol = (v) => volTop + volH * (1 - v / vMax);
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, w, h);
|
||||||
|
ctx.fillStyle = COLORS.bg;
|
||||||
|
ctx.fillRect(0, 0, w, h);
|
||||||
|
|
||||||
|
if (large) {
|
||||||
|
ctx.strokeStyle = COLORS.grid;
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
for (let i = 0; i <= 4; i++) {
|
||||||
|
const y = pad.t + (priceH * i) / 4;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(pad.l, y);
|
||||||
|
ctx.lineTo(w - pad.r, y);
|
||||||
|
ctx.stroke();
|
||||||
|
const price = pMax - (pRange * i) / 4;
|
||||||
|
ctx.fillStyle = COLORS.text;
|
||||||
|
ctx.font = "11px Segoe UI, system-ui, sans-serif";
|
||||||
|
ctx.textAlign = "right";
|
||||||
|
ctx.fillText(price.toPrecision(6), pad.l - 8, y + 4);
|
||||||
|
}
|
||||||
|
ctx.fillStyle = COLORS.text;
|
||||||
|
ctx.font = "12px Segoe UI, system-ui, sans-serif";
|
||||||
|
ctx.textAlign = "left";
|
||||||
|
ctx.fillText("价格", pad.l, pad.t - 4);
|
||||||
|
ctx.fillText("成交量", pad.l, volTop - 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.strokeStyle = COLORS.grid;
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(pad.l, volTop - 2);
|
||||||
|
ctx.lineTo(w - pad.r, volTop - 2);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
const bodyW = Math.max(large ? 2 : 1, step * (large ? 0.72 : 0.68));
|
||||||
|
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const c = candles[i];
|
||||||
|
const up = c.close >= c.open;
|
||||||
|
const x = pad.l + i * step + step / 2;
|
||||||
|
const color = up ? COLORS.up : COLORS.down;
|
||||||
|
const volColor = up ? COLORS.volUp : COLORS.volDown;
|
||||||
|
|
||||||
|
const yHigh = yPrice(c.high);
|
||||||
|
const yLow = yPrice(c.low);
|
||||||
|
const yOpen = yPrice(c.open);
|
||||||
|
const yClose = yPrice(c.close);
|
||||||
|
|
||||||
|
ctx.strokeStyle = color;
|
||||||
|
ctx.lineWidth = large ? 1.5 : 1;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, yHigh);
|
||||||
|
ctx.lineTo(x, yLow);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
const top = Math.min(yOpen, yClose);
|
||||||
|
const bodyHeight = Math.max(large ? 2 : 1, Math.abs(yClose - yOpen));
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.fillRect(x - bodyW / 2, top, bodyW, bodyHeight);
|
||||||
|
|
||||||
|
const v = volOf(c);
|
||||||
|
const barH = volH * (v / vMax);
|
||||||
|
if (barH > 0.5) {
|
||||||
|
ctx.fillStyle = volColor;
|
||||||
|
ctx.fillRect(x - bodyW / 2, yVol(v), bodyW, barH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawEmptyChart(canvas, large = false) {
|
||||||
|
if (!canvas) return;
|
||||||
|
const size = large ? MODAL_SIZE : MINI_SIZE;
|
||||||
|
const { ctx, w, h } = setupCanvas(canvas, size.w, size.h);
|
||||||
|
ctx.fillStyle = "#1a2332";
|
||||||
|
ctx.fillRect(0, 0, w, h);
|
||||||
|
ctx.fillStyle = COLORS.text;
|
||||||
|
ctx.font = "13px sans-serif";
|
||||||
|
ctx.fillText("暂无数据", w / 2 - 28, h / 2);
|
||||||
|
}
|
||||||
|
|
||||||
async function loadMiniChart(box) {
|
async function loadMiniChart(box) {
|
||||||
const symbol = box.dataset.symbol;
|
const symbol = box.dataset.symbol;
|
||||||
if (!symbol) return;
|
if (!symbol) return;
|
||||||
@@ -53,85 +196,21 @@ async function loadMiniChart(box) {
|
|||||||
chartDataCache.set(symbol, candles);
|
chartDataCache.set(symbol, candles);
|
||||||
}
|
}
|
||||||
if (!candles.length) throw new Error("无K线数据");
|
if (!candles.length) throw new Error("无K线数据");
|
||||||
drawCandlestickChart(canvas, candles);
|
drawCandlestickChart(canvas, candles, { large: false });
|
||||||
box.dataset.loaded = "1";
|
box.dataset.loaded = "1";
|
||||||
const srcLabel =
|
const srcLabel =
|
||||||
source === "db" ? "本地" : source === "db_stale" ? "本地(旧)" : source === "cache" ? "缓存" : "同步";
|
source === "db" ? "本地" : source === "db_stale" ? "本地(旧)" : source === "cache" ? "缓存" : "同步";
|
||||||
if (status) status.textContent = `${candles.length}日·${srcLabel}`;
|
if (status) status.textContent = `${candles.length}日·${srcLabel}`;
|
||||||
box.title = `${symbol} 最近${candles.length}根日K (${srcLabel})`;
|
box.title = `${symbol} 日K+量 ${candles.length}根 (${srcLabel}),点击放大`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (status) status.textContent = "—";
|
if (status) status.textContent = "—";
|
||||||
box.title = `${symbol}: ${e.message}`;
|
box.title = `${symbol}: ${e.message}`;
|
||||||
drawEmptyChart(canvas);
|
drawEmptyChart(canvas, false);
|
||||||
} finally {
|
} finally {
|
||||||
box.dataset.loading = "0";
|
box.dataset.loading = "0";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawEmptyChart(canvas) {
|
|
||||||
if (!canvas) return;
|
|
||||||
const ctx = canvas.getContext("2d");
|
|
||||||
const w = canvas.width;
|
|
||||||
const h = canvas.height;
|
|
||||||
ctx.clearRect(0, 0, w, h);
|
|
||||||
ctx.fillStyle = "#3a4558";
|
|
||||||
ctx.fillRect(0, 0, w, h);
|
|
||||||
ctx.fillStyle = "#8b9cb3";
|
|
||||||
ctx.font = "11px sans-serif";
|
|
||||||
ctx.fillText("暂无", 8, h / 2 + 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawCandlestickChart(canvas, candles) {
|
|
||||||
if (!canvas || !candles.length) return;
|
|
||||||
const ctx = canvas.getContext("2d");
|
|
||||||
const w = canvas.width;
|
|
||||||
const h = canvas.height;
|
|
||||||
const pad = { t: 4, r: 4, b: 4, l: 4 };
|
|
||||||
const plotW = w - pad.l - pad.r;
|
|
||||||
const plotH = h - pad.t - pad.b;
|
|
||||||
|
|
||||||
let min = Infinity;
|
|
||||||
let max = -Infinity;
|
|
||||||
for (const c of candles) {
|
|
||||||
min = Math.min(min, c.low);
|
|
||||||
max = Math.max(max, c.high);
|
|
||||||
}
|
|
||||||
const range = max - min || 1;
|
|
||||||
const n = candles.length;
|
|
||||||
const step = plotW / n;
|
|
||||||
const bodyW = Math.max(1, step * 0.65);
|
|
||||||
|
|
||||||
ctx.clearRect(0, 0, w, h);
|
|
||||||
ctx.fillStyle = "#121820";
|
|
||||||
ctx.fillRect(0, 0, w, h);
|
|
||||||
|
|
||||||
const yOf = (price) => pad.t + plotH * (1 - (price - min) / range);
|
|
||||||
|
|
||||||
for (let i = 0; i < n; i++) {
|
|
||||||
const c = candles[i];
|
|
||||||
const up = c.close >= c.open;
|
|
||||||
const x = pad.l + i * step + step / 2;
|
|
||||||
const yHigh = yOf(c.high);
|
|
||||||
const yLow = yOf(c.low);
|
|
||||||
const yOpen = yOf(c.open);
|
|
||||||
const yClose = yOf(c.close);
|
|
||||||
const color = up ? "#0ecb81" : "#f6465d";
|
|
||||||
|
|
||||||
ctx.strokeStyle = color;
|
|
||||||
ctx.lineWidth = 1;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(x, yHigh);
|
|
||||||
ctx.lineTo(x, yLow);
|
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
const top = Math.min(yOpen, yClose);
|
|
||||||
const bodyH = Math.max(1, Math.abs(yClose - yOpen));
|
|
||||||
ctx.fillStyle = color;
|
|
||||||
ctx.fillRect(x - bodyW / 2, top, bodyW, bodyH);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 点击放大 */
|
|
||||||
function setupChartModal() {
|
function setupChartModal() {
|
||||||
let modal = document.getElementById("chart-modal");
|
let modal = document.getElementById("chart-modal");
|
||||||
if (!modal) {
|
if (!modal) {
|
||||||
@@ -142,7 +221,10 @@ function setupChartModal() {
|
|||||||
<div class="chart-modal-inner">
|
<div class="chart-modal-inner">
|
||||||
<button type="button" class="chart-modal-close" aria-label="关闭">×</button>
|
<button type="button" class="chart-modal-close" aria-label="关闭">×</button>
|
||||||
<h3 id="chart-modal-title"></h3>
|
<h3 id="chart-modal-title"></h3>
|
||||||
<canvas id="chart-modal-canvas" width="900" height="360"></canvas>
|
<p class="chart-modal-hint">日K + 成交量 · 300根 · 滚轮可缩放页面</p>
|
||||||
|
<div class="chart-modal-canvas-wrap">
|
||||||
|
<canvas id="chart-modal-canvas"></canvas>
|
||||||
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
document.body.appendChild(modal);
|
document.body.appendChild(modal);
|
||||||
modal.querySelector(".chart-modal-close").onclick = () =>
|
modal.querySelector(".chart-modal-close").onclick = () =>
|
||||||
@@ -150,6 +232,9 @@ function setupChartModal() {
|
|||||||
modal.addEventListener("click", (e) => {
|
modal.addEventListener("click", (e) => {
|
||||||
if (e.target === modal) modal.classList.add("hidden");
|
if (e.target === modal) modal.classList.add("hidden");
|
||||||
});
|
});
|
||||||
|
document.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Escape") modal.classList.add("hidden");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
document.body.addEventListener("click", (e) => {
|
document.body.addEventListener("click", (e) => {
|
||||||
@@ -160,11 +245,9 @@ function setupChartModal() {
|
|||||||
if (!candles) return;
|
if (!candles) return;
|
||||||
modal.classList.remove("hidden");
|
modal.classList.remove("hidden");
|
||||||
document.getElementById("chart-modal-title").textContent =
|
document.getElementById("chart-modal-title").textContent =
|
||||||
`${symbol} · 日K ${candles.length}根`;
|
`${symbol} · 日K + 成交量(${candles.length} 根)`;
|
||||||
drawCandlestickChart(
|
const canvas = document.getElementById("chart-modal-canvas");
|
||||||
document.getElementById("chart-modal-canvas"),
|
drawCandlestickChart(canvas, candles, { large: true });
|
||||||
candles
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<h1>币安 U本位合约 · 成交额排名</h1>
|
<h1>币安 U本位合约 · 成交额排名</h1>
|
||||||
<p class="subtitle">北京时间 08:00 切日 · Top30 · 高亮:≥1000万 USDT / |涨跌|≥5% · 合约右侧 300 日K · 点击图表放大</p>
|
<p class="subtitle">北京时间 08:00 切日 · Top30 · 合约右侧 300 日K+成交量 · 点击图表放大查看</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="panel" id="panel-yesterday">
|
<section class="panel" id="panel-yesterday">
|
||||||
|
|||||||
+26
-15
@@ -22,7 +22,7 @@ body {
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
max-width: 1280px;
|
max-width: 1380px;
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,7 +225,7 @@ button:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chart-col {
|
.chart-col {
|
||||||
min-width: 320px;
|
min-width: 400px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
@@ -237,19 +237,17 @@ button:hover {
|
|||||||
|
|
||||||
.mini-chart {
|
.mini-chart {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 300px;
|
width: 380px;
|
||||||
height: 64px;
|
height: 100px;
|
||||||
cursor: zoom-in;
|
cursor: zoom-in;
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
background: #121820;
|
background: #0d1118;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mini-chart canvas {
|
.mini-chart canvas {
|
||||||
display: block;
|
display: block;
|
||||||
width: 300px;
|
|
||||||
height: 64px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-status {
|
.chart-status {
|
||||||
@@ -278,14 +276,29 @@ button:hover {
|
|||||||
.chart-modal-inner {
|
.chart-modal-inner {
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 10px;
|
border-radius: 12px;
|
||||||
padding: 1rem 1.25rem;
|
padding: 1.25rem 1.5rem 1.5rem;
|
||||||
max-width: 95vw;
|
max-width: 96vw;
|
||||||
|
max-height: 96vh;
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-modal-inner h3 {
|
.chart-modal-inner h3 {
|
||||||
|
margin: 0 0 0.25rem;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-modal-hint {
|
||||||
margin: 0 0 0.75rem;
|
margin: 0 0 0.75rem;
|
||||||
font-size: 1rem;
|
font-size: 0.8rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-modal-canvas-wrap {
|
||||||
|
overflow: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: #0d1118;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-modal-close {
|
.chart-modal-close {
|
||||||
@@ -300,7 +313,5 @@ button:hover {
|
|||||||
|
|
||||||
#chart-modal-canvas {
|
#chart-modal-canvas {
|
||||||
display: block;
|
display: block;
|
||||||
max-width: 100%;
|
background: #0d1118;
|
||||||
border-radius: 6px;
|
|
||||||
background: #121820;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user