A Beginner’s Guide to Backtesting Moving Average Crossover Trading Strategies with Python

Moving average crossover is one of the most popular technical analysis trading strategies. It is widely used by both novice and experienced traders to identify trend direction and potential buy or sell signals. In this article, we will explore how to backtest different moving average crossover strategies using Python and optimize them for the best performance.
Step 1: Download Historical Data
The first step is to download historical data for the stock or ETF you want to trade. In this example, we will use SPY, which is an ETF that tracks the S&P 500 index. We will use the Yahoo Finance API to download the data.
import pandas as pd
import yfinance as yf
# Download SPY data
spy = yf.download("SPY", start="2010-01-01", end="2021-12-31")Step 2: Define the Backtesting Strategy
Next, we will define the moving average crossover strategy. The strategy is simple: buy when the short-term moving average crosses above the long-term moving average, and sell when the short-term moving average crosses below the long-term moving average. We will test different combinations of short-term and long-term moving averages to find the best-performing strategy.
def backtest_strategy(short_ma, long_ma):
spy["short_ma"] = spy["Adj Close"].rolling(short_ma).mean()
spy["long_ma"] = spy["Adj Close"].rolling(long_ma).mean()
# Buy signal: short-term moving average crosses above long-term moving average
spy["signal"] = (spy["short_ma"] > spy["long_ma"]).astype(int)
# Calculate daily returns and cumulative returns
spy["returns"] = spy["Adj Close"].pct_change() * spy["signal"].shift(1)
spy["cum_returns"] = (1 + spy["returns"]).cumprod()
# Calculate total return and annualized return
total_return = spy["cum_returns"].iloc[-1]
n_years = len(spy) / 252
annualized_return = (total_return ** (1 / n_years)) - 1
# Calculate Sharpe ratio
daily_returns = spy["returns"].dropna()
sharpe_ratio = np.sqrt(252) * (daily_returns.mean() / daily_returns.std())
return annualized_return, sharpe_ratioStep 3: Test Different Moving Average Combinations
We will now test different combinations of short-term and long-term moving averages. We will iterate through all possible combinations and store the results in a dictionary.
results = {}
for short_ma in range(1, 51):
for long_ma in range(5, 201):
if short_ma >= long_ma:
continue
annualized_return, sharpe_ratio = backtest_strategy(short_ma, long_ma)
results[(short_ma, long_ma)] = (annualized_return, sharpe_ratio)Step 4: Find the Optimal Strategy
Finally, we will find the optimal moving average crossover strategy based on the highest Sharpe ratio. The Sharpe ratio measures the risk-adjusted return of the strategy and is a commonly used metric to evaluate trading performance.
# Find the optimal moving average combination based on the highest Sharpe ratio
optimal_ma = max(results, key=lambda x: results[x][1])
optimal_return, optimal_sharpe = results[optimal_ma]
print("Optimal moving average crossover:", optimal_ma)
print("Annualized return:", optimal_return)
print("Sharpe ratio:", optimal_sharpe)Step 5: Interpret Results and
Overall, this code provides a basic framework for backtesting a moving average crossover trading strategy using Python. The strategy is defined by two moving averages, a short-term and a long-term one. When the short-term moving average crosses above the long-term one, it signals a buy, while a sell signal is generated when the short-term moving average crosses below the long-term one.
The code uses historical data from Yahoo Finance to simulate trades over a given time period and calculate performance metrics such as annualized return and Sharpe ratio. The backtesting strategy is tested over a range of different moving average combinations to find the optimal parameters that maximize the Sharpe ratio.
It’s worth noting that this is just a basic example, and there are many other factors to consider when designing and testing a trading strategy, such as transaction costs, slippage, and overfitting. Additionally, it’s important to remember that past performance does not guarantee future results, and a backtested strategy may not perform as well in real-world trading.
Overall, this code can serve as a starting point for those interested in learning more about backtesting trading strategies and using Python to analyze financial data.
