How to Backtest a Trading Strategy in Python (Even If You’re Not a Programmer)

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:

  1. Buy (Go Long) when the 50-day simple moving average (SMA) crosses above the 200-day SMA (a “Golden Cross”).
  2. 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 as pd) is our data wizard. It will hold and manipulate our stock price data in a table-like structure called a DataFrame.
  • yfinance (imported as yf) is our data fetcher. It will grab historical stock prices from Yahoo Finance.
  • matplotlib.pyplot (imported as plt) 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.download what 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 called SMA_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_200 will 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 a 1 in the ‘Signal’ column, otherwise put a 0.”
  • data['Signal'].diff() calculates the difference between the current signal and the previous one. This creates our Position column:
    • 1 in the ‘Position’ column means the signal just changed from 0 to 1 -> BUY.
    • -1 means the signal just changed from 1 to 0 -> SELL.
    • 0 means no change in position.

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.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top