Replay and backtesting design
Design notes for bar replay, the broker emulator, paper trading in replay, strategy scripts and the tester panel.
Companion to language.md. Three features share one engine:
| Feature | Who drives orders | Who steps bars |
|---|---|---|
| Bar replay | nobody (or the user, see below) | the user: step, play, speed |
| Paper trading in replay | the user, via an order ticket and hotkeys | the user |
| Strategy backtest | an OxScript strategy() script | the engine, over loaded history |
The shared pieces are a replay datafeed, a broker emulator, a trade ledger with a metrics module, and a tester panel. Building them in that order means replay ships first and each later feature reuses everything before it.
References: Chartwo's widget demo (https://d11pdshe9w28u4.cloudfront.net/, the embedded demo on chartwo.com) for the replay-with-trading interaction model; TradingView Bar Replay and Strategy Tester for the controls and metrics users already know; OpenMarket's kScript strategies for the fill model and ambiguity disclosure.
What Chartwo does (observed 2026-09-21)
- Replay is a toolbar button. Clicking it drops a control bar under the pane:
[✂ select start] [▶ play] [▶| step] [1x speed]on the left and[Buy] [qty] [Sell] [✕ exit]on the right. The cut tool shows a dashed vertical cursor with the bar's timestamp; clicking a bar truncates the chart there. Future bars are gone; the legend and last-price line follow the replay bar. - Orders live on the chart. Buy or Sell places a market order at the replay bar's close. The
position is a horizontal line at the average price with a chip:
[↕ drag] [TP] [SL] [qty] [P&L] [×]. TP and SL are set by clicking the chip button and dragging a shaded band to the target price; the working orders render as lines too.×flattens. - Stepping advances one bar; the chip's P&L and the panel update every step. Speed cycles through multipliers when playing.
- Replay Trading panel replaces the account panel:
Position / Avg price / Open P&L / Realized P&L / Net P&L, then tabsOrders,Positions,Closed positionswith columns time, side, type, qty, price, status, realized P&L. - Trade (outside replay) opens a side panel with
OrderandDOMtabs: a depth ladder with bid and ask sizes per price, click-to-trade cells, andFlatten,CXL All,Reversebuttons. The same ticket works during replay. - Everything is one net position per symbol, quantity-based sizing, no commission or slippage settings, and no strategy scripting on the replay side.
1. Bar replay
Status (2026-09-22): v1 shipped on branch feat/bar-replay (single cell, whole-bar steps, persistence, shortcuts). Paper trading in replay shipped (2026-09-22) on feat/chart-trading: the replay bar carries Buy / qty / Sell and a one-click toggle, b / s / x trade the session from the keyboard, the active cell's stepped bars drive the paper broker, and the status line says "orders evaluate on the next step" while paused with working orders. Changing symbol or resolution ends the session. Open: sub-bar interval, multi-cell sync, depth truncation, disabling future-reading tools during replay.
Data model
Replay is a datafeed wrapper, not a chart mode. IDatafeed is getHistory plus subscribe, so
createReplayDatafeed(inner, controller):
- serves
getHistoryfrom the inner feed but truncated to bars withtime <= cursor; - ignores the inner feed's live subscription;
- emits each stepped bar through the same
onBarcallback the live feed uses, so the cell'sDataStore.update, the indicator manager, order-flow layers, legends and the last-price line all behave as if the bar had just closed.
Nothing downstream knows it is in replay. Indicators recompute over the truncated series, so there
is no lookahead. Drawings are unaffected and persist after exit. Footprint bars flow through
IFootprintDatafeed the same way. Symbol and resolution changes re-request truncated history at
the same cursor, so switching timeframe mid-replay keeps the point in time.
ReplayController state: { cursor: UTCTimestamp, playing, speed, interval }. cursor is a
timestamp, never an index. interval is the replay step and defaults to the chart resolution; when
a smaller divisor with data is chosen (for example 1m steps on a 5m chart), the forming 5m bar is
rebuilt from the smaller bars and emitted as an in-place update, which is exactly what a live tick
does. Sub-bar steps are the only way to see a bar form; without lower-timeframe data the step is a
whole bar.
Multi-cell shells share one controller ("all cells" mode). Cells on larger resolutions wait for the smaller ones to reach their close. v1 may ship single-cell if the shared cursor is not ready.
Controls
Toolbar "Replay" button opens a control bar directly under the pane, Chartwo-style, with trading on the right:
[✂ Select start] [⏮ Go to date] [◀ Step back] [▶ Play/Pause] [▶| Step forward] [Speed 1x ▾] [Interval same as chart ▾] … [Buy] [qty] [Sell] [TP/SL ▾] [Exit ✕]
- Select start: a vertical cut cursor over the pane; clicking a bar sets
cursorto that bar's time. Also "first available bar" and "random bar". - Step back is cheap for us because every step is a truncated replay from the store, so we offer it (TradingView does not).
- Hotkeys: Shift+Right step, Shift+Left step back, Shift+Down play/pause, Escape exit. These register through the existing shell shortcut system and show in the shortcuts sheet.
- Persist
{ symbol, resolution, cursor, speed, interval }per shell, resume on reopen. - Disabled during replay: alerts, live-order routing, tools that read future bars (fixed-range volume profile). Kagi and PnF cells refuse to enter replay.
2. Broker emulator
One deterministic, lookahead-free order engine used by both manual and scripted trading.
interface BrokerConfig {
initialCapital: number; // 10 000
currency: string;
qtyType: "fixed" | "cash" | "percentOfEquity";
qtyValue: number;
commission: { type: "percent" | "perUnit" | "perOrder"; value: number };
slippageTicks: number; // market and stop fills only
pyramiding: number; // 1
processOrdersOnClose: boolean; // false: fill at next bar open
fillModel: "pessimistic" | "pathHeuristic";
verifyLimitTicks: number; // 0
marginLong: number; marginShort: number; // 100 = no leverage
}Fill rules, in evaluation order per bar:
- Orders placed on bar N are evaluated on bar N+1. Market orders fill at N+1 open plus slippage.
With
processOrdersOnClosethey fill at N close. - Per bar: protective exits, then signal exits, then entries, each in insertion order. One-cancels- all groups cancel siblings on fill.
- Limit: fills when the bar's range reaches the level (buy: low ≤ limit, sell: high ≥ limit) at the
better of open and limit, no slippage.
verifyLimitTicksrequires penetration by N ticks. - Stop: fills when the range reaches the level (buy: high ≥ stop, sell: low ≤ stop) at the worse of open and stop, plus slippage.
- A gap through any level fills at the open.
- Both take-profit and stop-loss reached in one bar:
pessimisticfills the stop first and counts an ambiguous fill, shown in the tester so users know the number is model-settled;pathHeuristicwalks open→high→low→close when the open is nearer the high, else open→low→ high→close. When lower-timeframe bars are loaded (replay interval smaller than the chart), the emulator walks those instead and the fill is not ambiguous. - Netting position model: one net position per symbol; a reversal closes fully and opens the new
position at the same fill.
pyramidingcaps same-direction entries. - Commission on every fill notional. Bars with NaN prices reject orders and count it.
The emulator is a fold like everything else in the engine: step(state, bar) → { state, fills }.
It runs inside the same one-step snapshot the indicator engine keeps, so a live-bar update rolls
back and re-evaluates, and a replay step back is a truncated re-run.
3. Paper trading in replay
Trading is part of the replay bar, not a separate mode. It is also available outside replay as live paper trading when the host opts in.
- Quick orders:
BuyandSellon the replay bar place a market order for the quantity in the box at the next fill per the emulator rules. ATP/SLmenu attaches a bracket to the next entry in ticks, %, price or R. Shift-click on the chart places a limit at the crosshair price; Alt-click a stop. - Order ticket (side panel, opened from a toolbar Trade button):
Ordertab with side, type (market, limit, stop, stop-limit), qty, price, TP, SL, trailing stop, and sizing mode (units, cash, % of equity, risk % from stop distance);DOMtab with a depth ladder when the feed supplies depth (the order-flow feed already does), click-to-trade cells, andFlatten,Cancel all,Reverse. - Position chip on the chart, Chartwo-style: a line at the average price with
[↕ drag] [TP] [SL] [qty] [P&L] [×]. TP and SL are set by clicking the button and dragging a shaded band; they render as lines with their own chips ([price] [qty] [×]). Dragging the position line converts to a limit order to add or a stop to reduce, with a confirm. Working limit and stop orders are draggable price lines. This reuses the existing long-position and short-position drawing primitives for the shaded risk and reward areas. - Markers: entry and exit arrows on candles, colour by side, tooltip with fill details.
- Hotkeys: B / S market, Shift+B / Shift+S limit at crosshair, X flatten, R reverse, Ctrl+Z cancel last pending order.
- Panel below the chart while replaying:
Position / Avg price / Open P&L / Realized P&L / Net P&L / Equityheader, thenOrders,Positions,Closed positions,Statstabs. Stats is the tester panel's Performance tab computed over the session. - Ledger: per trade
#, side, qty, entry time and price, exit time and price, exit via (signal, stop, limit, trail, flatten), P&L in currency and %, R, cumulative P&L, maximum favourable and adverse excursion, bars held, note. - Session: trades save with the replay session; CSV export.
4. Scripted strategies (OxScript strategy())
Same language as indicators, plus the Pine strategy namespace with Pine's names:
//@version=1
strategy("EMA cross", overlay=true, initial_capital=10000, default_qty_type=strategy.percent_of_equity,
default_qty_value=25, commission_type=strategy.commission.percent, commission_value=0.05,
slippage=1, pyramiding=1, process_orders_on_close=false)
fast = ta.ema(close, input.int(9, "Fast"))
slow = ta.ema(close, input.int(21, "Slow"))
if ta.crossover(fast, slow)
strategy.entry("Long", strategy.long)
if ta.crossunder(fast, slow)
strategy.entry("Short", strategy.short)
strategy.exit("TP/SL", from_entry="Long", profit=200, loss=100) // ticks, like Pine
plot(fast); plot(slow)Order API, Pine signatures: strategy.entry(id, direction, qty, limit, stop, oca_name, comment),
strategy.exit(id, from_entry, qty, qty_percent, profit, limit, loss, stop, trail_points, trail_offset, oca_name, comment), strategy.order, strategy.close(id), strategy.close_all(),
strategy.cancel(id), strategy.cancel_all(). Getters: strategy.position_size,
strategy.position_avg_price, strategy.equity, strategy.openprofit, strategy.netprofit,
strategy.closedtrades, strategy.wintrades, strategy.losstrades, strategy.max_drawdown,
strategy.opentrades, and the strategy.closedtrades.* accessors used by community scripts.
Header settings map one-to-one onto BrokerConfig; every value is overridable in the tester's
Properties tab without editing the script, as TradingView does.
Execution: the strategy fold runs after the script's indicator part on each bar and hands orders
to the broker emulator, which fills them on the next bar. Backtests run over the loaded history in
the same Worker as indicators. There is no cloud dependency; if pegasus later offers deeper history
or a hosted runner, the contract is the same BrokerConfig plus the trade ledger.
5. Tester panel
Bottom dock in the shell, collapsible to a one-line strip: name, sparkline equity, NET %, WIN %, DD %, TRADES. Tabs:
- Overview: net profit ($ and %), win rate with W/L counts, profit factor, max drawdown ($ and %), Sharpe; equity curve with drawdown series and buy-and-hold line.
- Performance: All / Long / Short columns. Net profit, gross profit, gross loss, commission paid, buy-and-hold return, Sharpe, Sortino, max drawdown and run-up with durations, exposure, total trades, win rate, avg trade, avg win, avg loss, payoff ratio, expectancy, avg R, largest win and loss, best and worst streak, avg bars in trade, P&L distribution histogram, ambiguous fills count, bars covered.
- Trades: sortable table from the ledger; hover highlights the trade on the chart, click scrolls to the entry.
- Properties:
BrokerConfigeditor.
Paper-trading sessions use the same panel with the Properties tab replaced by the order ticket.
Metric definitions
Closed trades unless stated.
| Metric | Definition |
|---|---|
| Net profit | gross profit − gross loss − commission; also as % of initial capital. Open P&L shown separately. |
| Gross profit / loss | Σ P&L of winning trades / |Σ P&L of losing trades| |
| Profit factor | gross profit ÷ gross loss |
| Win rate | winners ÷ closed trades; break-even trades counted as losers and stated as such |
| Avg trade, avg win, avg loss | arithmetic means; payoff ratio = avg win ÷ avg loss |
| Expectancy | win rate × avg win − loss rate × avg loss (equals avg trade); also mean R |
| R multiple | trade P&L ÷ initial risk, initial risk = |entry − initial stop| × qty; undefined without a stop and excluded from mean R |
| Max drawdown / run-up | max over time of (peak − equity) / (equity − trough) on the bar-by-bar equity curve including open P&L; $ and % of peak, with duration |
| Sharpe / Sortino | on daily equity returns, annualised with √252, risk-free rate from config (default 2%); labelled "daily" so it cannot be confused with per-bar values |
| Buy and hold | (last close − first close) ÷ first close over the tested range |
| Exposure | bars with an open position ÷ bars tested |
6. Delivery order
- Replay datafeed + controls (single cell, whole-bar steps, hotkeys, persistence). No new rendering code: everything reuses the live path.
- Sub-bar replay interval when lower-timeframe data is available from the feed.
- Broker emulator + ledger + metrics as pure core modules with a fixture-based test suite (each fill rule has a bar sequence and an expected fill).
- Paper trading UI: Buy/Sell/qty on the replay bar, position chip with TP/SL drag, order ticket with DOM, hotkeys, replay-trading panel with Orders/Positions/Closed/Stats.
strategy()in the compiler: parse and type-check the namespace, wire orders to the emulator, Properties tab, markers, trade highlighting.- Multi-cell synchronised replay.