增加K线
This commit is contained in:
+180
-21
@@ -52,6 +52,14 @@ let lwcOnCrosshairMove = null;
|
||||
let lwcOnChartClick = null;
|
||||
let lwcPinnedCandleTime = null;
|
||||
|
||||
const BINANCE_KLINE_WS = "wss://fstream.binance.com/ws";
|
||||
let klineWs = null;
|
||||
let klineWsSymbol = "";
|
||||
let klineWsInterval = "";
|
||||
let klineWsReconnectTimer = null;
|
||||
let modalMetaBase = { count: 0, source: "db", interval: "1d" };
|
||||
let highLowWsRaf = 0;
|
||||
|
||||
function cacheKey(symbol, interval) {
|
||||
return `${symbol}:${interval}`;
|
||||
}
|
||||
@@ -98,6 +106,7 @@ function saveKlineToLS(symbol, interval, candles, source, priceMeta) {
|
||||
}
|
||||
|
||||
function sourceLabel(source) {
|
||||
if (source === "ws") return "实时";
|
||||
if (source === "browser") return "浏览器";
|
||||
if (source === "db") return "本地";
|
||||
if (source === "db_stale") return "本地(旧)";
|
||||
@@ -365,6 +374,151 @@ function candlesToLwc(candles, interval) {
|
||||
return { ohlc, vol };
|
||||
}
|
||||
|
||||
function upsertModalCandleFromWs(k) {
|
||||
const candle = {
|
||||
time: Number(k.t),
|
||||
open: Number(k.o),
|
||||
high: Number(k.h),
|
||||
low: Number(k.l),
|
||||
close: Number(k.c),
|
||||
volume: Number(k.v),
|
||||
quote_volume: Number(k.q),
|
||||
};
|
||||
const idx = lwcModalCandles.findIndex((c) => c.time === candle.time);
|
||||
if (idx >= 0) {
|
||||
lwcModalCandles[idx] = candle;
|
||||
} else if (
|
||||
!lwcModalCandles.length ||
|
||||
candle.time > lwcModalCandles[lwcModalCandles.length - 1].time
|
||||
) {
|
||||
lwcModalCandles.push(candle);
|
||||
const max = limitForInterval(lwcModalInterval);
|
||||
while (lwcModalCandles.length > max) lwcModalCandles.shift();
|
||||
}
|
||||
return candle;
|
||||
}
|
||||
|
||||
function applyRealtimeCandle(candle) {
|
||||
if (!lwcCandleSeries || !lwcVolumeSeries) return;
|
||||
const t = toLwcTime(candle.time, lwcModalInterval);
|
||||
const up = candle.close >= candle.open;
|
||||
lwcCandleSeries.update({
|
||||
time: t,
|
||||
open: candle.open,
|
||||
high: candle.high,
|
||||
low: candle.low,
|
||||
close: candle.close,
|
||||
});
|
||||
lwcVolumeSeries.update({
|
||||
time: t,
|
||||
value: Number(candle.quote_volume || candle.volume || 0),
|
||||
color: up ? COLORS.volUp : COLORS.volDown,
|
||||
});
|
||||
}
|
||||
|
||||
function syncChartCacheFromModal() {
|
||||
const key = cacheKey(chartModalSymbol, lwcModalInterval);
|
||||
const cached = chartDataCache.get(key);
|
||||
if (cached) {
|
||||
cached.candles = lwcModalCandles.slice();
|
||||
cached.source = "ws";
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleHighLowFromWs() {
|
||||
if (highLowWsRaf) return;
|
||||
highLowWsRaf = requestAnimationFrame(() => {
|
||||
highLowWsRaf = 0;
|
||||
updateHighLowForVisibleWindow();
|
||||
});
|
||||
}
|
||||
|
||||
function onWsKlineUpdate(k) {
|
||||
const candle = upsertModalCandleFromWs(k);
|
||||
applyRealtimeCandle(candle);
|
||||
syncChartCacheFromModal();
|
||||
if (lwcPinnedCandleTime == null) {
|
||||
renderOhlcPanel(candle, "实时");
|
||||
}
|
||||
scheduleHighLowFromWs();
|
||||
}
|
||||
|
||||
function isKlineWsLive() {
|
||||
return klineWs?.readyState === WebSocket.OPEN;
|
||||
}
|
||||
|
||||
function updateModalHint() {
|
||||
const hint = document.getElementById("chart-modal-hint");
|
||||
if (!hint) return;
|
||||
const src = isKlineWsLive() ? "实时 WS" : sourceLabel(modalMetaBase.source);
|
||||
hint.textContent = `${modalMetaBase.count} 根 · ${src} · 十字线看当前 · 点击选中 · Esc 关闭`;
|
||||
}
|
||||
|
||||
function disconnectKlineWs() {
|
||||
if (klineWsReconnectTimer) {
|
||||
clearTimeout(klineWsReconnectTimer);
|
||||
klineWsReconnectTimer = null;
|
||||
}
|
||||
if (klineWs) {
|
||||
klineWs.onopen = null;
|
||||
klineWs.onmessage = null;
|
||||
klineWs.onclose = null;
|
||||
klineWs.onerror = null;
|
||||
try {
|
||||
klineWs.close();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
klineWs = null;
|
||||
}
|
||||
klineWsSymbol = "";
|
||||
klineWsInterval = "";
|
||||
}
|
||||
|
||||
function scheduleKlineWsReconnect() {
|
||||
if (!chartModalSymbol || klineWsReconnectTimer) return;
|
||||
klineWsReconnectTimer = setTimeout(() => {
|
||||
klineWsReconnectTimer = null;
|
||||
if (chartModalSymbol) connectKlineWs(chartModalSymbol, chartModalInterval);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function connectKlineWs(symbol, interval) {
|
||||
disconnectKlineWs();
|
||||
const sym = symbol.toUpperCase();
|
||||
const iv = interval.toLowerCase();
|
||||
klineWsSymbol = sym;
|
||||
klineWsInterval = iv;
|
||||
|
||||
const ws = new WebSocket(`${BINANCE_KLINE_WS}/${sym.toLowerCase()}@kline_${iv}`);
|
||||
klineWs = ws;
|
||||
|
||||
ws.onopen = () => updateModalHint();
|
||||
|
||||
ws.onmessage = (ev) => {
|
||||
try {
|
||||
const msg = JSON.parse(ev.data);
|
||||
if (msg.e !== "kline" || !msg.k) return;
|
||||
if (msg.k.s !== sym || msg.k.i !== iv) return;
|
||||
onWsKlineUpdate(msg.k);
|
||||
} catch {
|
||||
/* ignore malformed */
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
if (klineWs === ws) {
|
||||
klineWs = null;
|
||||
updateModalHint();
|
||||
if (chartModalSymbol && klineWsSymbol === sym && klineWsInterval === iv) {
|
||||
scheduleKlineWsReconnect();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = () => updateModalHint();
|
||||
}
|
||||
|
||||
function enqueueCharts(root) {
|
||||
root.querySelectorAll(".mini-chart[data-symbol]").forEach((box) => {
|
||||
const symbol = box.dataset.symbol;
|
||||
@@ -497,27 +651,30 @@ function drawEmptyChart(canvas) {
|
||||
ctx.fillText("暂无数据", w / 2 - 28, h / 2);
|
||||
}
|
||||
|
||||
async function fetchKlines(symbol, interval = DEFAULT_MINI_INTERVAL) {
|
||||
async function fetchKlines(symbol, interval = DEFAULT_MINI_INTERVAL, forceRefresh = false) {
|
||||
const key = cacheKey(symbol, interval);
|
||||
let cached = chartDataCache.get(key);
|
||||
if (cached) return cached;
|
||||
if (!forceRefresh) {
|
||||
const cached = chartDataCache.get(key);
|
||||
if (cached) return cached;
|
||||
|
||||
const ls = loadKlineFromLS(symbol, interval);
|
||||
if (ls) {
|
||||
const priceMeta = rememberPriceMeta(symbol, ls);
|
||||
const result = {
|
||||
candles: ls.candles,
|
||||
source: ls.source || "browser",
|
||||
interval,
|
||||
tick_size: priceMeta?.tick_size,
|
||||
price_precision: priceMeta?.price_precision,
|
||||
};
|
||||
chartDataCache.set(key, result);
|
||||
return result;
|
||||
const ls = loadKlineFromLS(symbol, interval);
|
||||
if (ls) {
|
||||
const priceMeta = rememberPriceMeta(symbol, ls);
|
||||
const result = {
|
||||
candles: ls.candles,
|
||||
source: ls.source || "browser",
|
||||
interval,
|
||||
tick_size: priceMeta?.tick_size,
|
||||
price_precision: priceMeta?.price_precision,
|
||||
};
|
||||
chartDataCache.set(key, result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
const limit = limitForInterval(interval);
|
||||
const res = await fetch(`/api/chart/${symbol}?interval=${interval}&limit=${limit}`);
|
||||
const refreshQ = forceRefresh ? "&refresh=true" : "";
|
||||
const res = await fetch(`/api/chart/${symbol}?interval=${interval}&limit=${limit}${refreshQ}`);
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error(err.detail || res.statusText);
|
||||
@@ -561,6 +718,7 @@ async function loadMiniChart(box) {
|
||||
}
|
||||
|
||||
function destroyLwcChart() {
|
||||
disconnectKlineWs();
|
||||
unbindVisibleRangeHighLow();
|
||||
unbindChartInteractions();
|
||||
clearHighLowAnnotations();
|
||||
@@ -753,14 +911,12 @@ function updateIntervalTabs() {
|
||||
}
|
||||
|
||||
function updateModalMeta(candles, source, interval) {
|
||||
modalMetaBase = { count: candles.length, source, interval };
|
||||
const title = document.getElementById("chart-modal-title");
|
||||
const hint = document.getElementById("chart-modal-hint");
|
||||
if (title) {
|
||||
title.textContent = `${chartModalSymbol} · ${interval.toUpperCase()} K线`;
|
||||
}
|
||||
if (hint) {
|
||||
hint.textContent = `${candles.length} 根 · ${sourceLabel(source)} · 十字线看当前 · 点击选中 · Esc 关闭`;
|
||||
}
|
||||
updateModalHint();
|
||||
}
|
||||
|
||||
async function loadModalChart(interval) {
|
||||
@@ -774,12 +930,15 @@ async function loadModalChart(interval) {
|
||||
try {
|
||||
const { candles, source, tick_size, price_precision } = await fetchKlines(
|
||||
chartModalSymbol,
|
||||
interval
|
||||
interval,
|
||||
true
|
||||
);
|
||||
if (!candles.length) throw new Error("无K线数据");
|
||||
renderLwcChart(candles, interval, { tick_size, price_precision });
|
||||
updateModalMeta(candles, source, interval);
|
||||
connectKlineWs(chartModalSymbol, interval);
|
||||
} catch (e) {
|
||||
disconnectKlineWs();
|
||||
if (hint) hint.textContent = `加载失败: ${e.message}`;
|
||||
destroyLwcChart();
|
||||
if (container) {
|
||||
|
||||
Reference in New Issue
Block a user