fix(web): poll klines API every 5s so new on-chain candles render live

This commit is contained in:
Bot
2026-08-31 02:58:02 +08:00
parent 7f3c6a3775
commit 38e027afa0
+45 -12
View File
@@ -26,6 +26,8 @@ export const TradingViewChart: React.FC<TradingViewChartProps> = ({
// 用 ref 保活最新真实价格,避免 5s 定时器闭包捕获到过期值
const priceRef = useRef<number>(currentPrice);
const lastTickRef = useRef<{ time: number; price: number } | null>(null);
// 已绘制到哪根柱(防止轮询/心跳重复写入同一根柱)
const lastBarTimeRef = useRef<number>(0);
const [timeframe, setTimeframe] = useState<string>('5s');
// 同步最新真实价格到 ref(同时立即打点一次,保证首根柱出现)
@@ -91,35 +93,61 @@ export const TradingViewChart: React.FC<TradingViewChartProps> = ({
seriesRef.current = candlestickSeries;
// 2. 从后端 API 异步获取真实 K 线(Indexer 聚合链上成交)
const loadRealKlines = async () => {
const fetchKlines = async (): Promise<any[] | null> => {
try {
const res = await fetch(
`${API_BASE}/api/v1/charts/klines?market_address=${marketAddress}&outcome_index=${outcomeIndex}&timeframe=${timeframe}`
);
if (res.ok) {
const data = await res.json();
if (data.bars && data.bars.length > 0) {
candlestickSeries.setData(data.bars);
chart.timeScale().fitContent();
const last = data.bars[data.bars.length - 1];
lastTickRef.current = { time: last.time, price: Number(last.close) };
return;
}
if (data.bars && data.bars.length > 0) return data.bars;
}
} catch (e) {
console.warn('Load klines failed:', e);
}
// 无历史成交:不画任何假柱,等 5s 心跳用真实边际价打点
return null;
};
const loadRealKlines = async () => {
const bars = await fetchKlines();
if (bars && bars.length > 0) {
candlestickSeries.setData(bars as any);
chart.timeScale().fitContent();
const last = bars[bars.length - 1];
lastBarTimeRef.current = last.time;
lastTickRef.current = { time: last.time, price: Number(last.close) };
}
// 无历史成交:不画任何假柱,等心跳用真实边际价打点
};
loadRealKlines();
// 3. 每 5 秒用最新真实边际价打点(价格未变化则不重复写入,绝不伪造曲线)
const interval = setInterval(() => {
// 3. 每 5 秒轮询后端:把 indexer 最新聚合出的成交柱增量追加/更新到图表
const refreshInterval = setInterval(async () => {
const bars = await fetchKlines();
if (!bars || bars.length === 0) return;
const series = seriesRef.current;
if (!series) return;
const lastBarTime = lastBarTimeRef.current;
// 仅处理「当前已绘制柱」与「其后新增柱」,避免重复写历史
for (const b of bars) {
if (b.time < lastBarTime) continue;
series.update({ time: b.time, open: b.open, high: b.high, low: b.low, close: b.close });
}
const newest = bars[bars.length - 1];
if (newest.time >= lastBarTime) {
lastBarTimeRef.current = newest.time;
lastTickRef.current = { time: newest.time, price: Number(newest.close) };
}
}, BAR_SECONDS * 1000);
// 4. 每 5 秒用最新真实边际价打点(仅在最新成交柱之后,价格未变化则不重复写入,绝不伪造曲线)
const heartbeatInterval = setInterval(() => {
const p = priceRef.current;
const series = seriesRef.current;
if (p <= 0 || !series) return;
const barTime = barTimeOfNow();
if (barTime < lastBarTimeRef.current) return;
const last = lastTickRef.current;
if (last && last.time === barTime && last.price === p) return;
series.update({
@@ -129,6 +157,9 @@ export const TradingViewChart: React.FC<TradingViewChartProps> = ({
low: p,
close: p,
});
if (barTime > lastBarTimeRef.current) {
lastBarTimeRef.current = barTime;
}
lastTickRef.current = { time: barTime, price: p };
}, BAR_SECONDS * 1000);
@@ -140,11 +171,13 @@ export const TradingViewChart: React.FC<TradingViewChartProps> = ({
window.addEventListener('resize', handleResize);
return () => {
clearInterval(interval);
clearInterval(refreshInterval);
clearInterval(heartbeatInterval);
window.removeEventListener('resize', handleResize);
chart.remove();
chartRef.current = null;
seriesRef.current = null;
lastBarTimeRef.current = 0;
};
}, [marketAddress, outcomeIndex, timeframe]);