Tick replay
How recorded quote, trade and book rows become the replay stream a strategy sees, and the integrity rule the replayed book obeys.
Tick replay is the offline twin of the live tick recorder. crates/vike-backtest/src/hist_replay.rs turns the quote, trade and book rows a HistStore recorded into the input of StrategyEngine::run_ticks and hands back the same BacktestResult a bar run produces. The whole module sits behind the hist-replay Cargo feature, compiles against the HistStore trait alone and names no DataFusion type; only replay_ticks_streaming, gated a step further behind datafusion-store, opens the concrete DataFusionHist.
From store rows to one stream
The pure core is merge_ticks: a linear three-way step over one symbol's already ts-ascending quotes, trades and book events into a single ts-ordered Vec<Tick>. At an equal timestamp the tie-break is Book, then Quote, then Trade — book first because a live derived quote follows the book application that produced it, quote before trade because that was the original documented policy. crates/vike-backtest/src/hist_replay.rs's merge_ticks states the consequence: reordering that tie-break breaks live/replay parity.
replay_ticks is the bounded reference loader. For each symbol it scans scan_quotes, scan_trades and scan_book_updates over the configured range, merges them, optionally seeds closed-bar context in process through vike_model::consolidate_quotes (or consolidate_trades when no quotes exist — book events never seed bars), and calls the existing run_ticks with no engine changes. A symbol with zero rows of all three kinds is skipped with a tracing::warn! rather than an error, so an all-sparse window degrades to an empty-but-valid result. replay_ticks_streaming obtains quotes and trades through DataFusionHist's row-group streams instead; in its current scope each stream is still collected into a Vec before the merge, so the loader is not O(batch) end to end — its point is API shape plus bit-identical output against replay_ticks, pinned by streaming_equals_bounded_bit_eq. Both entry points share replay_ticks_core and differ only in how a symbol's rows are obtained.
Inside the engine, run_ticks k-way merges the per-symbol streams by timestamp (ties by stream order) and routes each tick by its own payload symbol. on_bar never fires on this path; a strategy sees on_quote_tick, on_trade_tick and on_order_book.
The book integrity rule
A recorded book chain is feed-local contiguous, and the replay treats it exactly as the live consumer did. crates/vike-backtest/src/engine.rs's apply_book_event anchors a per-symbol book on a Snapshot, and applies a Delta only when the shared L2Book::delta_decision law answers Apply under SeqPolicy::Strict — seq == last_seq + 1, the same decision the live depth fold consults. Anything else — a gap, an orphan, a recorded gap-start or stale marker — drops the book until the next snapshot re-anchors it. on_order_book is delivered only on a state-changing apply. The books live on the broker, so the L2Book fill tier and a strategy's quote_vwap and depth_within_price reads answer from the same state the loop maintains.
What the config controls
TickReplayConfig carries the run's default venue, its symbols, the range, and params — the caller's cash, fees and sizer, with fill_model and default_venue overwritten by the loader, since a tick replay only makes sense under the L1 spread-crossing tier tagged to one venue (an explicit L2Book choice is respected — see fill models). Three opt-ins default off and are byte-identical when off:
seed_bar_interval_msseeds closed bars. It must be positive (ReplayError::InvalidBarIntervalotherwise), and with several symbols the per-symbol bar counts must agree, or the loader returnsReplayError::MisalignedSeededBarsinstead of drivingStrategyEngine::new's length assertion into a panic — bar-seeded replay is single-symbol in this phase.snap_to_propertieswiresHistStore::properties_as_ofintoEngineParams.properties, so a fill rounds price to tick and size to step and an opening fill is gated against the grid in effect at that tick's timestamp. It is single-venue, because the grid is looked up under onedefault_venue.seriesreplaces thevenue × symbolsexpansion with an explicit list ofSeriesRefs, each naming its own venue and aSeriesKindlane filter (tick,quote,trade,book), so a Polymarket outcome token's trade tape can ride beside a spot quote series. Symbols must stay unique across the list, andBacktestProfile::validaterefusessnap_to_propertiesalongside a series list naming more than one venue, for the single-venue reason above.
A profile reaches all of this with data.kind = "tick": crates/vike-backtest/src/harness/run.rs's run_backtest builds the config from [engine], and [[data.series]] maps 1:1 onto SeriesRef. Series order matters — the k-way merge breaks an equal-timestamp tie by stream order, so a reference series listed first is delivered before the print it should inform. Three of the tick-only knobs (feed_latency, queue_model, equity_sample_every) are rejected at validation on a bar profile rather than ignored; order_latency_ms and fill_latency_ms are read only by run_backtest's tick arm, so a bar profile that sets either runs as if it had not.
Feed-latency replay
Venue order flatters every backtest on a proxied feed
Recorded ticks carry two clocks: the venue's ts and the machine's local_ts. The default replay orders by the venue clock, so the strategy sees each event the instant the venue stamped it — earlier than any real consumer could. On a proxied feed that systematically flatters the result: the strategy reacts to prints it could not yet have had.
feed_latency: true orders each symbol's merged stream by tick_arrival_ts instead: max(local_ts, ts), with an unstamped local_ts <= 0 falling back to the venue stamp and clock skew clamped up, so the modelled latency is never negative. Nothing is retagged — every tick keeps its venue ts, which remains the only clock run_ticks matches resting orders, advances core.now and stamps the equity curve with. Only delivery order moves, through a stable sort, so an unstamped tape replays byte-identically in either mode. Two limits are stated in the module rather than hidden: the re-order is per symbol, so on a multi-symbol replay the cross-symbol interleaving stays venue-ordered; and a genuinely lagging tape makes the per-tick clock non-monotonic, which is the honest shape of "the consumer saw it later" — combining this mode with the opt-in latency, resolution or settlement arms is out of scope.
See latency models for order latency on this path, queue models for resting maker fills, and the record-and-replay tutorial for a run end to end.
Latency models
Order latency on the tick/book replay path — the entry and response legs, the constant and recorded-series models, the venue-enforced holds, what the response leg delays and what it deliberately does not.
Timeframes
How a backtest synthesises higher timeframes from one fine base stream, and why a coarse bar is never visible before its window closes.