Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

replay-lab

A bar-by-bar futures backtest engine that reproduces TradingView trade-for-trade, plus a visual replay lab for inspecting what it did.

The engine exists to answer one question honestly: is this an edge, or is it noise? Most backtest code answers that question wrong in ways that are invisible until real money is on the line. The design here is organized around making those failure modes structurally impossible, or at least loudly detectable.

Rust + egui. ~9,500 lines of engine, 77 tests, no async runtime.

The replay lab mid-replay

The replay lab paused mid-replay on synthetic bars: price with a stacked-EMA trend ribbon and ATR level rails, a volume pane with relative-volume state, and a phase oscillator below. The badges across the top (PHASE DISTRIB, TIDE MIXED, RANGE —, PRESS BUY 0.7x) are semantic states emitted by the indicator engine, not drawing output — the same values a strategy sees on that bar. Transport controls scrub the replay bar-by-bar.

BarSource ──▶ BarSeries ──▶ replay engine ──▶ Broker (TV fill semantics) ──▶ Trade[]
                                 │                                              │
                                 ▼                                              ▼
                          Indicator engine                            fixture comparator
                        (semantic states, not                       (trade-for-trade vs
                          drawing commands)                          TradingView export)
                                 │
                                 ▼
                          egui replay lab

The interesting part: three trust invariants

1. Lookahead is unrepresentable, not "avoided"

The engine owns the Vec<Bar>. On step i a strategy receives Ctx { bars: &bars[..=i], .. } — a slice that ends at the current bar. There is no API that yields a future bar. Indicators and higher-timeframe aggregates are incremental state machines fed one bar at a time; they cannot be precomputed over the full series, because nothing downstream of the engine ever holds the full series.

This is a type-level guarantee rather than a code-review convention, which matters because lookahead bias is the single most common way a backtest lies.

2. Explicit time, DST-aware, one definition of "trading day"

Bar.time is always UTC unix seconds at bar open. Vendor CSVs and TradingView exports are US-Eastern wall clock and convert at the boundary. Session gates evaluate in ET. The futures daily session (18:00 ET → next 17:00 ET, labeled by the ending civil date) has exactly one implementation.

Timezone bugs in backtests are silent — they shift entries by an hour twice a year and quietly change your results. So this layer has its own tests for DST transitions, session-date rollover, and wall-clock round-tripping.

3. TradingView broker-emulator semantics, reproduced exactly

Orders emitted while processing bar i activate on bar i+1. Market orders fill at next bar open ± slippage. Stops check open before low. Limits take no slippage. For a stop and limit on the same bar, TV's intrabar path heuristic is reproduced (high − open < open − low ⇒ assume open→high→low→close) and the first level touched fills while the other cancels.

None of this is documented by TradingView. It was established empirically by fixture archaeology and is written down in docs/BACKTEST.md, which is the most substantive document in this repo.


Verification: what "ported" actually means

A strategy is not "ported" when it runs. It is ported when, replayed bar-by-bar over the fixture period, it reproduces a TradingView List-of-Trades export trade-for-trade — entry and exit bar times exact, prices within a tick, signal labels matching.

replay-lab --backtest orb-tue --data data/cache/MNQ_300.csv --fixture export.csv

The comparator reports matched / missing / extra / price-off with per-trade diffs. Across the private research run this harness was built for: 11 fixtures, 766/798 trades exact (96%), 4 formal 100% passes — with every residual trade individually characterized (vendor bar differences, roll weeks, half-tick prints) rather than dismissed as noise.

Discoveries that only surfaced because verification was trade-for-trade:

  • A contract roll rule TradingView doesn't publish. MNQ1! does not switch at expiry. Price-bracketing across six quarterly rolls pinned the switch to the Monday 18:00 ET session reopen of expiry week. This matters well beyond roll week: a wrong roll date injects a false true-range spike that feeds daily ATR(14) for ~14 sessions afterward.
  • An undocumented pivot tie-break. ta.pivothigh/ta.pivotlow let a pivot tie earlier values but require it to strictly beat later ones. Established by sweeping all four tie rules against a 201-trade fixture (196 matches vs 187 / 195 / 195).
  • Ratio-adjusted vendor data is ~8% off at 2024-02 (file 19124.72 vs TV-implied 17636.75) — fine for walk-forward, disqualifying for price verification. The two are kept strictly separate.

What it found

Full board, with the book redacted: docs/VERDICTS.md.

Strategies were run against a bar written down before any results were seen: PF ≥ 1.15 at live costs, net positive excluding the top-3 winners, 2022 positive or a coherent regime story, and a flat walk-forward plateau.

