Replay the bars you ship.
History and real time come back from one endpoint, so nothing about your test path is retrofitted.
Run a rule on the bars.
Fills, realized and open — off one response, at whatever speed you read it.
−0.28%
realized plus unrealized · 29 of 29 bars fed
−0.28%
1 closed trade
0.00%
flat, so nothing to mark
Flat
in above 1.1560
29 daily bars for Euro, from 2026-08-26 to 2026-09-23. The drawn window runs from 1.1323 to 1.1863.
Breakout buys a close above the channel, sells a close below it
- Sell2026-09-111.1598−0.28%
- Buy2026-09-031.1630—
GET /v1/prices/ohlcv?asset=EUR&interval=1d&from=2026-07-26T00:00:00.000Z&to=2026-09-24T00:00:00.000Z&limit=60One response. The cursor is an index into it, not a second call.
- Rule
- breakout · 4-bucket channel · one unit · long only · filled at the close · no fees or slippage
- No size
- 8 of 29
Page the window, then replay.
let from = windowStart while (from < windowEnd) { const { data } = await ohlcv('EUR', { interval: '1d', from }) if (data.length === 0) break for (const bar of data) strategy.onBar(bar) // to is exclusive, so the next page opens on the next bucket from = nextBucket(data[data.length - 1].timestamp) }
One request per page, ordered by construction, and the same four prices the live screens draw.
A gap is an absence.
A bucket nothing was published for is missing from the response, not carried forward and not zeroed.
Null is not zero.
volume is null when no size was published — and on a coarse bucket rather than a partial sum of the minutes that did.
The universe stays yours.
Survivorship, corporate actions and which instruments you were allowed to hold that day are your harness’s — this contract ends at the bar.
Test against what ships.
Read the candles reference, then point a run at it.