Backtesting with Python – Moving Average Strategy

In this post, we will learn how to do backtesting in Python. For that, we will build a backtesting with Python on a simple moving average (MA) strategy.

In one of my latest posts, I showed how to compute and plot a moving average strategy using Python. Now, we will learn to simulate how the moving average strategy would have performed over the last few months by doing a backtesting on our algorithm.

Backtesting with Python - Moving Average
Photo by Pixabay on Pexels.com

What is Backtesting?

Before moving to build our backtesting strategy on the moving averages, I would like to quickly made clear what backtesting is.

Backtesting is nothing more than taking a trading strategy and test how well that strategy would have performed using historical prices. The difficult part about backtesting is to find a good trading strategy to backtest.

Why Backtesting and why Using Python to Backtest Trading Strategies?

If we performed a backtesting and the results are good, then it may be an indication that we have a good strategy to apply going forward.

The problem is how can we perform a backtesting. For users who are not familiar with Python, you could build a model in Excel to backtest your trading strategies. However, this approach is rather slow and non-replicable.

Fortunately for us, we can make use of Python and its great libraries to do our Backtesting. By doing the backtest using Python instead of Excel, we will be able to run the test using big amounts of historical data and using different companies.

Let’s backtest our Moving Average algorithm that we created in one of my previous post.

Recap the Moving Average Strategy

Let’s first quickly recap what our Moving Average Strategy is about. The model was rather simple, we built a Python script to calculate and plot a short moving average (20 days) and long moving average (250 days).

I recommend you to have a look at my previous post to learn more in detail about moving averages and how to build the Python script. In this post, I will only post the code to get the moving averages and the stock prices of the selected stock.

Note that you need to sign up to financialmodelingprep in order to get an API key. You can get one for free with up to 250 API requests a month. If you opt to sign up for a paid subscription using my link, you will get a 25% discount.

import requests
import pandas as pd
import matplotlib.pyplot as plt

api_key = 'your api key'

def stockpriceanalysis(stock):
    stockprices = requests.get(f"https://financialmodelingprep.com/api/v3/historical-price-full/{stock}?apikey={api_key}")
    stockprices = stockprices.json()

#Parse the API response and select only last 1200 days of prices
    stockprices = stockprices['historical'][-1200:]

#Convert from dict to pandas datafram

    stockprices = pd.DataFrame.from_dict(stockprices)
    stockprices = stockprices.set_index('date')
    #20 days to represent the 22 trading days in a month
    stockprices['20d'] = stockprices['close'].rolling(20).mean()
    stockprices['250d'] = stockprices['close'].rolling(250).mean()
  
stockpriceanalysis('aapl')   

Python Backtesting Strategy – Moving Averages

To perform the backtesting with Python we will simulate below scenario:

  • Go long on 100 stocks (i.e. buy 100 stocks), when the short term moving average crosses above the long term moving average. This is known as golden cross.
  • Sell the stock a few days later. For instance, we will keep the stock 20 days and then sell them.
  • Compute the profit

It is a very simple strategy. We will have historical daily close prices for the selected stock. The strategy could also be used with minutes or hourly data but I will keep it simple and perform the backtesting based on daily data.

Since I do not expect to have many entry points, that is when we buy the stocks, I will ignore the transaction costs for simplicity.

Remember from our previous post, that if we run the script by passing the name of the stock to analyse as an argument, we will get a Pandas DataFrame called stockprices containing the closing price and moving averages from the last 1200 days.

stockpriceanalysis('aapl')

#Outcome stored in a DataFrame call stockprices
	        close	20d	         250d
date			
2020-02-07	320.03	316.7825	224.76192
2020-02-10	321.55	317.3435	225.36456
2020-02-11	319.61	317.4760	225.96228
2020-02-12	327.20	318.2020	226.58788

Moving Average Backtesting Strategy in Python

To backtest the algorithm in Python, we start by creating a list containing the profit for each of our long positions.

First (1), we create a new column that will contain True for all data points in the data frame where the 20 days moving average cross above the 250 days moving average.

Of course, we are only interested in the first or second day when the crossover happens (i.e. 20 days MA goes over 250 days MA). Therefore, we are interested in locating the first or second date (rows) where the crossover happen (2).

As well stated in this article, we will use the two-day rule only (ie we start the trade only after it is confirmed by one more day’s closing), and keep the date as the entry point only if the 20 days MA is above 250 days MA two days in a row. This approach will help us to avoid daily trading noise fluctuations. When this happens, we will have the entry points in the column firstbuy where the value equals to True:

#Start longposiiton list

longpositions = []
# (1)
stockprices['buy'] =(stockprices['20d'] > stockprices['250d'])
# (2)
stockprices['firstbuy'] =   ((stockprices['buy'] == True) & (stockprices['buy'].shift(2) == False)& (stockprices['buy'].shift(1) == True))

# (3) identify the buying points 
buyingpoints = np.where(stockprices['firstbuy'] == True)
print(buyingpoints)

#Outcome
(array([ 307,  970, 1026]),)

The rule (stockprices[‘buy’].shift(2) == False), helps us to find out the first date after the crossover has happened.

Now we have in the variable buyingpoints (3), the dates where we should enter enter the market with our long strategy.

