Algorithmic Trading Backtest Lab
A research harness built to make strategies fail honestly
By Renjith ·
Prerequisites
- Strong Python
- Understanding of returns, drawdown and volatility
- Historical price data (the repository includes a sample set)
- Realistic expectations about what backtesting can tell you
The problem
Almost every published backtest is wrong in the same direction. Not fraudulently — structurally. The default way to write one quietly includes a set of advantages the strategy would never have had:
- Prices that were revised after the fact, treated as if they were available live.
- A universe of instruments that still exist today, silently excluding the ones that failed.
- Zero transaction costs, or a flat fee that ignores spread and market impact.
- A parameter chosen by testing forty variants on the same history.
Each of these makes results better. None of them are available to a real trader. A framework that makes them easy to do by accident is producing fiction with a Sharpe ratio attached.
What was built
A research harness whose defaults are pessimistic. Costs, slippage and point-in-time universes are on by default and must be explicitly disabled; walk-forward is the standard evaluation rather than an advanced option.
The reporting is deliberately unflattering: in-sample and out-of-sample results are always shown side by side, and every report includes the degradation between them. If a strategy only works in-sample, the report says so on the first line.
This is educational research infrastructure. It is not investment advice and produces no recommendation to trade anything.
How it works
Costs are not a detail#
The most common objection to including realistic costs is that they are "roughly a rounding error". For anything trading more than a few times a month, they are not.
costs.pypythondef transaction_cost(
notional: float,
volatility: float,
adv_participation: float,
commission_bps: float = 1.0,
) -> float:
"""Commission + half-spread + a square-root market impact term."""
commission = notional * commission_bps / 10_000
half_spread = notional * (0.5 * volatility * 0.1)
impact = notional * 0.1 * volatility * (adv_participation ** 0.5)
return commission + half_spread + impact
The square-root impact term is what stops the harness rewarding strategies that only work at sizes nobody could actually trade. Without it, the optimiser reliably finds a "strategy" that trades enormous size in illiquid instruments.
What walk-forward actually shows#
Running a moving-average crossover with a parameter grid, in-sample and out-of-sample:
| Window | In-sample Sharpe | Out-of-sample Sharpe | Retention |
|---|---|---|---|
| 2015–2017 → 2018 | 1.42 | 0.31 | 22% |
| 2016–2018 → 2019 | 1.38 | 0.44 | 32% |
| 2017–2019 → 2020 | 1.51 | −0.12 | — |
| 2018–2020 → 2021 | 1.29 | 0.38 | 29% |
Roughly a quarter to a third of in-sample performance survives, and one window is negative. That is the normal, honest result for a simple technical strategy, and the fact that it looks disappointing is the finding.
Educational research only
Nothing here is financial advice, a recommendation, or an offer to trade. Backtested results are hypothetical and benefit from hindsight. Past performance — simulated or real — does not indicate future results. Trading involves substantial risk of loss, including total loss of capital. Do your own research and consult a licensed professional before risking money.
Why the framework is built to disappoint#
A backtesting framework has one job: to be harder to fool yourself with than a spreadsheet. Every default in this one is chosen to make the strategy's life difficult, because the alternative — a framework whose defaults flatter — is how people end up trading a curve fit.
Build steps
- 1
Build point-in-time data handling
Store the universe as it existed on each date, including delisted instruments. This is unglamorous and it is the single largest source of inflated results.
- 2
Model costs before signals
Commission, spread and slippage as a function of size and volatility — implemented before any strategy, so no strategy is ever evaluated without them.
- 3
Implement a reference strategy
A moving-average crossover: simple, well understood, and a useful baseline precisely because expectations for it are realistic.
- 4
Add walk-forward analysis
Rolling in-sample optimisation with strictly out-of-sample evaluation. The gap between the two is the most informative number the harness produces.
- 5
Report degradation prominently
Out-of-sample performance as a fraction of in-sample, printed at the top of every report rather than buried in an appendix.
- 6
Add position sizing and risk limits
Volatility-scaled sizing with per-position and portfolio caps, so results are not driven by a single concentrated bet.
Lessons learned
Survivorship bias is the largest single distortion, and correcting it requires data work that most tutorials skip entirely.
In-sample results are close to meaningless on their own. The number worth reporting is the retention ratio.
Costs change which strategies work, not just how well. Several strategies that look viable at zero cost invert under realistic assumptions.
Build the framework to be pessimistic by default, because optimistic defaults are how honest people produce dishonest results.
Limitations
Daily bar data only. Intraday strategies need tick or minute data and a substantially more detailed execution model.
Market impact is modelled with a standard square-root approximation, which is a simplification of a genuinely complex phenomenon.
No modelling of borrow costs or availability for short positions.
Sample data covers liquid instruments over roughly a decade. Regime coverage is limited, and a decade is not many independent observations of a market cycle.
Future improvements
Monte Carlo resampling of trade sequences, to show the distribution of outcomes rather than the single realised path.
Regime-conditional reporting, since aggregate statistics hide that many strategies work in exactly one environment.
A multiple-testing correction that accounts for how many variants were tried, because the number of tests run is a fact about the researcher that belongs in the report.
Free resource
Backtesting Starter Notebook
A Jupyter notebook with point-in-time data handling, realistic costs and walk-forward validation already wired in.
Related course
Algorithmic Trading Research With Python
Build a research harness that makes strategies fail honestly: point-in-time data, realistic costs, walk-forward validation. Educational only.
Related builds and resources
Why Most Backtests Lie
Backtests fail in a consistent direction — optimistic. Here are the five structural reasons, with a detection method for each. Educational research only.
Does RSI Actually Work?
Testing the classic RSI(14) mean-reversion rule across 20 years and 500 instruments, with costs included and multiple-testing accounted for.
Backtesting Starter Notebook
A Jupyter notebook with point-in-time data handling, realistic costs and walk-forward validation already wired in.
Backtest Metrics Calculator
Paste a return series and get the risk-adjusted metrics that matter, with the assumptions stated. Educational only.
Algorithmic Trading Research With Python
Build a research harness that makes strategies fail honestly: point-in-time data, realistic costs, walk-forward validation. Educational only.
Why Most Backtests Lie
Five structural biases that make trading strategies look better than they are.