RESEARCH

How we verify two backtest engines produce the same results

Study details

Measured
2026-06-29 – 2026-07-07, five recorded sessions
Instruments
US equities, real recordings by date and ticker
Method
run the research implementation and the production engine over identical fixtures and compare every intermediate value, not just the final trades
Result
213 fixed cases across 17 modules, 3,373,795 compared values, 0 mismatches
Contents
We keep two implementations of the same trading logic. Research work happens in Python, where it is fast to try things. Live trading and backtesting run on a separate engine, because that is what has to be correct at nine in the morning. Two implementations of the same rules will drift. The question is how you find out before your money does.

1. Compare intermediates, not outcomes

The obvious check is to run both and compare the trade list. It is also the weakest check available, because a trade list is the end of a long chain. Two errors can cancel. A difference in an indicator can leave the trades identical on one day and change them on the next. So the comparison runs at every stage of the chain: preprocessing, candle patterns, microstructure, volatility, volume profile, stop and take levels, risk scoring, the strategy DSL, the order manager, and finally the backtest. Each stage is compared value by value against fixed inputs.
Stage groupCases
Preprocessing and raw candles34
Pattern and microstructure inference83
Model training paths30
Risk analyzer, per symbol40
DSL, order manager, backtest9
Every case is pinned to a real recorded session and ticker, so the inputs are market data we actually saw rather than synthetic sequences.

2. Result

213 cases, 3,373,795 compared values, zero mismatches. The largest single case compares 170,000 preprocessed rows. 5,054 values landed inside a declared boundary tolerance and are counted separately rather than being silently accepted. The rule is that ties are reported and the run fails if their share crosses a threshold, so a slow drift into tolerance cannot hide as a pass.

3. Floating point is not a rounding detail

The most useful thing this harness caught had nothing to do with logic. NumPy's mean and sum use pairwise summation; a straightforward sequential loop does not. Over a long window the two disagree in the last bits. That is invisible until a value sits on a decision boundary — and a risk score that crosses a bucket edge changes a trade. On one fixture, a sequential sum produced 9 differing risk scores out of 6,000 rows. Matching the summation strategy brought that to zero across 8,118 checks. The logic had been identical the whole time. Since then the comparison policy states its tolerances explicitly rather than leaving them to chance: exact equality for integers and categories, relative tolerance for ratios, a looser bound for compound products, and an absolute floor for values near zero where relative error is meaningless.

4. What it does not prove

A parity harness proves that two implementations agree. It says nothing about whether they are right. Both can share a wrong assumption and pass every case. That is why this sits alongside the other checks rather than replacing them: eligibility audits for whether the inputs could have existed, cost re-pricing for whether the result survives contact with a spread, and paper trading before anything is funded. Parity is the cheapest of the four and catches a category the others cannot — silent divergence between the thing you researched and the thing that trades. Conclusion If you maintain a research implementation and a production one, compare them at every stage on real recorded inputs, declare your tolerances instead of discovering them, and treat summation order as part of the specification. The bug we found this way was not in anyone's logic.

Limitations

  • Five recorded sessions on US equities. Coverage is broad across modules, not across market regimes.
  • Fixtures are fixed inputs. A code path that no fixture exercises is not covered. For candle patterns specifically, 55 of 61 fired on real data; the remaining 6 are verified by construction rather than by observation.
  • The comparison is between our own two implementations. Agreement is not correctness.
  • Backtest, DSL and order manager have 3 cases each — enough to catch structural divergence, not enough to be called exhaustive.
  • Tolerances are our choices. Different bounds would move the tie count, though the mismatch count is currently zero under the bounds we publish.

Related reading

← All research

Originally published by TraderWe on August 11, 2026. You may quote and link to this page. Republishing the full text without a link back to the original is not permitted.

3 replies

Q
QuietVol· Aug 2026 ago· edited Aug 2026 ago
Zero mismatches over 3.37M values reads well, but the number I'd want to see is the distribution of those 5,054 tolerance-band values, if they cluster in one or two modules that's a systematic rounding difference, not noise, and it will eventually walk past the boundary. Five sessions is also a narrow window; regime-wise you've sampled whatever those days happened to be, and the paths that only fire on gaps or halts may simply never have been exercised. Are the tolerance hits spread across the 17 modules or concentrated in the volatility/preprocessing stages?
GrandpaGrizzly· Aug 2026 ago
Seen more than one shop blow up because the model on the desk and the model in production quietly disagreed, and nobody found out till the fills looked wrong. Comparing trade lists only is like checking your parachute after you land. Stay humble.
F
FiveMinFiona· Aug 2026 ago
Good point that two errors can cancel out at the end - I hadn't thought about that. Curious how you build the fixtures though: are those recorded sessions chosen for being messy days, or just whatever the calendar gave you? Feels like parity on a quiet tape proves less than parity on a chaotic open.
Sign in to reply →