The headline finding is a negative one, and it's why pdl-sweep ships here as a bundled example. Its TradingView backtest looked excellent. The port verifies 26/27 against the fixture, so the disagreement is real, not a porting bug. The .pine requests 30-min EMAs with lookahead_on and no [1] offset — the trend gate reads the forming bucket's final close up to 25 minutes early.

Measured: +$1,148 with the lookahead vs +$621 without. 85% of the "edge" was manufactured by reading the future.

And the honest remainder still fails the bar: ex-top-3 P&L is −$3.50 over seven years. The entire remaining edge is three trades.

Both variants ship — pdl-sweep (live-causal) and pdl-sweep-tv (lookahead re-enabled to reproduce TV). The gap between them is the deliverable.

A second result, from --intraday-dll: a widely-circulated prop-firm analysis models a daily loss limit as an end-of-day floor, max(day × M, −cap), which assumes full upside capture. Real firms enforce the limit intraday — the moment combined equity touches −cap you are flat, so a day that would have closed green but dipped intraday is forfeited. Reconstructing the true intraday equity path from per-bar mark-to-market shows upside capture collapsing 71% → 43% as the size multiplier goes 1 → 5. The naive model inverts the conclusion.


Analyses

# Regime table by year + rolling walk-forward (IS-optimized vs default OOS)
replay-lab --walkforward orb-tue --data <1min.csv> --daily <1day.csv> --mode wf

# Day-aligned portfolio: correlation, worst-day attribution, prop-firm overlay
replay-lab --portfolio --data <1min.csv> --daily <1day.csv> --book orb-tue,pdl-sweep

# Intraday daily-loss-limit sweep: cap policy × size multiplier
replay-lab --intraday-dll --data <1min.csv> --daily <1day.csv> --cap 500

Walk-forward exists to catch curve-fitting, and it earns its keep: across nearly every strategy tested, in-sample optimization performed ≤ default parameters out-of-sample. The engine's own recommendation is to not re-optimize.


The visual lab

The other half: an egui replay UI for watching bars, indicator state, and trades advance one at a time.

cargo run --release                                          # synthetic bars
cargo run --release -- --csv data/es_1min.csv --symbol ES --tf 10m
cargo run --release -- --view chart --shot out.png           # headless render

Controls: play/pause, jump, speed (bars/sec), window (visible bars), seek.

The design rule is that indicators emit states, not drawings. compute_phase returns a PhaseSample per bar (coil young/building/mature, release up/down/neutral); the app layer decides what a MatureCoil looks like. That seam is what lets the renderer swap to wgpu later, and lets the same semantic state feed alerts or journaling instead of only pixels.

It also renders trade-review frames from a replay-gym run — freezing the chart at each fill, overlaying side/price/outcome, and writing an index plus PNGs per closed trade:

replay-lab --gym-run <run-dir> [--gym-tf 1m] [--gym-window 120] [--gym-force]

Live data

Bars come from Interactive Brokers TWS on localhost:7497 via ibapi (sync). Requires TWS with Enable ActiveX and Socket Clients on, plus a market data subscription. Synthetic bars are the offline fallback.

cargo run --example tws_smoke -- ES Futures
replay-lab --backfill MNQ --tf 10m --cache data/cache

Status

Working: replay engine, TV broker semantics, fixture verification harness, higher-timeframe security() emulation, walk-forward, portfolio and intraday risk analysis, TWS backfill, Phase Oscillator, replay UI, headless rendering.

In progress: ATR Cascade / Pivot Ribbon / Volume Stack render engines, the multi-timeframe wave-propagation view, wgpu renderer, backgrounding the TWS fetch (it currently blocks the UI for ~1s).

Build

cargo build --release
cargo test            # 77 tests

No data files are included — vendor futures data isn't redistributable. The TWS backfill path (--backfill) regenerates a usable cache from a TWS connection. Two tests are gated behind data that isn't in the repo and skip cleanly without it.

A note on scope

This is extracted from a larger private research repo. The engine, the verification harness, the analyses, and the documented findings are here in full. Of ~20 ported strategies, two are includedorb-tue, which verifies 56/56 exact, and pdl-sweep, the lookahead case study. The rest, and the identities in the verdicts board, are redacted. The engineering is the point; the book isn't mine to publish.

License

MIT — see LICENSE. Indicator concept attribution is in NOTICE.

About

Futures backtest engine that reproduces TradingView trade-for-trade, with structural no-lookahead guarantees — plus an egui replay lab

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages