Fix: Tick Backtest Chart Not Loading + SSE Connection Drop
Commit: 270383f
Branch: develop → main
Date: 2026-05-13
Problem
After completing a tick-timeframe backtest, the chart showed "Loading chart..." indefinitely and the tool card displayed "Connection closed before the tool finished." with "0 / 1 candles" in the replay bar.
Two independent bugs caused this:
Bug 4a: Chart endpoint missing '1h' candidates for tick timeframe
File: services/backend/src/routes/backtests/backtests-handlers.ts
Root Cause
resolveBacktestCandles calls resolveStoredTimeframe(timeframe) which maps 'tick' → '1m' (the catch-all). This produced a timeframeCandidates array of ['1m', '1', 'tick'].
However, the data-staging pipeline (resolveSourceTimeframe in data-source-adapters.ts) stages tick backtest candles at '1h' for forex symbols (or '1m' for newer runs after the tick fix in commit 53514de). If the backtest was run before that fix, or if any 1m staging failed and fell back to 1h, the chart endpoint returned 0 bars because no '1h' / '60' candidates were tried.
Fix
const timeframeCandidates = Array.from(
new Set([
storedTimeframe,
storedTimeframe === '1m' ? '1' : storedTimeframe === '1h' ? '60' : 'D',
timeframe,
// For 'tick', the data-staging pipeline may have stored candles at 1h
// granularity (e.g. for forex symbols). Include '1h' and its ClickHouse
// alias '60' as fallback candidates so the chart never shows 0 bars.
...(timeframe === 'tick' ? ['1h', '60'] : []),
]),
);
Bug 4b: No snapshot polling after SSE disconnect
File: packages/chat-ui/src/StreamingChatPanel.tsx — BacktestToolCard
Root Cause
When the SSE stream from the backend drops (browser disconnect, Cloudflare DO timeout), the frontend's settleRunningToolCalls helper marks all in-flight tool calls as status: 'error' with call.error = 'Connection closed before the tool finished.'.
The snapshot polling effect had:
const shouldPoll = isRunning || isTimedOut;
With call.status === 'error':
isRunning = falseisTimedOut = !isRunning && !isError && ...→false(becauseisError = true)
So shouldPoll = false. The polling stopped. loadSnapshot() was called once (at effect mount) but if the backtest workflow was still running at that moment, the returned snapshot had 0 or very few candles. After the backtest completed, no further refresh occurred → chart stayed stuck.
Fix
Added an isConnectionDropped flag:
const isConnectionDropped =
isError &&
typeof call.error === 'string' &&
call.error.includes('Connection closed before the tool finished') &&
Boolean(backtestId);
Updated shouldPoll:
const shouldPoll = isRunning || isTimedOut || isConnectionDropped;
Added a self-stopping interval (stops once candles are available):
const timer = window.setInterval(() => {
// Stop polling once the chart has candles — covers the isConnectionDropped
// case where the backtest completes while the SSE stream was disconnected.
if (isConnectionDropped && (liveSnapshotRef.current?.candles?.length ?? 0) > 0) {
window.clearInterval(timer);
return;
}
void loadSnapshot();
}, 2500);
The isConnectionDropped variable is also added to the effect's dependency array.
Behaviour After Fix
| Scenario | Before | After |
|---|---|---|
| Tick backtest chart, data staged as 1h | 0 bars, "Loading chart..." forever | Finds 1h/60 rows, chart renders |
| SSE drops mid-backtest | Stuck on partial candles, never refreshes | Polls every 2.5s until candles load, then stops |
| Normal completed backtest | Unchanged | Unchanged |
| isTimedOut backtest | Unchanged | Unchanged |
Validation
pnpm typecheckinservices/backend— ✅tsc --noEmitinpackages/chat-ui— ✅pnpm lintinservices/backendandservices/frontend— ✅