09.Backtest — stryke-JIT engine + walk-forward sweeper
Strategies compile to stryke bytecode, run through the JIT, and produce a per-trade ledger that flows into the same FIFO roll-up the real broker imports use. Walk-forward sweeper splits in-sample / out-of-sample and aggregates results across windows. Deterministic, reproducible, presetable.
Why stryke-JIT
A backtest is a tight loop: for each bar, evaluate N conditions, maybe place an order, maybe exit, accumulate state. Interpreted-AST evaluation is the bottleneck for any non-trivial sweep. TraderView's strategies compile to stryke bytecode (the same VM the backtest engine shares with the custom-indicator AST) and execute through a Cranelift JIT — orders of magnitude faster than a tree-walking interpreter, and fast enough that walk-forward sweeps with thousands of parameter combinations finish in minutes instead of hours.
The strategy DSL is small and intentional — entry / exit conditions, sizing, stops, targets, time-based filters. Anything more exotic compiles to a custom indicator (step 10) and feeds back into the DSL.
Defining a strategy
Strategies are stored in backtest_presets. Each preset has:
- Universe — single symbol / watchlist / S&P 500 / Russell 3000 / custom universe query.
- Timeframe — 1m through 1d.
- Date range — IS / OOS split is configured at sweep time.
- Entry conditions — boolean expression over indicators, price, volume, time-of-day, day-of-week.
- Exit conditions — stop loss, profit target, time-based, trailing, signal-based.
- Sizing — fixed shares, fixed dollar, fixed-fractional, Kelly, or a custom indicator value.
- Costs — per-share commission, per-trade commission, fixed slippage (bps or cents).
// Pseudocode for an opening-range breakout preset
universe: small_caps_above_5
timeframe: 5m
entry: bar.high > or(0930, 0935, 0940).high
AND volume > sma(volume, 20) * 2
AND time < 1030
exit: stop = entry - 1 * atr(14)
target = entry + 2 * atr(14)
time_stop = 1300
sizing: kelly_capped(0.25, max_dollar = 5000)
costs: 0.005/sh + 1bps slippage
Running a backtest
POST /api/backtest/run with the preset ID and any date-range override. The engine:
- Compiles the strategy DSL + referenced custom indicators to stryke bytecode.
- JIT-compiles hot loops via Cranelift.
- Streams bars from
bars_*tables in chronological order (no look-ahead). - Evaluates entries / exits per bar, emits virtual executions.
- Rolls up through the same FIFO logic that real trades use, into a parallel
backtest_tradestable keyed by run ID. - Aggregates stats — same statistics the live Reports view uses (expectancy, R-distribution, drawdown, profit factor, SQN, Sharpe, Sortino, Calmar). Plus per-day equity series for the equity-curve chart.
Results land at GET /api/backtest/runs/{id}. The view renders equity curve, R-distribution, by-symbol breakdown, drawdown distribution, and a paginated trade list. Trade rows have the same right-click menu as live trades — drop into Charts with markers, open the journal slot, etc.
Walk-forward sweeper
POST /api/backtest/walk-forward. Splits the date range into N rolling windows; for each window:
- In-sample (IS) — sweep the parameter grid over the IS window. Pick the parameter combo with best objective (configurable: total return / Sharpe / Sortino / Calmar / expectancy).
- Out-of-sample (OOS) — re-run the chosen combo on the immediately-following OOS window.
- Roll the window forward and repeat.
The output is a per-window IS-vs-OOS table + a glued-together OOS equity curve. Big IS/OOS divergence is the curve-fitting smell test.
- Parameter grid — Cartesian product of grid axes (e.g. stop = [1.0, 1.5, 2.0] × target = [1.5, 2.0, 2.5, 3.0] × ATR window = [10, 14, 20]).
- Parallelism — runs fan out via Rayon across the host's cores. A 1 000-cell grid × 12 windows on a 16-core host finishes in minutes, not hours.
- Determinism — same preset + same date range + same seed reproduces identical equity curves.
Presets & the Presets view
GET/POST /api/backtest-presets. Save / share / clone strategies. The Backtest Presets tile lists them with last-run R-multiple distribution and a small equity-curve sparkline so you can spot which presets are still alive.
Each preset has a unique slug; export / import as JSON (round-trips losslessly so you can pin a preset in version control and re-import on another install).
What backtests don't include
- Real fills. A backtest evaluates "if the bar's high > trigger, fill at trigger + slippage". Live fills include queue position, latency, partial fills, and rejections. The fill-quality TCA report (step 05) compares your live fills to NBBO; the slippage assumption in the backtest is the simplest possible model.
- Liquidity. The default model assumes you can always fill 100 % of the requested size at the bar. The liquidity report (step 05) and the optional liquidity guard in the sizing block (configurable %-of-bar-volume cap) bring this closer to reality.
- Borrow cost. Short borrow is not modeled in the default cost block; configure it explicitly per-symbol if it matters for the strategy.
- Survivorship bias. Universes built from "currently in the Russell 3000" are survivorship-biased. Universe definitions support point-in-time historical constituent lists if you've imported them.