Trading SystemsADVANCEDDifficulty: ADVANCED~24h build

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.pypython
def 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. 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. 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. 3

    Implement a reference strategy

    A moving-average crossover: simple, well understood, and a useful baseline precisely because expectations for it are realistic.

  4. 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. 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. 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

AlgoStrategy

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.

IntermediateDifficulty: Intermediate2 min readPython · Polars
Market research

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.

IntermediateDifficulty: IntermediatePython · Polars · DuckDB
NotebookDataset
NotebooksFree

Backtesting Starter Notebook

A Jupyter notebook with point-in-time data handling, realistic costs and walk-forward validation already wired in.

180 downloads
Email required
Trading researchFree

Backtest Metrics Calculator

Paste a return series and get the risk-adjusted metrics that matter, with the assumptions stated. Educational only.

930 runs
UDEMY$99

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.

AdvancedDifficulty: Advanced★ 4.612h720 studentsPython · Polars · DuckDB
Video

Why Most Backtests Lie

Five structural biases that make trading strategies look better than they are.

Watch