Moving Average Crossover Strategy in Python
The moving average crossover is one of the most popular trading strategies in existence. When a short-term moving average crosses above a long-term one, it signals a potential buy. When it crosses below, it signals a sell. In this tutorial, we implement both Simple Moving Average (SMA) and Exponential Moving Average (EMA) crossovers in Python, then backtest them against real market data.
Prerequisites
- ✓Basic Python and pandas
- ✓pip installed
- ✓Understanding of what a moving average is
Step 1.Install dependencies
We use yfinance for market data, pandas for calculations, and matplotlib for visualisation.
pip install yfinance pandas matplotlibStep 2.Download historical data
We grab 4 years of Apple daily closing prices to have enough data for meaningful backtesting.
import yfinance as yf
import pandas as pd
import matplotlib.pyplot as plt
df = yf.download("AAPL", start="2020-01-01", end="2024-01-01")
df = df[["Close"]].copy()
df.columns = ["close"]
print(f"Downloaded {len(df)} rows")Step 3.Calculate moving averages
SMA is a simple arithmetic mean. EMA gives more weight to recent prices, reacting faster to changes.
SHORT_WINDOW = 20
LONG_WINDOW = 50
df["sma_short"] = df["close"].rolling(window=SHORT_WINDOW).mean()
df["sma_long"] = df["close"].rolling(window=LONG_WINDOW).mean()
df["ema_short"] = df["close"].ewm(span=SHORT_WINDOW, adjust=False).mean()
df["ema_long"] = df["close"].ewm(span=LONG_WINDOW, adjust=False).mean()
print(df[["close", "sma_short", "sma_long"]].tail(10))Step 4.Generate crossover signals
A buy signal fires when the short MA crosses above the long MA. A sell signal fires on the reverse crossover.
# SMA crossover signals
df["signal"] = 0
df.loc[df["sma_short"] > df["sma_long"], "signal"] = 1 # Buy
df.loc[df["sma_short"] <= df["sma_long"], "signal"] = -1 # Sell
df["position"] = df["signal"].diff()
buys = df[df["position"] == 2]
sells = df[df["position"] == -2]
print(f"Buy signals: {len(buys)}, Sell signals: {len(sells)}")Step 5.Backtest returns
Compare the strategy returns against simple buy-and-hold to see if the crossover adds value.
df["returns"] = df["close"].pct_change()
df["strategy_returns"] = df["returns"] * df["signal"].shift(1)
cumulative_market = (1 + df["returns"]).cumprod()
cumulative_strategy = (1 + df["strategy_returns"]).cumprod()
print(f"Buy & Hold return: {(cumulative_market.iloc[-1] - 1) * 100:.1f}%")
print(f"SMA Crossover return: {(cumulative_strategy.iloc[-1] - 1) * 100:.1f}%")Step 6.Visualise the results
The top chart shows price with buy/sell signals. The bottom chart compares cumulative returns.
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 10))
ax1.plot(df.index, df["close"], label="Price", alpha=0.7)
ax1.plot(df.index, df["sma_short"], label=f"SMA {SHORT_WINDOW}", alpha=0.8)
ax1.plot(df.index, df["sma_long"], label=f"SMA {LONG_WINDOW}", alpha=0.8)
ax1.scatter(buys.index, buys["close"], marker="^", color="green", s=100, label="Buy")
ax1.scatter(sells.index, sells["close"], marker="v", color="red", s=100, label="Sell")
ax1.set_title("SMA Crossover Strategy")
ax1.legend()
ax2.plot(df.index, cumulative_market, label="Buy & Hold")
ax2.plot(df.index, cumulative_strategy, label="SMA Crossover")
ax2.set_title("Cumulative Returns")
ax2.legend()
plt.tight_layout()
plt.savefig("crossover.png", dpi=150)
plt.show()Expected Output
Downloaded 1007 rows
Buy signals: 12, Sell signals: 12
Buy & Hold return: 78.4%
SMA Crossover return: 45.2%Next Steps
- →Try different window combinations (10/30, 50/200)
- →Add transaction costs to the backtest
- →Compare SMA vs EMA crossover performance
Recommended Reading
Technical Analysis of Financial Markets →As an Amazon Associate we may earn from qualifying purchases.