Skip to main content

Replay

TradeScript chart time-range menu open above the historical time scale
Replay is host-composed, but its available start range comes from the same loaded history and time scale shown here.

Replay steps through historical bars already loaded on the chart without pretending the replay clock is live market time. It is driven by a replay controller — widget.replay() on a mounted widget, or a self-managed ReplayController — and surfaced in the UI through the features.replay control bar.

Data requirements

Replay operates on the chart's loaded bars, not on new datafeed requests. When picking starts, the controller captures a stable copy of the loaded series so bar indices stay fixed for the whole session even while live updates continue to arrive. Consequences:

  • History must be loaded first — any picking or stepping call with no loaded bars throws Replay requires loaded chart bars. That is the unavailable-history state: catch it and tell the user to load more history before offering replay.
  • The replayable range is exactly the loaded range. Bar indices are zero-based positions in that captured series; out-of-range indices throw Replay bar index is outside the loaded range.
  • Exiting releases the captured copy; the next session captures fresh bars.

Replay lifecycle

ReplayState.status is one of idle, picking, paused, playing, or ended:

StatusMeaningAvailable actionsChart behavior
idleReplay not activestartPickingNormal live/realtime behavior
pickingChoosing a start barpickStartBar, cancelPickingLoaded bars captured; timer stopped
pausedHolding a replay barplay, stepForward, stepBackward, jumpToBar, setSpeed, exitCursor bar scrolled to center
playingAuto-advancingpause, setSpeed, jumpToBar, exitOne bar per timer tick at the configured speed
endedCursor on the final barjumpToBar (restart earlier), exitHolds the last available bar

End-of-data is not a separate status flag: playing into the last bar, stepping onto it, or jumping to it transitions to ended and emits the ended event. Unavailable history never enters the lifecycle at all — the entry calls throw instead.

Driving replay

Use the widget's controller accessor; every action returns the resulting ReplayState snapshot:

const replay = widget.replay()

replay.startPicking() // status: 'picking'
replay.pickStartBar(120) // status: 'paused', cursor on bar 120

replay.play() // status: 'playing'
replay.setSpeed(10) // bars per second; timer restarts if playing
replay.pause() // status: 'paused'

replay.stepForward() // +1 bar, stays 'paused'
replay.stepBackward(5) // -5 bars, clamped to bar 0
replay.jumpToBar(200) // jump; landing on the last bar => 'ended'

replay.exit() // status: 'idle', captured bars released

Speed is bars per second: the auto-advance timer ticks every max(16, round(1000 / speed)) ms. Invalid speeds (zero, negative, non-finite) throw.

Subscribe for state transitions — each action emits its specific event (picking-started, picking-cancelled, started, played, paused, stepped, jumped, speed-changed, ended, exited, saved, loaded) followed by a generic state event. The same events are mirrored onto the chart event bus as replay-change, so hosts can listen in one place:

const stop = replay.subscribe((event) => {
console.log(event.type, event.state?.status)
})

chart.on('replay-change', (event) => updateReplayToolbar(event.state))

Hosts without a mounted widget can construct the controller directly with new ReplayController({ chart, storage?, context?, scrollOnUpdate? }) from @tradescript/pro/sdk, and must call destroy() when done with it.

Controls

features.replay configures the built-in surface; every switch defaults to enabled:

OptionControls
controlsThe on-chart replay control bar
pickingInteractive start-bar picking on the chart
speedControlThe playback-speed control
persistenceReplay-state persistence through the chart storage adapter

A host-composed toolbar needs nothing beyond the controller: render buttons from getState() and the event stream, and call the actions above.

Live and replay transitions

  • Entering replay does not tear down live subscriptions; the captured bar copy isolates replay indices from live updates arriving behind it.
  • Picks, steps, jumps, and loads scroll the chart to the cursor bar (disable with scrollOnUpdate: false on a self-managed controller).
  • exit() stops the timer, resets to idle while keeping the configured speed, releases the captured bars, and returns the chart to normal live behavior. Destroying the widget destroys the controller.

Persisting replay position

With a chart storage adapter that implements saveReplayState / loadReplayState (and features.replay.persistence on), the controller can round-trip its state:

await replay.saveState() // stamps persistedAt, emits 'saved'

const restored = await replay.loadState()
// null when nothing was stored; a persisted 'playing' status is demoted to
// 'paused' so playback never auto-resumes on load.

Both throw when no adapter with the matching method is configured. The chart's symbol and interval fill in any storage-context fields the request omits.

Verification

  1. Call startPicking() before any bars are loaded and confirm it throws the unavailable-history error.
  2. Pick a bar in the middle of loaded history and confirm status is paused with currentBarIndex === startBarIndex.
  3. play(), then pause(): the cursor stops advancing and status is paused.
  4. stepForward() advances exactly one bar per call.
  5. setSpeed(20) while playing restarts the timer without skipping or duplicating bars.
  6. Step or play onto the final bar: status becomes ended and the ended event fires once.
  7. exit(): status returns to idle, live updates behave normally, and no further stepped events arrive.

Next steps

  • TradeScript Widget — mount, readiness, and cleanup ownership for the chart replay runs on.
  • Historical Bars — the data contract replay steps through.
  • Replay state — persist replay progress separately from chart layouts.