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

In [1]:
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()
Out [1]:
close high low open volume ma_10 ma_20
date
2020-01-02 72.716080 72.776606 71.466820 71.721026 135480400 69.823918 68.134838
2020-01-03 72.009102 72.771729 71.783947 71.941313 146322800 70.221224 68.503815
2020-01-06 72.582893 72.621631 70.876060 71.127851 118387200 70.650618 68.892299
2020-01-07 72.241562 72.849239 72.021246 72.592609 108872000 70.939881 69.211276
2020-01-08 73.403633 73.706264 71.943744 71.943744 132079200 71.387836 69.610548

Implementation

Next, I will show you how to backtest using the Moving Average (MA) Crossover strategy.

Notes:

  1. We pass a pandas DataFrame to the bt.Strategy and access the technical indicator of the timestamp in the next method shortly.
  2. If row["ma_10"] >= row["ma_20"], we call self.buy to buy; otherwise, we sell.
In [2]:
import warnings

warnings.simplefilter(action='ignore', category=FutureWarning)
warnings.simplefilter(action='ignore', category=UserWarning)
In [3]:
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)
2020-01-03T00:00:00, B: 71.94131293879263 134.26
2020-02-25T00:00:00, S: 73.03425797216033 -134.26
2020-04-14T00:00:00, Order Canceled/Margin/Rejected
2020-04-15T00:00:00, B: 68.53256171861511 141.12
2020-09-15T00:00:00, S: 115.38342123925264 -141.12
2020-10-09T00:00:00, B: 112.40938507947116 144.06
2020-10-29T00:00:00, S: 109.57184138543089 -144.06
2020-11-10T00:00:00, B: 112.86704412515682 137.2
2021-02-19T00:00:00, S: 127.40608766559274 -137.2
2021-04-07T00:00:00, B: 123.09202364516216 141.12
2021-05-11T00:00:00, S: 121.01794071068073 -141.12
2021-06-15T00:00:00, B: 127.3285349014823 132.3
2021-09-20T00:00:00, S: 141.12109417657655 -132.3
2021-10-20T00:00:00, B: 145.92978977650145 126.42
2022-01-19T00:00:00, S: 167.07646257144611 -126.42
2022-02-03T00:00:00, B: 171.47940471663034 121.52
2022-02-22T00:00:00, S: 162.34938552584524 -121.52
2022-03-24T00:00:00, B: 168.33241890873853 117.6
2022-04-18T00:00:00, S: 161.30625525719185 -117.6
2022-07-08T00:00:00, B: 143.1538148575739 129.35999999999999
2022-09-01T00:00:00, S: 154.58328048345916 -129.35999999999999
2022-10-27T00:00:00, B: 146.12576416144415 135.24
2022-11-04T00:00:00, S: 140.4568862236132 -135.24
2022-11-16T00:00:00, B: 147.4160140311921 128.38
2022-11-30T00:00:00, S: 139.77482505617792 -128.38
2022-12-01T00:00:00, B: 146.5065813559001 121.52
2022-12-07T00:00:00, S: 140.55574731051863 -121.52
2023-01-23T00:00:00, B: 136.53251917986734 124.46
2023-08-07T00:00:00, S: 180.5610076771683 -124.46
2023-09-01T00:00:00, B: 188.11129296200207 118.58
2023-09-11T00:00:00, S: 178.75981812268648 -118.58
2023-10-12T00:00:00, B: 178.759844529376 116.62
2023-10-23T00:00:00, S: 169.6664671323833 -116.62
2023-11-07T00:00:00, B: 177.87629469765844 109.75999999999999
2024-01-03T00:00:00, S: 183.1205708792931 -109.75999999999999
2024-01-24T00:00:00, B: 194.25372573180164 102.89999999999999
2024-02-02T00:00:00, S: 178.78658786144558 -102.89999999999999
2024-05-03T00:00:00, Order Canceled/Margin/Rejected
2024-05-06T00:00:00, B: 181.49302494186847 99.96
2024-08-02T00:00:00, S: 218.4159219627094 -99.96
2024-08-16T00:00:00, B: 223.4282615540399 96.03999999999999
2024-09-11T00:00:00, S: 220.97367404683666 -96.03999999999999
2024-09-23T00:00:00, B: 226.8407407931877 93.1
2024-11-04T00:00:00, S: 220.5046966292777 -93.1
2024-11-22T00:00:00, B: 227.80952873867184 89.17999999999999
2025-01-10T00:00:00, S: 239.7463920626497 -89.17999999999999
2025-02-18T00:00:00, B: 244.1499938964844 86.24
2025-03-07T00:00:00, S: 235.11000061035162 -86.24
output png
Start date2020-01-02
End date2025-04-28
Total months63
Backtest
Annual return 14.781%
Cumulative returns 107.803%
Annual volatility 20.345%
Sharpe ratio 0.78
Calmar ratio 0.60
Stability 0.52
Max drawdown -24.59%
Omega ratio 1.19
Sortino ratio 1.17
Skew 0.38
Kurtosis 7.56
Tail ratio 1.12
Daily value at risk -2.5%
Worst drawdown periods Net drawdown in % Peak date Valley date Recovery date Duration
0 24.59 2022-01-03 2022-12-07 2023-06-15 379
1 24.57 2023-07-31 2024-05-06 NaT NaN
2 16.69 2020-09-01 2020-11-23 2021-01-22 104
3 13.08 2020-02-12 2020-04-21 2020-05-08 63
4 12.16 2021-01-26 2021-06-15 2021-07-14 122
Stress Events mean min max
Covid 0.06% -7.86% 10.21%
output png
output png

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:

  1. SMA (Simple Moving Average): Calculates the average price over a specified period, giving equal weight to all data points.
  2. EMA (Exponential Moving Average): Gives more weight to recent prices, making it more responsive to new price changes than an SMA.
  3. 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.
  4. 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.
  5. TEMA (Triple Exponential Moving Average): Similar to DEMA, TEMA further reduces lag and increases responsiveness by applying a triple smoothing formula involving multiple EMAs.
  6. 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.
  7. 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.
  8. 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.
  9. 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.