AlgoPro UniversityCourse home
Part 5 · Lesson

Vectorised backtesting from scratch

The fastest way to test an idea: turn signals into positions into returns with pure pandas — no loop, no engine, a whole strategy in a dozen lines.

A vectorised backtest treats your entire price history as one big array and computes the strategy in a handful of column operations — no for loop over bars. It is the single most useful tool in this course: you can test an idea across ten years of data in milliseconds, which means you can throw away bad ideas fast and cheaply.

The whole technique rests on one chain of thought: prices → signal → position → strategy returns → equity curve. Master that chain and you can backtest almost any rule-based strategy without writing an engine.

Step 1 — get prices and market returns

We start with a daily close series. The market return is the percentage change from one close to the next — this is what a buy-and-hold investor earns, and the yardstick every strategy is measured against.

pythonPrices and the daily return series
import numpy as np
import pandas as pd
import yfinance as yf

class="c"># Daily closes for a liquid ETF; use any OHLCV source you like
df = yf.download(class="s">"SPY", start=class="s">"class="n">2015-class="n">01-class="n">01", end=class="s">"class="n">2024-class="n">12-class="n">31", auto_adjust=True)
px = df[class="s">"Close"].dropna()

class="c"># Simple (arithmetic) daily returns of the underlying market
market_ret = px.pct_change().fillna(class="n">0.0)

print(px.head())
print(market_ret.describe())

Step 2 — the signal

Our strategy is the classic moving-average cross: go long when a fast average is above a slow average, flat otherwise. The signal is a boolean condition evaluated across the *whole* series at once — that is the vectorised part.

pythonA fast/slow moving-average cross signal
fast = px.rolling(class="n">20).mean()
slow = px.rolling(class="n">100).mean()

class="c"># Raw signal: +class="n">1 (long) when fast > slow, else class="n">0 (flat). No loop.
signal = (fast > slow).astype(int)

class="c"># How often are we in the market?
print(fclass="s">"Time in market: {signal.mean():.class="n">1%}")

Step 3 — position, lagged correctly

We shift the signal forward by one bar to get the position we actually hold. Today we act on yesterday's completed signal. This single .shift(1) is the difference between an honest backtest and a fantasy.

pythonFrom signal to a tradeable position
class="c"># Hold today the position implied by yesterday's signal
position = signal.shift(class="n">1).fillna(class="n">0.0)

Step 4 — strategy returns and the equity curve

Now the payoff. Multiply the position by the market return to get the strategy's daily return, then compound it into an equity curve — the running value of one unit of capital.

pythonThe complete vectorised backtest
class="c"># Daily P&L of the strategy (costless for now — Lesson class="n">3 fixes that)
strategy_ret = position * market_ret

class="c"># Equity curves: compound the daily returns. Start at class="n">1.0 = class="n">100% of capital.
equity   = (class="n">1 + strategy_ret).cumprod()
buy_hold = (class="n">1 + market_ret).cumprod()

total_return = equity.iloc[-class="n">1] - class="n">1
print(fclass="s">"Strategy total return: {total_return:.class="n">1%}")
print(f"Buy & hold total return: {buy_hold.iloc[-class="n">1] - class="n">1:.class="n">1%}")

That is a full backtest in roughly fifteen lines. The equity curve below is the shape you will spend the rest of your trading life staring at — a strategy that sidesteps the worst drawdowns by going flat, at the cost of missing part of the raw upside.

Equity curve — MA-cross vs buy & hold
Growth of 1 unit of capital. The MA-cross strategy (blue) sits out the deep drawdowns by going flat, trailing buy & hold (violet) in strong trends but suffering shallower falls. Illustrative values.

Why vectorised is the right first tool

Next we make this backtest *mean* something: a fifteen-line script can print a return, but a return alone tells you almost nothing about whether the strategy is any good.