Simple Moving Average (MA) Crossover Strategy with Backtrader and PyFolio in Python
- Published on
Introduction
The Moving Average (MA) Crossover strategy is a popular technical analysis tool used by traders to identify potential shifts in market trends and generate buy or sell signals
When the long-term moving average (e.g., 20-day) crosses above the short-term moving average (e.g., 10-day), it generates a buy signal, indicating a potential upward trend. Conversely, when the short-term moving average crosses below the long-term moving average, it generates a sell signal, indicating a potential downward trend.
In this post, I'd like to show you how to implement a simple MA crossover strategy using Backtrader, and we will analyze the performance of the strategy using PyFolio.
Get Data Ready
from data_provider.yfinance import download_data_from_yahoo, read_data_from_yahoo
import talib
symbol = "AAPL"
# If you want to download the data again, uncomment the line below
# download_data_from_yahoo(symbol)
data = read_data_from_yahoo(symbol)
data["ma_10"] = talib.EMA(data["close"], timeperiod=10)
data["ma_20"] = talib.EMA(data["close"], timeperiod=20)
data = data["2020-01-01":]
data.head()
Implementation
Next, I will show you how to backtest using the Moving Average (MA) Crossover strategy.
Notes:
- We pass a pandas
DataFrame
to thebt.Strategy
and access the technical indicator of the timestamp in thenext
method shortly. - If
row["ma_10"] >= row["ma_20"]
, we callself.buy
to buy; otherwise, we sell.
import warnings
warnings.simplefilter(action='ignore', category=FutureWarning)
warnings.simplefilter(action='ignore', category=UserWarning)
import backtrader as bt
from data_provider.yfinance import download_data_from_yahoo, read_data_from_yahoo
import talib
from backtrader.feeds import PandasData
import pandas as pd
import pyfolio as pf
class SimpleMACrossStrategy(bt.Strategy):
def __init__(self, data):
self.raw_data = data
def next(self):
data = self.data
bar_timestamp = pd.to_datetime(data.datetime.datetime())
row = self.raw_data.loc[bar_timestamp]
cur_position = self.getposition(data).size
if cur_position> 0:
if row["ma_10"] < row["ma_20"]:
self.sell(size=cur_position)
else:
if row["ma_10"] >= row["ma_20"]:
cash = self.broker.getcash()
price = data.close[0]
size = int(cash / price) * 0.98
self.buy(size=size)
def notify_order(self, order):
if order.status in [order.Completed]:
if order.isbuy():
self.log(
f"B: {order.executed.price} {order.executed.size}",
)
elif order.issell():
self.log(
f"S: {order.executed.price} {order.executed.size}",
)
elif order.status in [order.Canceled, order.Margin, order.Rejected]:
self.log("Order Canceled/Margin/Rejected")
def log(self, txt):
bar_timestamp = pd.to_datetime(self.data.datetime.datetime())
print("%s, %s" % (bar_timestamp.isoformat(), txt))
if __name__ == "__main__":
symbol = "AAPL"
# download_data_from_yahoo(symbol)
data = read_data_from_yahoo(symbol)
data["ma_10"] = talib.EMA(data["close"], timeperiod=10)
data["ma_20"] = talib.EMA(data["close"], timeperiod=20)
data = data["2020-01-01":]
cerebro = bt.Cerebro()
cerebro.adddata(PandasData(dataname=data))
cerebro.addstrategy(SimpleMACrossStrategy, data)
cerebro.broker.setcash(10000.0)
cerebro.broker.setcommission(commission=0.005)
cerebro.addanalyzer(bt.analyzers.PyFolio, _name="pyfolio")
results = cerebro.run()
cerebro.plot(iplot=False)
strat = results[0]
pyfoliozer = strat.analyzers.getbyname("pyfolio")
returns, positions, transactions, gross_lev = pyfoliozer.get_pf_items()
# generate performance report
pf.create_full_tear_sheet(returns)
Pros, Cons
Pros of MA Crossover Strategy:
- Simple & Clear Signals: Easy to understand and provides clear visual cues for potential buy/sell points and trend direction.
- Effective Trend Identification: Helps identify and follow the prevailing market trend by smoothing out price volatility.
- Versatile & Customizable: Can be adapted to various markets, timeframes, and trading styles by adjusting MA types and periods.
Cons of MA Crossover Strategy:
- Lagging Nature: Signals appear after the price has already started moving, potentially leading to late entries or exits.
- Whipsaws in Ranging Markets: Generates many false signals (whipsaws) and performs poorly when prices are not trending clearly.
- Parameter Dependent: Performance heavily relies on the chosen MA periods, which may need optimization and can vary across assets/time.
- Often Needs Confirmation: Relying solely on crossovers can be risky; many traders use other indicators or analysis for confirmation.
Moving Average Functions
In this post, we use EMA
indicator, but you should know there are many other options in ta-lib:
SMA
(Simple Moving Average): Calculates the average price over a specified period, giving equal weight to all data points.EMA
(Exponential Moving Average): Gives more weight to recent prices, making it more responsive to new price changes than an SMA.WMA
(Weighted Moving Average): Assigns more weight to recent data points in a linear fashion; the most recent price gets the highest weight, and the weight decreases for older prices.DEMA
(Double Exponential Moving Average): A more advanced moving average that aims to reduce the lag found in traditional EMAs by using two EMAs in its calculation.TEMA
(Triple Exponential Moving Average): Similar to DEMA, TEMA further reduces lag and increases responsiveness by applying a triple smoothing formula involving multiple EMAs.TRIMA
(Triangular Moving Average): A weighted moving average where the middle portion of the data series receives the most weight, with weights decreasing in a triangular pattern towards the beginning and end of the period.KAMA
(Kaufman Adaptive Moving Average): This moving average adapts its responsiveness based on market volatility. It follows prices more closely when volatility is low and smooths out more when volatility is high.MAMA
(MESA Adaptive Moving Average): Developed by John Ehlers, this is an adaptive moving average that uses a Hilbert Transform to identify cycle periods, aiming to adjust to price changes more effectively.T3
(Triple Exponential Moving Average T3): Developed by Tim Tillson, this is a smoother and more responsive moving average that aims to reduce lag compared to traditional EMAs.