Each of the elements in the array buyingpoints represent the row where we need to go long. Therefore, we can loop though them to get the close price and buy 100 stocks (4).

Then, we keep the stocks for 20 days (5) and sell the 100 stocks at +20 days close price.

#(4)
for item in buyingpoints[0]:
    #close price of the stock when 20MA crosses 250 MA 
    close_price = stockprices.iloc[item].close

    #Enter long position by buying 100 stocks
    long = close_price*100

    # (5) sell the stock 20 days days later:
    sellday = item + 20
    close_price = stockprices.iloc[sellday].close

    #Sell 20 days later:
    sell = close_price*100
   
    # (6) Calculate profit
    profit = sell - long

    longpositionsprofit.append(profit)

Finally, we calculate the profit and add the result of the strategy to the longpositionprofit array (6).

Moving Average Backtesting Results

To find out how we did with our strategy, we can print out the long position profit list and calculate the sum:

print(longpositionsprofit)
#outcome
(array([ 307,  970, 1026]),)

print(sum(longpositionsprofit))
#outcome
2100.0

Great, our backtesting strategy for Apple show us that over 1,200 days, we entered a long position and sell after 20 days a total of three times.

In the first occasion, we got a profit of $307. In the second occasion the profit was of $970. And in the last long position we amounted a profit of $1,026. That makes a total of $2,100.

Not bad at all. However, the profit is not telling us anything. We need to somehow find a benchmark and compare against it.

For instance, how we would have done if we had bought the stock 1,200 days ago and keep until today? Lets find out. We can calculate the profit of buying and holding the stock by getting the last available price and the first available price in our stockprices DataFrame.

firstdayprice = stockprices.iloc[0].close
lastdayprice = stockprices.iloc[-1].close

profit = 100*(lastdayprice - firstdayprice)
profit

#Outcome
15906.999999999996

Interesting, by just holding the stock for 1,200 days, our profit would have been $15,906 plus the annual dividends. Much higher than if we had followed the moving average Strategy.

In case you are getting an error when running the code, it means that the script could not find the desired stock. Select a different company and it will eventually work.

Wrapping Up

We have backtested a simple strategy of buying the stock when the 20 days MA crosses above the 250 days MA. Then, we kept the stock for 20 days before selling it.

This was rather a simple strategy. We may have followed some other strategies instead. For instance, we could have bought the stock when the moving average Crossover took place and kept the stock until the end. Or, we could have just sold the stock if the 250 days moving average crosses below the 20 days moving average.

I will let you now play around and test these other strategies. Happy to get your feedback in my Twitter account.

As a follow-up on this post on technical analysis, you can have a look at my other post on how to perform a technical analysis using Bollinger Bands with Python.

If you like my blog on Python for Finance, I would be more than happy if you can support and can share the posts in your social media.

Please note that this post is not intended to offer investment advise. I am not a financial advisor and all posts in codingandfun.com are intended for educational purposes on the use of Python for Finance. Before making any investment decisions, please consult with a financial professional.

See below the whole Python script for backtesting moving average strategies for any company. Just replace Apple by any other company stockpriceanalysis(‘aapl’)

#Moving Averages and backtesting 
import requests
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np

api_key = 'your api key'

def stockpriceanalysis(stock):
    stockprices = requests.get(f"https://financialmodelingprep.com/api/v3/historical-price-full/{stock}?apikey={api_key}")
    stockprices = stockprices.json()

#Parse the API response and select only last 1200 days of prices
    stockprices = stockprices['historical'][-1200:]

#Convert from dict to pandas datafram

    stockprices = pd.DataFrame.from_dict(stockprices)
    stockprices = stockprices.set_index('date')
    #20 days to represent the 22 trading days in a month
    stockprices['20d'] = stockprices['close'].rolling(20).mean()
    stockprices['250d'] = stockprices['close'].rolling(250).mean()
    
    ###STARTING BACKTESTING STRATEGY
    #Start longposiiton list
    longpositionsprofit = []

    shortpositions = []

    stockprices['buy'] =(stockprices['20d'] > stockprices['250d'])

    #We find the first True since it is first day when the line has crossed. Also, ensure that we have at least two days where sticoprices
    stockprices['firstbuy'] =   ((stockprices['buy'] == True) & (stockprices['buy'].shift(2) == False)& (stockprices['buy'].shift(1) == True))


    buyingpoints = np.where(stockprices['firstbuy'] == True)

    for item in buyingpoints[0]:
        #close price of the stock when MA crosses
        close_price = stockprices.iloc[item].close
        #Enter long position
        long = close_price*100
        sellday = item + 20
        close_price = stockprices.iloc[sellday].close
        #Sell 20 days later:
        sell = close_price*100

        #Calculate profti
        profit = sell - long

        longpositionsprofit.append(profit)
        
    #Result of Moving Average Strategy of going long
    print(longpositionsprofit )
    print(str(sum(longpositionsprofit)) + ' is the profit of Moving Averag Strategy')

    #Result of just holding the stock over 1200 days
    firstdayprice = stockprices.iloc[0].close
    lastdayprice = stockprices.iloc[-1].close

    profit = 100*(lastdayprice - firstdayprice)
    print(str(profit) + ' is the profit of holding over the selected period of 1200 days')


#Change apple for the ticker of any company you want to backtest
stockpriceanalysis('AAPL')