增加K线

This commit is contained in:
dekun
2026-05-30 10:48:57 +08:00
parent e9f1a6a46f
commit 48df49b09c
3 changed files with 266 additions and 11 deletions
+160 -7
View File
@@ -40,6 +40,8 @@ let lwcChart = null;
let lwcCandleSeries = null;
let lwcVolumeSeries = null;
let lwcResizeObserver = null;
let lwcPriceLines = [];
const symbolPriceMeta = new Map();
function cacheKey(symbol, interval) {
return `${symbol}:${interval}`;
@@ -69,11 +71,18 @@ function loadKlineFromLS(symbol, interval) {
}
}
function saveKlineToLS(symbol, interval, candles, source) {
function saveKlineToLS(symbol, interval, candles, source, priceMeta) {
try {
localStorage.setItem(
LS_KLINE_PREFIX + symbol + "_" + interval,
JSON.stringify({ ts: Date.now(), candles, source, interval })
JSON.stringify({
ts: Date.now(),
candles,
source,
interval,
tick_size: priceMeta?.tick_size,
price_precision: priceMeta?.price_precision,
})
);
} catch {
/* quota */
@@ -88,6 +97,53 @@ function sourceLabel(source) {
return "同步";
}
function parseMinMove(tickSize) {
const n = Number(tickSize);
return Number.isFinite(n) && n > 0 ? n : 0.01;
}
function formatPrice(price, precision) {
return Number(price).toFixed(precision);
}
function rememberPriceMeta(symbol, meta) {
if (!meta?.tick_size) return null;
const priceMeta = {
tick_size: meta.tick_size,
price_precision: Number(meta.price_precision ?? 2),
};
symbolPriceMeta.set(symbol, priceMeta);
return priceMeta;
}
function getPriceMeta(symbol, fallback) {
return (
symbolPriceMeta.get(symbol) ||
(fallback?.tick_size ? rememberPriceMeta(symbol, fallback) : null) || {
tick_size: "0.01",
price_precision: 2,
}
);
}
function findCandleExtremes(candles, interval) {
let maxHigh = -Infinity;
let minLow = Infinity;
let highTime = null;
let lowTime = null;
for (const c of candles) {
if (c.high > maxHigh) {
maxHigh = c.high;
highTime = toLwcTime(c.time, interval);
}
if (c.low < minLow) {
minLow = c.low;
lowTime = toLwcTime(c.time, interval);
}
}
return { maxHigh, minLow, highTime, lowTime };
}
function toLwcTime(ms, interval) {
if (interval === "1d" || interval === "1w") {
const d = new Date(ms);
@@ -261,7 +317,14 @@ async function fetchKlines(symbol, interval = DEFAULT_MINI_INTERVAL) {
const ls = loadKlineFromLS(symbol, interval);
if (ls) {
const result = { candles: ls.candles, source: ls.source || "browser", interval };
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;
}
@@ -273,13 +336,16 @@ async function fetchKlines(symbol, interval = DEFAULT_MINI_INTERVAL) {
throw new Error(err.detail || res.statusText);
}
const data = await res.json();
const priceMeta = rememberPriceMeta(symbol, data);
const result = {
candles: data.candles || [],
source: data.source || "db",
interval,
tick_size: priceMeta.tick_size,
price_precision: priceMeta.price_precision,
};
chartDataCache.set(key, result);
saveKlineToLS(symbol, interval, result.candles, result.source);
saveKlineToLS(symbol, interval, result.candles, result.source, priceMeta);
return result;
}
@@ -308,6 +374,7 @@ async function loadMiniChart(box) {
}
function destroyLwcChart() {
clearHighLowAnnotations();
if (lwcResizeObserver) {
lwcResizeObserver.disconnect();
lwcResizeObserver = null;
@@ -320,6 +387,85 @@ function destroyLwcChart() {
}
}
function clearHighLowAnnotations() {
if (lwcCandleSeries) {
lwcPriceLines.forEach((line) => {
try {
lwcCandleSeries.removePriceLine(line);
} catch {
/* already removed */
}
});
lwcCandleSeries.setMarkers([]);
}
lwcPriceLines = [];
}
function applySeriesPriceFormat(priceMeta) {
if (!lwcCandleSeries) return;
const precision = priceMeta.price_precision;
const minMove = parseMinMove(priceMeta.tick_size);
lwcCandleSeries.applyOptions({
priceFormat: {
type: "price",
precision,
minMove,
},
});
}
function applyHighLowAnnotations(candles, interval, priceMeta) {
if (!lwcCandleSeries || !candles.length) return;
clearHighLowAnnotations();
const { maxHigh, minLow, highTime, lowTime } = findCandleExtremes(candles, interval);
if (!Number.isFinite(maxHigh) || !Number.isFinite(minLow)) return;
const precision = priceMeta.price_precision;
const highText = formatPrice(maxHigh, precision);
const lowText = formatPrice(minLow, precision);
lwcPriceLines.push(
lwcCandleSeries.createPriceLine({
price: maxHigh,
color: COLORS.up,
lineWidth: 1,
lineStyle: LightweightCharts.LineStyle.Dotted,
axisLabelVisible: true,
title: `最高 ${highText}`,
}),
lwcCandleSeries.createPriceLine({
price: minLow,
color: COLORS.down,
lineWidth: 1,
lineStyle: LightweightCharts.LineStyle.Dotted,
axisLabelVisible: true,
title: `最低 ${lowText}`,
})
);
const markers = [];
if (highTime != null) {
markers.push({
time: highTime,
position: "aboveBar",
color: COLORS.up,
shape: "circle",
text: `${highText}`,
});
}
if (lowTime != null) {
markers.push({
time: lowTime,
position: "belowBar",
color: COLORS.down,
shape: "circle",
text: `${lowText}`,
});
}
lwcCandleSeries.setMarkers(markers);
}
function ensureLwcChart(container) {
if (typeof LightweightCharts === "undefined") {
container.innerHTML = '<p class="chart-lwc-fallback">图表库加载失败</p>';
@@ -380,16 +526,20 @@ function ensureLwcChart(container) {
return lwcChart;
}
function renderLwcChart(candles, interval) {
function renderLwcChart(candles, interval, priceMeta) {
const container = document.getElementById("chart-modal-container");
if (!container) return;
if (!lwcChart) ensureLwcChart(container);
if (!lwcCandleSeries || !lwcVolumeSeries) return;
const meta = getPriceMeta(chartModalSymbol, priceMeta);
applySeriesPriceFormat(meta);
const { ohlc, vol } = candlesToLwc(candles, interval);
lwcCandleSeries.setData(ohlc);
lwcVolumeSeries.setData(vol);
applyHighLowAnnotations(candles, interval, meta);
lwcChart.timeScale().fitContent();
}
@@ -419,9 +569,12 @@ async function loadModalChart(interval) {
if (hint) hint.textContent = "加载中…";
try {
const { candles, source } = await fetchKlines(chartModalSymbol, interval);
const { candles, source, tick_size, price_precision } = await fetchKlines(
chartModalSymbol,
interval
);
if (!candles.length) throw new Error("无K线数据");
renderLwcChart(candles, interval);
renderLwcChart(candles, interval, { tick_size, price_precision });
updateModalMeta(candles, source, interval);
} catch (e) {
if (hint) hint.textContent = `加载失败: ${e.message}`;