vike
Guides

Replay a recorded session

Two different replays — a recorded market tape through the backtest engine, and a command journal through the real core against a state-hash fence.

"Replay" means two things here, and they answer different questions. Tick replay feeds a recorded market tape back through the backtest engine, so a strategy sees the quotes, trades and book events you actually recorded. Journal replay re-folds a live session's command journal through the real single-writer core and proves the result bit-identical. The first tests a strategy; the second tests the run.

Replaying a recorded tape

Set kind = "tick" on the run profile's [data] table and the harness loads from the history store instead of the bar lane (crates/vike-backtest/src/harness/profile.rs's DataKind).

[data]
kind = "tick"
venue = "polymarket"
symbols = ["71321045679252212594626385532706912750332728571942532289631379312455583992563"]
from = "2026-08-01T00"
to   = "2026-08-02T00"

crates/vike-backtest/src/hist_replay.rs's replay_ticks scans each symbol's scan_quotes, scan_trades and scan_book_updates over the range and merges them into one timestamp-ordered stream. The tie-break at equal timestamps is Book, then Quote, then Trade — book first because live derived quotes follow the book application that produced them, and quote-before-trade preserves the original documented policy. A symbol with zero rows of all three kinds in range is sparse-tolerant: a warning and a skip, not an error, so an all-sparse window degrades to an empty-but-valid result rather than failing the run.

A cross-venue slice uses the [[data.series]] form instead, one entry per series with its own venue, symbol and lane kind. Order is preserved and is meaningful: the engine's k-way merge breaks an equal-timestamp tie by stream order, so listing a reference series first delivers a same-millisecond reference sample before the print it should inform.

The default replay is faster than any real consumer

Recorded ticks carry two clocks — the venue's ts and the machine's local_ts. The default orders each stream by the venue clock alone, so a replayed strategy sees every event the instant the venue stamped it, earlier than a live consumer ever could. On a proxied feed that systematically flatters the backtest. engine.feed_latency = true orders each symbol's stream by the arrival clock instead. It retags nothing: every tick keeps its venue ts, and matching stays on venue time — the venue matched when it matched. Only delivery order moves.

Two edge cases in that mode are documented and pinned by tests. A local_ts of zero or less (a fixture, a backfilled row, a pre-local_ts part) falls back to the venue ts, so a partly-stamped tape degrades gracefully instead of collapsing every unstamped tick to the front. A local_ts earlier than the venue ts is clock skew — a machine cannot receive a tick before the venue stamped it — and is clamped up to the venue ts, the only conservative direction, so modelled feed latency is never negative. The re-order is a stable sort, so an unstamped tape replays byte-identically in either mode. In v1 the re-order is per symbol: a single-symbol replay is fully arrival-ordered, while cross-symbol interleaving stays venue-ordered.

feed_latency is tick-mode only and a bar-mode profile setting it is rejected at load, as is the queue-position fill model, which the tick loop alone consults. See tick replay, queue models and run profiles; getting the tape in the first place is record a live feed.

Replaying a command journal

The command journal is off by default, so the first step is to have recorded one. crates/vike-core/src/run_profile.rs's journal_config_from_env resolves it from exactly two places, and the precedence is deliberate: a loaded VIKE_RUN_PROFILE is authoritative — its [sinks.journal] decides, even when that means no journal — and the VIKE_JOURNAL_DIR quick knob is not consulted when a profile is present, so a profile can never be silently overridden. A set-but-malformed profile disables journaling and logs a warning rather than falling through.

Then hand the directory to the replay_journal binary, which takes one argument — the journal directory — and prints a fixed set of lines: the total source record count (commands and snapshots, labelled honestly rather than as the re-folded tail length), the number of snapshots verified, the final state hash, and one line per engine giving its open-order count, its position count and its balance. A failed replay prints REPLAY FAILED and exits non-zero (crates/vike-core/src/bin/replay_journal.rs).

crates/vike-core/src/replay.rs's replay_offline restores the engine from the journal's first Snap, re-folds the whole tail through a real core, and fences the reproduced final hash against the source journal's last Snap hash. Restoring from the latest checkpoint instead would leave nothing to replay on a clean run — its exit snapshot already reflects everything — which is a worthless fence. The replay core's sequence numbers differ from the source's, so per-record comparison is impossible; final-hash equality is the guarantee.

Three details make the fold faithful. The replay core uses a pure no-op ReplayClient, because the original venue events are already journaled and get re-pumped — a client that also emitted events would double the state. A QueueClock replays the tail's recorded now_ms, so timestamp-derived state such as an order's creation time matches. And the replay core mounts no strategy at all: a mounted strategy reacts to non-journaled market data, but its order flow does not, because apply_strategy_intent journals each already-resolved intent write-ahead of applying it. The same shape covers an emulated conditional's fire and its disarm — the decision is journaled write-ahead and re-applied through the identical site, never re-evaluated, so a fire can neither double nor vanish.

Two tails a replay refuses rather than approximates

v1 is single-engine: a multi-engine base returns ReplayError::Unsupported, because per-engine seed_cash is not part of the engine snapshot. A watchdog-tick tail is Unsupported too — the stuck-order and dead-man wall-clock sweeps it carries are not deterministic functions of the journal. A refusal is the honest answer; a fence that quietly skipped them would prove nothing.

Reading an older journal is safe and does not affect determinism. A build writes VERSION and reads anything from MIN_READABLE_VERSION upward, appending to an older directory in place rather than rejecting it, so upgrading a binary never costs an operator their live journal. The rule that makes the range safe is that a version step may only add a record variant or a serde(default)-guarded field; anything that renames or removes one must raise MIN_READABLE_VERSION in the same commit. A version above VERSION — written by a later build — is a hard error.

replay_offline has a sibling that shares the same engine: restore_from_journal restores from the latest Snap, re-folds the usually-empty crash tail, and returns the reproduced engines and coid session with no fence — it is a restore, not a proof. What a restarted node actually picks up is covered by stop, liquidate, restart; the format itself is journal replay.

On this page