Advanced

Backtesting strategies on history

Backtesting runs a strategy over past prices to see how it would have done. It's the heart of this work, and the most dishonest one, because it's easy to build a result that looks incredible and is completely useless in the real world.

What you will see

Train window: [100, 105, 110, 108] Test window: [115, 120, 118, 125]

About 2 min of reading

Try It

Backtesting runs a strategy over past prices to see how it would have done, but the hardest part is trusting the result. I run a simple buy-and-sell loop over history, split the data into train and test, and make sure every signal only ever uses information that actually existed at that moment.

1

I run a simple loop over historical prices, buying when the price rises and selling when it falls, then print the final account value, return, and trade count.

prices = [100, 105, 110, 108, 115, 120, 118, 125] cash = 1000.0 holding = 0.0 trades = 0 for i in range(1, len(prices)): prev = prices[i - 1] cur = prices[i] if cur > prev and holding == 0: holding = cash / cur cash = 0.0 trades += 1 elif cur < prev and holding > 0: cash = holding * cur holding = 0.0 trades += 1 final_value = cash + holding * prices[-1] print("Backtesting strategy over history") print("Initial capital: $1000.00") print("Final value: $%.2f" % final_value) print("Return: %.1f%%" % ((final_value / 1000.0 - 1.0) * 100.0)) print("Trades executed: %d" % trades)

Before you run it, say out loud what it will print. Then open the answer.

What it prints
Backtesting strategy over history Initial capital: $1000.00 Final value: $1055.40 Return: 5.5% Trades executed: 5
A backtest that looks incredible is very often a lie. Check whether it used data that could not have existed yet before you trust the return.
2

I make sure a signal only ever relies on prices known at the moment of the decision, so the backtest never accidentally peeks into the future.

prices = [100, 105, 110, 108, 115, 120, 118, 125] signal_price = prices[-1] honest_price = prices[-1] print("Using only data known at decision time: honest")

Before you run it, say out loud what it will print. Then open the answer.

What it prints
Using only data known at decision time: honest
Any signal that depends on tomorrow's close is worthless before it ever trades. A backtest that looks ahead cannot be fixed later.
3

I split the price history into a train window and a test window, then print both so the model is only ever judged on data it never trained on.

prices = [100, 105, 110, 108, 115, 120, 118, 125] train = prices[:4] test = prices[4:] print("Train window: %s" % train) print("Test window: %s" % test)

Before you run it, say out loud what it will print. Then open the answer.

What it prints
Train window: [100, 105, 110, 108] Test window: [115, 120, 118, 125]
If a strategy only works on the data it memorized, it will fall apart the moment it touches real prices. Unseen data is the only honest judge.

Put it in order

Before you write anything, put these lines back in the right order. Click a line to move it up one step. This support step is easier than writing code from scratch, and it is here to make the last example stick.

Data Flow

A strategy runs over past prices, trades are simulated, and the returns are checked for honesty before anyone trusts them.
1 Generate signals from price
↓
2 Simulate buys and sells
↓
3 Check for lookahead
↓
4 Test on fresh data
Line by line

Read the first example one line at a time. A line that prints shows what appears on screen; the rest set things up quietly.

prices = [100, 105, 110, 108, 115, 120, 118, 125] sets nothing on screen
cash = 1000.0 sets nothing on screen
holding = 0.0 sets nothing on screen
trades = 0 sets nothing on screen
for i in range(1, len(prices)): sets nothing on screen
prev = prices[i - 1] sets nothing on screen
cur = prices[i] sets nothing on screen
if cur > prev and holding == 0: sets nothing on screen
holding = cash / cur sets nothing on screen
cash = 0.0 sets nothing on screen
trades += 1 sets nothing on screen
elif cur < prev and holding > 0: sets nothing on screen
cash = holding * cur sets nothing on screen
holding = 0.0 sets nothing on screen
trades += 1 sets nothing on screen
final_value = cash + holding * prices[-1] sets nothing on screen
print("Backtesting strategy over history") Backtesting strategy over history
print("Initial capital: $1000.00") Initial capital: $1000.00
print("Final value: $%.2f" % final_value) Final value: $1055.40
print("Return: %.1f%%" % ((final_value / 1000.0 - 1.0) * 100.0)) Return: 5.5%
print("Trades executed: %d" % trades) Trades executed: 5
Why this works
Avoid looking ahead

The biggest lie a backtest can tell is peeking at the future. If your code uses information that didn't exist yet, the numbers are fake by construction. Only ever use data that was actually available at the moment you're testing.

Beware overfitting

A strategy tuned perfectly to fit past noise isn't an edge, it's a memorized answer key. It looks flawless on history and falls apart live. Test it on data the strategy never saw, and hope it still holds up.

Examples

The simplest trading loop

for price in history:
    if signal(price):
        buy(price)
    elif sell_signal(price):
        sell(price)

Loop that over five years and it'll tell you the return you'd have earned. The hard part comes after: deciding whether that number means anything, or whether you just got lucky while pretending to be a genius.

The trap of looking ahead

# Bad: using tomorrow's close to decide today
if tomorrow_close > today_close:
    buy(today)

This looks like a strategy that never loses. It can't, of course, because you're peeking at tomorrow. It's the single most common way a backtest lies, and it feels like winning, which is what makes it dangerous. If the signal uses anything that wouldn't exist yet, the result is worthless before it ever trades.

Splitting train from test

train = data[:'2020-01-01']
test = data['2020-01-01':]

Build the strategy on the first part, then test it on data it never got to see. If it crumbles on the test set, it only ever fit the past, that's overfitting, and the backtest was telling you a nice lie. Data you didn't train on is the only honest judge you've got.

Words this lesson uses

Each one is defined here on this site. Open the entry for the full definition and the other words it points at.

← The three numbers that actually mean something
Previous Lesson
The three numbers that actually mean something
Next Lesson
Managing risk and drawdown
Managing risk and drawdown →