feat: push chart tail candles over SSE for faster market refresh
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user