The temptation of a flawless trading strategy is strong. You conjure up a machine that cranks out cash around the clock, while you slumber. But how do you know if your great idea actually works? Hoping for best by intuition or a little bit of luck is the recipe for financial ruin. There’s only one way to find out whether a strategy works, and that’s by testing it on historical data — a procedure called backtesting.
When the average person hears the word “backtesting,” they think of convoluted code and finance algorithms. But what if you could take advantage of one of the world’s most popular programming languages, Python, without being a programming expert? This guide is the first in a series on our experience developing a trading system in python, and will introduce the reader to backtesting with Python. You’ll understand the basics and have the structure of a script to tweak and grow.
Why Backtest? And Why Use Python?
Backtesting is the financial equivalent of a flight simulator. It allows you to crash your strategy in a virtual environment, learning from the mistakes without losing a single dollar of real capital. It transforms trading from a speculative art into a systematic, evidence-based discipline.
Python is the perfect tool for this job because:
- It’s Readable: Python code often reads almost like plain English.
- It Has Powerful Libraries: A vast ecosystem of free libraries does the heavy lifting for financial analysis.
- It’s the Industry Standard: From hedge funds to individual traders, Python is the go-to language for quantitative finance.
Your Step-by-Step Guide to a Simple Backtest
We are going to backtest a simple moving average crossover strategy. The rules are:
- Buy (Go Long) when the 50-day simple moving average (SMA) crosses above the 200-day SMA (a “Golden Cross”).
- Sell (Close Position) when the 50-day SMA crosses below the 200-day SMA (a “Death Cross”).
We will use the pandas library for data manipulation and yfinance to easily download stock data.
Step 1: Setting Up Your Environment (The “Kitchen”)
Before you cook, you need a kitchen. Similarly, before you code, you need to import the necessary tools.
python
# Import the necessary libraries
import pandas as pd
import yfinance as yf
import matplotlib.pyplot as plt
# This just makes the graphs look nicer in the output
plt.style.use('seaborn-v0_8')
Explanation:
pandas(imported aspd) is our data wizard. It will hold and manipulate our stock price data in a table-like structure called a DataFrame.yfinance(imported asyf) is our data fetcher. It will grab historical stock prices from Yahoo Finance.matplotlib.pyplot(imported asplt) is our artist. It will help us visualize the results with graphs.
Step 2: Downloading Historical Data (The “Ingredients”)
You can’t test a recipe without ingredients. Let’s download Apple (AAPL) stock data for the last 10 years.
python
# Define the stock ticker and the time period ticker = "AAPL" start_date = "2013-01-01" end_date = "2023-12-31" # Download the data data = yf.download(ticker, start=start_date, end=end_date) # Print the first 5 rows to check print(data.head())
Explanation:
- We tell
yf.downloadwhat stock we want (AAPL) and over what time period. - It returns a DataFrame and stores it in the variable
data. - The
data.head()command shows us the first five rows, confirming we have data including the Open, High, Low, Close, and Adjusted Close prices.
Step 3: Calculating the Strategy Indicators (The “Recipe”)
Now, we apply the logic of our strategy by calculating the moving averages.
python
# Calculate the 50-day and 200-day Simple Moving Averages data['SMA_50'] = data['Close'].rolling(window=50).mean() data['SMA_200'] = data['Close'].rolling(window=200).mean() # Print to see the new columns print(data[['Close', 'SMA_50', 'SMA_200']].tail())
Explanation:
data['Close'].rolling(window=50).mean()creates a new column calledSMA_50. For each day, it looks back at the previous 50 days of closing prices, calculates their average, and puts the result in the new column.- We do the same for the 200-day average.
- The first 199 rows for the
SMA_200will be empty (NaN) because there isn’t enough data to calculate a 200-day average yet. This is normal.
Step 4: Generating the Trading Signals (The “Cook’s Commands”)
This is the core logic. We need to translate our strategy rules into code that creates explicit “Buy” and “Sell” signals.
python
# Generate the trading signals data['Signal'] = 0 # Create a new column and set all values to 0 (no position) # A '1' will represent a Buy signal, a '-1' will represent a Sell signal. data['Signal'] = np.where(data['SMA_50'] > data['SMA_200'], 1, 0) data['Position'] = data['Signal'].diff() # Print the data around a specific date to see the signal print(data.loc['2020-03-01':'2020-04-15', ['Close', 'SMA_50', 'SMA_200', 'Signal', 'Position']])
Explanation:
np.where(condition, value_if_true, value_if_false)is a powerful function. Here, it says: “Where the 50-SMA is above the 200-SMA, put a1in the ‘Signal’ column, otherwise put a0.”data['Signal'].diff()calculates the difference between the current signal and the previous one. This creates ourPositioncolumn:- A
1in the ‘Position’ column means the signal just changed from 0 to 1 -> BUY. - A
-1means the signal just changed from 1 to 0 -> SELL. - A
0means no change in position.
- A
Step 5: Calculating Strategy Returns (The “Taste Test”)
Now, let’s see how profitable our strategy would have been.
python
# Calculate the daily returns of the stock
data['Market_Return'] = data['Close'].pct_change()
# Calculate the strategy's returns
# The strategy return is the market return only on days we are holding the stock (Signal == 1)
data['Strategy_Return'] = data['Signal'].shift(1) * data['Market_Return']
# Calculate the cumulative returns for both
data['Cumulative_Market_Return'] = (1 + data['Market_Return']).cumprod()
data['Cumulative_Strategy_Return'] = (1 + data['Strategy_Return']).cumprod()
# Print the final cumulative return
print(f"Buy & Hold Return: {data['Cumulative_Market_Return'].iloc[-1]:.2f}")
print(f"Strategy Return: {data['Cumulative_Strategy_Return'].iloc[-1]:.2f}")
Explanation:
data['Signal'].shift(1)uses the previous day’s signal to determine today’s action. This prevents “look-ahead bias,” the fatal error of trading on data you couldn’t have known at the time.- We calculate cumulative returns to see the total growth of a $1 investment over the entire period.
Step 6: Visualizing the Results (The “Plating”)
A picture is worth a thousand words. Let’s create a chart to see our strategy in action.
python
# Create a plot with two subplots
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 10), sharex=True)
# Plot 1: Price and Moving Averages
ax1.plot(data['Close'], label='AAPL Price', alpha=0.7)
ax1.plot(data['SMA_50'], label='50-day SMA', alpha=0.7)
ax1.plot(data['SMA_200'], label='200-day SMA', alpha=0.7)
# Plot buy and sell signals
ax1.plot(data[data['Position'] == 1].index, data['SMA_50'][data['Position'] == 1], '^', markersize=10, color='g', label='Buy Signal')
ax1.plot(data[data['Position'] == -1].index, data['SMA_50'][data['Position'] == -1], 'v', markersize=10, color='r', label='Sell Signal')
ax1.set_ylabel('Price ($)')
ax1.set_title('AAPL Price and Moving Average Crossover Strategy')
ax1.legend()
ax1.grid(True)
# Plot 2: Cumulative Returns
ax2.plot(data['Cumulative_Market_Return'], label='Buy & Hold', alpha=0.7)
ax2.plot(data['Cumulative_Strategy_Return'], label='SMA Crossover Strategy', alpha=0.7)
ax2.set_ylabel('Cumulative Returns')
ax2.set_xlabel('Date')
ax2.legend()
ax2.grid(True)
plt.show()
Next Steps and Important Caveats
Congratulations! You have now executed your first backtest! Yet this is only the beginning, not the end. A professional backtest should take into consideration:
Transaction Costs: Commissions and slippage (the difference between the anticipated price and the actual execution price) can doom a theoretical strategy.
Robustness Testing: Does the strategy work on other stocks (MSFT, TSLA) as well as different market conditions (bull markets, bear markets)?
Risk Metrics: Determine the strategy’s maximum drawdown and Sharpe Ratio.
The script is simple and this will be your base. Now you can play around by modifying the moving average periods, adding new indicators such as RSI or even add stop-losses. When you learn how to backtest, you own your trading future by replacing hope with evidence and guessing with proof.
