Skip to main content

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 = false
  • isTimedOut = !isRunning && !isError && ... → false (because isError = 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​

ScenarioBeforeAfter
Tick backtest chart, data staged as 1h0 bars, "Loading chart..." foreverFinds 1h/60 rows, chart renders
SSE drops mid-backtestStuck on partial candles, never refreshesPolls every 2.5s until candles load, then stops
Normal completed backtestUnchangedUnchanged
isTimedOut backtestUnchangedUnchanged

Validation​

  • pnpm typecheck in services/backend — ✅
  • tsc --noEmit in packages/chat-ui — ✅
  • pnpm lint in services/backend and services/frontend — ✅