Caloogy Article
中文

The Seven Sins of Backtesting

Date
Feb 16, 2026
Series
Quant
Reading
6 min
Topics
QuantBacktestOverfittingLook ahead bias

The one-sentence version

Backtesting uses historical data to test strategies, and it is extremely easy to produce beautiful but fake results. The seven most common errors are: look-ahead bias, survivorship bias, overfitting, data snooping, ignoring transaction costs, ignoring capacity, and the fallacy of out-of-sample safety. This article uses code to demonstrate two of the most fatal ones: on purely random prices, if you try enough parameter combinations, you can "find" an in-sample strategy that performs well, but it is worthless out-of-sample; if you shift the signal timing by just one day (peeking at the closing price of the same day), a losing strategy can become a winning one.

Disclaimer

This article is for technical educational purposes only and does not constitute any investment advice. The code in this article uses simulated data.


Layer 1: The Intuition

1.1 Hindsight Bias

"If I had bought Bitcoin in 2010..." Anyone can find a perfect path in hindsight. The danger of backtesting lies in the fact that it is a systematic hindsight generator.

A backtesting result is often unreliable not because the code is written incorrectly, but because:

1.2 Overview of the Seven Sins

Sin What it is Typical Example
1. Look-ahead bias Using data that was not yet known at the time Calculating signals using the current day's closing price and executing at that same closing price; using the reporting period instead of the announcement date for financial reports
2. Survivorship bias Backtesting only on "surviving" assets Backtesting the last ten years using today's index constituents, causing delisted or removed stocks to disappear
3. Overfitting Tuning parameters to "just right" for history Testing every moving average period from 2 to 200 and picking the best one
4. Data snooping Repeatedly testing different ideas on the same dataset An entire team conducting research on the same historical period for years (see Multiple Testing and the Factor Zoo)
5. Ignoring costs Disregarding commissions, spreads, and impact A high-turnover strategy that turns from profitable to losing after deducting costs (see Modeling Transaction Costs)
6. Ignoring capacity Not considering the impact of fund size on price A small-cap strategy backtested with 100 million USD, which would be impossible to execute in reality due to liquidity constraints
7. Fallacy of out-of-sample Thinking that setting aside out-of-sample data makes you safe Modifying the strategy after seeing out-of-sample results, then looking at out-of-sample again... the out-of-sample becomes in-sample

Layer 2: Demo 1: "Good Strategies" on Random Data

2.1 Experimental Design

import numpy as np, pandas as pd
rng = np.random.default_rng(7)
n = 2000
ret = pd.Series(rng.normal(0, 0.01, n))          # Pure random walk: no predictability
price = 100 * np.exp(ret.cumsum())
ins, oos = slice(0, 1000), slice(1000, n)

def sharpe(x):
    return x.mean() / x.std() * np.sqrt(252) if x.std() > 0 else 0.0

def ma_strategy(price, fast, slow):
    sig = (price.rolling(fast).mean() > price.rolling(slow).mean()).astype(float)
    return sig.shift(1) * price.pct_change()        # Trade today using yesterday's signal to avoid look-ahead bias

results = []
for fast in range(2, 50, 2):
    for slow in range(fast + 5, 200, 5):
        r = ma_strategy(price, fast, slow)
        results.append((fast, slow, sharpe(r.iloc[ins].dropna()), sharpe(r.iloc[oos].dropna())))
res = pd.DataFrame(results, columns=["fast", "slow", "is_sharpe", "oos_sharpe"])
best = res.loc[res["is_sharpe"].idxmax()]
print("Number of parameter combinations tried:", len(res))
print("In-sample best:", best.round(2).to_dict())
print("Mean out-of-sample Sharpe across all combinations:", round(res["oos_sharpe"].mean(), 2))

Output:

Number of parameter combinations tried: 826
In-sample best: {'fast': 44.0, 'slow': 84.0, 'is_sharpe': 0.61, 'oos_sharpe': -0.1}
Mean out-of-sample Sharpe for all combinations: -0.13

2.2 Interpretation


Layer 3: Demo 2: Forward Error Only Needs One Line

3.1 The same strategy, two ways

sig = (price.rolling(5).mean() > price.rolling(20).mean()).astype(float)
cheat = sig * price.pct_change()                     # Use today's signal to earn today's return
honest = sig.shift(1) * price.pct_change()           # Use yesterday's signal to earn today's return
print("Look-ahead version Sharpe:", round(sharpe(cheat.dropna()), 2), " Correct version Sharpe:", round(sharpe(honest.dropna()), 2))

Output:

Look-ahead version Sharpe: 0.57  Correct version Sharpe: -0.07

3.2 Why the difference is so large

Today's closing price simultaneously affects "today's moving average" (the signal) and "today's return." If the price jumps today, the moving average is more likely to cross up, making the signal more likely to be 1, thus "coincidentally" holding the asset on the day it jumps. This correlation arises entirely from the same data point being used twice, which is impossible in reality: you do not know the closing price before the market closes.

On purely random data, look-ahead bias creates a Sharpe ratio of 0.57 out of thin air. On real data, the fake returns it generates may be even larger and harder to detect because they are often hidden within complex data processing workflows:

3.3 Defense Checklist

Sin Defense
Look-ahead bias Explicitly shift all signals; use point-in-time databases; specifically check "could this data have been obtained at that time" during code reviews
Survivorship bias Use databases that include delisted stocks and historical constituents
Overfitting Fewer parameters; parameters should be effective around a reasonable range (parameter plateau) rather than a single spike
Data snooping Record all ideas and the number of attempts; use the Deflated Sharpe Ratio for correction (see Multiple Testing and the Factor Zoo)
Ignoring costs Incorporate commission, spread, and market impact cost models into the backtest (see Modeling Transaction Costs)
Ignoring capacity Limit position sizes based on target fund size and target asset volume
Fallacy of out-of-sample Look at out-of-sample data only once; or use "vault" data that is only opened for final confirmation (see Agent Factor Mining)

3.4 A Healthy Research Habit

  1. Write down your hypothesis and rationale first, then look at the data (to avoid looking at results first and then inventing a story);
  2. Record every attempt, including the failures;
  3. Run your backtesting framework on simulated or shuffled data: if it can "make money" on random data, the framework definitely has look-ahead bias;
  4. Use paper trading or small-capital live trading as the final out-of-sample test.

Summary

Layer Key points
Seven Sins Look-ahead, survivorship, overfitting, data snooping, ignoring costs, ignoring capacity, fallacy of out-of-sample
Demonstration 1 Testing 826 parameter sets on random data results in an in-sample Sharpe of 0.61 and zero out-of-sample
Demonstration 2 A shift(1) difference: look-ahead bias turns a Sharpe of −0.07 into 0.57
Habits Write hypotheses first; record all attempts; use random data to test the backtesting framework; verify with small-capital live trading