ox-charts

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:

FeatureWho drives ordersWho steps bars
Bar replaynobody (or the user, see below)the user: step, play, speed
Paper trading in replaythe user, via an order ticket and hotkeysthe user
Strategy backtestan OxScript strategy() scriptthe 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 tabs Orders, Positions, Closed positions with columns time, side, type, qty, price, status, realized P&L.
  • Trade (outside replay) opens a side panel with Order and DOM tabs: a depth ladder with bid and ask sizes per price, click-to-trade cells, and Flatten, CXL All, Reverse buttons. 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 getHistory from the inner feed but truncated to bars with time <= cursor;
  • ignores the inner feed's live subscription;
  • emits each stepped bar through the same onBar callback the live feed uses, so the cell's DataStore.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 cursor to 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:

  1. Orders placed on bar N are evaluated on bar N+1. Market orders fill at N+1 open plus slippage. With processOrdersOnClose they fill at N close.
  2. Per bar: protective exits, then signal exits, then entries, each in insertion order. One-cancels- all groups cancel siblings on fill.
  3. 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. verifyLimitTicks requires penetration by N ticks.
  4. Stop: fills when the range reaches the level (buy: high ≥ stop, sell: low ≤ stop) at the worse of open and stop, plus slippage.
  5. A gap through any level fills at the open.
  6. Both take-profit and stop-loss reached in one bar: pessimistic fills the stop first and counts an ambiguous fill, shown in the tester so users know the number is model-settled; pathHeuristic walks 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.
  7. Netting position model: one net position per symbol; a reversal closes fully and opens the new position at the same fill. pyramiding caps same-direction entries.
  8. 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: Buy and Sell on the replay bar place a market order for the quantity in the box at the next fill per the emulator rules. A TP/SL menu 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): Order tab 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); DOM tab with a depth ladder when the feed supplies depth (the order-flow feed already does), click-to-trade cells, and Flatten, 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 / Equity header, then Orders, Positions, Closed positions, Stats tabs. 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: BrokerConfig editor.

Paper-trading sessions use the same panel with the Properties tab replaced by the order ticket.

Metric definitions

Closed trades unless stated.

MetricDefinition
Net profitgross 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 factorgross profit ÷ gross loss
Win ratewinners ÷ closed trades; break-even trades counted as losers and stated as such
Avg trade, avg win, avg lossarithmetic means; payoff ratio = avg win ÷ avg loss
Expectancywin rate × avg win − loss rate × avg loss (equals avg trade); also mean R
R multipletrade P&L ÷ initial risk, initial risk = |entry − initial stop| × qty; undefined without a stop and excluded from mean R
Max drawdown / run-upmax over time of (peak − equity) / (equity − trough) on the bar-by-bar equity curve including open P&L; $ and % of peak, with duration
Sharpe / Sortinoon 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
Exposurebars with an open position ÷ bars tested

6. Delivery order

  1. Replay datafeed + controls (single cell, whole-bar steps, hotkeys, persistence). No new rendering code: everything reuses the live path.
  2. Sub-bar replay interval when lower-timeframe data is available from the feed.
  3. 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).
  4. 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.
  5. strategy() in the compiler: parse and type-check the namespace, wire orders to the emulator, Properties tab, markers, trade highlighting.
  6. Multi-cell synchronised replay.

On this page