feat: push chart tail candles over SSE for faster market refresh

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-08 12:24:25 +08:00
parent 4918699276
commit e68e29629e
4 changed files with 141 additions and 49 deletions
+77 -45
View File
@@ -1,10 +1,9 @@
/**
* 中控行情区:K 线 + 成交量;Hub 后台轮询 + SSE 推送;「自动」控制价格轴与视口跟随。
* 中控行情区:K 线 + 成交量;Hub 后台轮询 + SSE 直推尾部 K 线;「自动」控制价格轴与视口跟随。
*/
(function () {
const AUTO_REFRESH_MS = 5000;
const CHART_WATCH_HEARTBEAT_MS = 25000;
const CHART_SSE_FALLBACK_MS = 30000;
const CHART_SSE_FALLBACK_MS = 60000;
const DEFAULT_VISIBLE_BARS = 200;
const CHART_LOAD_LEFT_THRESHOLD = 25;
const CHART_INITIAL_LIMITS = {
@@ -2288,6 +2287,60 @@
}
}
function applyIncomingTailCandles(incoming, meta) {
meta = meta || {};
const vKey = currentViewSeriesKey();
if (!vKey || !lastCandles.length || chartDataLoading) return false;
if (!lastViewKey || vKey !== lastViewKey) return false;
const epochAtStart = chartViewEpoch;
const autoFollow = priceAutoScale;
let savedRange = null;
if (chart) savedRange = chart.timeScale().getVisibleLogicalRange();
if (!incoming || !incoming.length) return false;
if (meta.price_tick != null) {
priceTick = meta.price_tick;
try {
applyChartPriceFormat();
} catch (fmtErr) {
priceTick = null;
applyChartPriceFormat();
}
}
const aligned = alignCandlesToTick(incoming);
if (!autoFollow && applyTailCandlePatch(aligned)) {
/* 手动模式:增量 update,不触碰时间轴 */
} else {
const merged = mergeCandles(lastCandles, aligned, { prepend: false });
applyCandlesToChart(merged, 0, {
preserveRange: false,
skipAutoScale: !autoFollow,
skipRightGap: !autoFollow,
});
if (epochAtStart !== chartViewEpoch) return false;
const n = lastCandles.length;
if (autoFollow) {
applyDefaultVisibleRange();
} else if (savedRange) {
applyPreservedVisibleRange(savedRange, n);
}
}
if (epochAtStart !== chartViewEpoch) return false;
scheduleRangeUiUpdate();
if (posContext) {
updateLivePosPnl();
refreshPosPnlFromBoard();
}
if (meta.series_version != null) {
localSeriesVersion = Number(meta.series_version) || localSeriesVersion;
}
if (meta.chart_version != null) {
localChartVersion = Number(meta.chart_version) || localChartVersion;
}
if (elUpdated) elUpdated.textContent = "数据 " + (meta.updated_at || "--");
tickLiveClock();
return true;
}
async function refreshChartTail() {
const exKey = (elExchange && elExchange.value) || "";
const sym = (elSymbol && elSymbol.value.trim().toUpperCase()) || "";
@@ -2297,9 +2350,6 @@
if (!lastViewKey || vKey !== lastViewKey) return;
const myToken = loadToken;
const epochAtStart = chartViewEpoch;
const autoFollow = priceAutoScale;
let savedRange = null;
if (chart) savedRange = chart.timeScale().getVisibleLogicalRange();
try {
const data = await fetchChartChunk({
exchange_key: exKey,
@@ -2312,43 +2362,12 @@
if (vKey !== lastViewKey) return;
if (epochAtStart !== chartViewEpoch) return;
if (!data.ok || !data.candles || !data.candles.length) return;
if (data.price_tick != null) {
priceTick = data.price_tick;
try {
applyChartPriceFormat();
} catch (fmtErr) {
priceTick = null;
applyChartPriceFormat();
}
}
const incoming = alignCandlesToTick(data.candles);
if (!autoFollow && applyTailCandlePatch(incoming)) {
/* 手动模式:增量更新,不触碰时间轴 */
} else {
const merged = mergeCandles(lastCandles, incoming, { prepend: false });
applyCandlesToChart(merged, 0, {
preserveRange: false,
skipAutoScale: !autoFollow,
skipRightGap: !autoFollow,
});
if (epochAtStart !== chartViewEpoch) return;
const n = lastCandles.length;
if (autoFollow) {
applyDefaultVisibleRange();
} else if (savedRange) {
applyPreservedVisibleRange(savedRange, n);
}
}
if (epochAtStart !== chartViewEpoch) return;
scheduleRangeUiUpdate();
if (posContext) {
updateLivePosPnl();
refreshPosPnlFromBoard();
}
if (data.series_version != null) localSeriesVersion = Number(data.series_version) || localSeriesVersion;
if (data.chart_version != null) localChartVersion = Number(data.chart_version) || localChartVersion;
if (elUpdated) elUpdated.textContent = "数据 " + (data.updated_at || "--");
tickLiveClock();
applyIncomingTailCandles(data.candles, {
price_tick: data.price_tick,
series_version: data.series_version,
chart_version: data.chart_version,
updated_at: data.updated_at,
});
} catch (_) {}
}
@@ -2482,14 +2501,27 @@
chartEventSource.addEventListener("chart", function (ev) {
try {
const st = JSON.parse(ev.data || "{}");
if (st.polling) return;
const ver = Number(st.chart_version) || 0;
const series = st.series || {};
const vKey = currentViewSeriesKey();
const tails = st.tails || {};
const tailPack = vKey && tails[vKey] ? tails[vKey] : null;
if (tailPack && tailPack.candles && tailPack.candles.length) {
if (
applyIncomingTailCandles(tailPack.candles, {
price_tick: tailPack.price_tick,
series_version: tailPack.series_version,
chart_version: ver,
updated_at: tailPack.updated_at || st.updated_at,
})
) {
return;
}
}
const sVer = vKey && series[vKey] ? Number(series[vKey].series_version) || 0 : 0;
const seriesChanged = vKey && sVer > 0 && sVer !== localSeriesVersion;
if (seriesChanged) {
localSeriesVersion = sVer;
localChartVersion = ver;
refreshChartTail();
} else if (posContext) {
updateLivePosPnl();