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.
Record a live feed
One TOML profile, one daemon, and a tape of the markets you actually watched — plus the two watchdogs that tell you it is really recording.
Run a sweep on the data service
Ship a profile to the box that holds the history, run the grid next to the data, and get back only the ranked report.