diff --git a/apps/web/src/components/TradingViewChart.tsx b/apps/web/src/components/TradingViewChart.tsx index 948606f..6a9cd48 100644 --- a/apps/web/src/components/TradingViewChart.tsx +++ b/apps/web/src/components/TradingViewChart.tsx @@ -26,6 +26,8 @@ export const TradingViewChart: React.FC = ({ // 用 ref 保活最新真实价格,避免 5s 定时器闭包捕获到过期值 const priceRef = useRef(currentPrice); const lastTickRef = useRef<{ time: number; price: number } | null>(null); + // 已绘制到哪根柱(防止轮询/心跳重复写入同一根柱) + const lastBarTimeRef = useRef(0); const [timeframe, setTimeframe] = useState('5s'); // 同步最新真实价格到 ref(同时立即打点一次,保证首根柱出现) @@ -91,35 +93,61 @@ export const TradingViewChart: React.FC = ({ seriesRef.current = candlestickSeries; // 2. 从后端 API 异步获取真实 K 线(Indexer 聚合链上成交) - const loadRealKlines = async () => { + const fetchKlines = async (): Promise => { 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 = ({ 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 = ({ 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]);