Recording and replay
The host supplies market data and owns files, network connections and playback scheduling. The library supplies data::ReplayDriver, a recording-backed feed that routes symbols through one ReplayClock and the same Sink used by live subscriptions. Existing bar replay remains available for sources that only supply candles.
Give every ReplayEvent a symbol, observation time and merge sequence. A symbol’s time and sequence pair must be unique, including across trades, quotes and book messages. Equal instants across symbols share one clock and are ordered by sequence, then symbol. Payload times and symbols must agree with the envelope. Book snapshot and delta sequence numbers stay separate from the merge sequence and follow the live depth contract.
In the data adapter, write each observed event with RecordingWriter::write and push it with ReplayEvent::push_to. RecordingWriter::capture also accepts an iterator of event results and propagates the first source or storage error. RecordingWriter::new(BufWriter::new(File::create(path)?)) writes a native file; a Vec<u8> works in a browser. finish flushes and propagates storage errors. Periodically record a complete book snapshot with the sequence of the last applied book event. Per-order snapshots list each price’s orders in queue order. The versioned binary format preserves raw fills, optional trade, aggressor and resting order ids, sale conditions and nanosecond timestamps. Writers emit version 2, which also retains resting-order participant identifiers and placement times; readers accept version 1 without that metadata. It uses fixed little-endian fields and length-prefixed records without external dependencies. RecordingReader::read returns events one at a time, reports malformed/truncated input and bounds each record to 16 MiB.
For playback, call ReplayDriver::load(reader, instruments) with the recording stream and one Instrument per symbol, including its tick size. Share the driver through an Arc and attach it with Chart::set_market_replay(Some(driver.clone())), or use ChartBuilder::with_feed for a dedicated playback chart. The driver resolves symbols, rebuilds subscriptions, routes events and publishes its time to quiet symbols. Loading retains the complete recording in memory; seeking rebuilds its preceding events.
Call advance once per frame, outside egui context locks and before drawing, with real elapsed time between frames. set_speed(2.0) plays twice as fast and zero pauses; keep calling advance while paused to publish the shared clock. Advance includes events at the current time and delivers them once. Use advance(Duration::ZERO) to deliver the starting instant without moving time.
driver.seek(time) is inclusive and clears prior bars, trades, quotes, books and heatmap history before rebuilding through that instant. It replays book events from the beginning so heatmap columns before the latest snapshot remain available. New subscriptions, including interval changes, rebuild at the same position. Chart::set_replay_time uses an exclusive cutoff, one nanosecond after the driver’s inclusive position. Chart::set_market_replay(None) restores the chart’s original feed and retained history. Candle-only replay hides live book views; attach the driver to show recorded depth and orders.
Hosts that already manage their own sinks can still use ReplayClock directly. Reset every sink with reset_replay before routing its seek output, and publish the clock time to every sink after advancing or seeking. The lower-level clock seeks book state from the latest preceding snapshot; use the driver when reconstructing heatmap history as well.
Trade, volume, delta and range bars use exactly the live aggregation rules. Volume and delta split a fill at an exact threshold while the raw tape retains it once. Unknown aggressor sides add volume and zero delta. Range bars require the instrument tick size and close at the first real execution that reaches the configured range. A jump across untraded prices can exceed the range; replay never invents trades there. A single fill producing more than 100,000 bars is rejected atomically. To reconstruct historical range bars directly, use aggregate_trades_with_tick_size with the same instrument tick size.
Feeds receiving both an execution event and its separate tape print must give both the same unique trade id. The raw store then counts the fill once and completes missing order ids even when the tape print arrives first. Tick::aggressor_order_id groups known fills into an aggressive order through Chart::combined_trades; resting_order_id identifies each resting order it filled. Per-price volume always uses the raw fills. Chart::order_queue exposes queue positions, largest orders and minimum-size filtered levels for host ladders and heatmaps. It returns no individual orders for aggregated-only sources.