Backtesting Mean Reversion Strategy with Python

In this post, we are going to backtest a mean reversion strategy using Python. First, we will learn more about what a mean reversion strategy is. Then, we will backtest a mean reversion strategy with Python using Apple as an example.

Mean Reversion Strategy backtesting with Python
Photo by Alex Knight on Pexels.com

As a side note, all content in this blog is only for education purposes, and therefore, it should not be used for trading or investing decisions. In addition, the content of this blog may not be free of errors.

What is a Mean Reversion Strategy?

A mean reversion strategy bets that stock prices trending up will revert back to the mean. That is, if we follow this strategy, we should expect a stock trading above the short or mid-term mean to go down in price until it finds convergence to the mean.

On the other hand, a momentum strategy suggest exactly the opposite. As per the momentum strategy, rather than following a reversions strategy, we should instead follow the trend (i.e. momentum) of the market. That is sell losers and buy winners. In my previous post, we backtested a RSI momentum strategy using Python. In this post, we will focus on the mean reversion strategy.

It is a strategy that has been quite popular since it is easy to build. There are different views on whether this strategy works or not. Let’s find out in this post whether it works using Apple and the last 5 year of prices.

Backtesting Mean Reversion Strategy with Python

In this post, we will create a simple strategy to test. Our strategy will go long, that is buy the stock, if the stock has recently fall down quite a bit in price.

To do this, we will use the 20 days moving average and the stock closing prices. (If you want to know how to perform a moving average technical analysis with Python, have a look at my previous post).

When the closing price is below the 20 days moving average plus a margin of 2$ (to be sure the cross has consolidated), We will buy the stock and go long until the price goes above the 20 days moving average. In that moment, we will sell the stock and hold no position on it.

To get the stock prices we will use financialmodelingprep. And to perform the backtesting, we will use Pandas and Numpy. We will test the backtesting strategy using the last 1200 daily Apple close prices.

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. Alternatively, you can use any other API or source to get your prices, the code should still work.

In the last section of the post, you will find the whole Python script. In the following section, I will just comment a few of the code lines which I find more interesting.

Python Script to Test Mean Reversion

We will start by making a request to the API end point to retrieve historical closing prices.

Then, we keep only the last 5 years of prices by slicing the dictionary using below line of code:

stockprices = stockprices['historical'][0:1200]

Next, we convert the stock prices dictionary to a Pandas DataFrame using pd.DataFrame.from_dict(stockprices) and set the index to be the date. If you print stockprices DataFrame now, you will find out that our most recent days are at the beginning of the DataFrame and the oldest days are in the last rows. I want to reverse this.

In order to reverse the order of the dates, we use below iloc pandas method:

stockprices = stockprices.iloc[::-1]

Now we have the most recent dates at the end of the DataFrame as per below screenshot:

Pandas DataFrame - Apple Historical Prices
Pandas DataFrame – Apple Historical Prices

We can move on with the code and calculate the moving average and add it as a new column to our DataFrame. For that, we use the rolling method offered by pandas. Then, calculate the stock return for each of the days and the difference between the 20 day moving average and the close price. That will be added as two new columns of the DataFrame.

stockprices['20d'] = stockprices['close'].rolling(20).mean() 

stockprices['return'] = np.log(stockprices['close'] / stockprices['close'].shift(1) )

stockprices['difference'] = stockprices['close'] - stockprices['20d']

Defining our Strategy

As mention before, our strategy will be rather simple. When the closing price is below the 20 days moving average we will go long and hold until the price moves above the 20 days moving average.

We can use Numpy where to define our position on the stock for each of the days. If the close stock price of the day is below the moving average plus a margin of $2, we will enter into a long position. This will be shown as 1 in the column Long. Otherwise, it will be shown a Nan.

In the next line of code, we multiply the difference of the current day with the previous day difference. This will let us see when the close prices crosses above the 20 day moving average since then there will be a change of sign. If this is the case, we will then hold the position. This is indicated by a 0 in the long column.

Also, the code will only trigger the long position when:

  • We have two consecutive days of negative difference (to ensure that there is a consolidated trend)
  • And the latest of the days the difference is below -2

Finally, we use ffill pandas method in order to replace Nan with 0, indicating that in these days we hold our position.

stockprices['long'] = np.where(stockprices['difference'] < -2 ,1,np.nan)
stockprices['long'] = np.where(stockprices['difference'] * stockprices['difference'].shift(1) < 0, 0, stockprices['long'])
stockprices['long'] = stockprices['long'].ffill().fillna(0)

Finally, we can calculate the return of our strategy. We will calculate that for each of the days cumulatively using cumsum() and show the result in the total column.

stockprices['gain_loss'] = stockprices['long'].shift(1) * stockprices['return']
stockprices = stockprices.dropna(subset=['20d'])

stockprices['total'] =  stockprices['gain_loss'].cumsum()
print(stockprices.tail(30))

Results and Wrapping Up

Now in our final DataFrame, we can see the results of this strategy in the column Total. The column Gain_loss shows the result of the day. Since in the most recent days we were just holding the position, we do not have any gain and loses in below screenshot.

As we can also see in the total column of the screenshot below, the mean reversion strategy seems not to be very effective when looking at the last 5 years of Apple prices.

Since the start of the strategy, and looking on the most recent date, we would have made a negative return of 21% if we had followed the strategy. Not a good strategy at all to follow! Specially knowing that the returns on Apple in the last 5 years were very high. In practice, the returns would be worse since we would have to add the transaction costs for buying and selling the stock.

If you have enjoyed the post, I recommend you to have a look at some of my other posts on Python for Finance. For example, you can follow the following post in order to backtest a different strategy using moving averages.

For your reference, you can find the whole code below. In addition, you can also find the version of the code in a Youtube video.

Strategy Mean Reversion Backtesting
Strategy Mean Reversion Backtesting
import requests 
import pandas as pd 
import numpy as np 

api_key = 'enter your api key here'

stock = 'AAPL'
stockprices = requests.get(f'https://financialmodelingprep.com/api/v3/historical-price-full/{stock}?serietype=line&apikey={api_key}').json()

stockprices = stockprices['historical'][0:1200]

stockprices = pd.DataFrame.from_dict(stockprices)
stockprices = stockprices.set_index('date')

stockprices = stockprices.iloc[::-1]

stockprices['20d'] = stockprices['close'].rolling(20).mean() 


stockprices['return'] = np.log(stockprices['close'] / stockprices['close'].shift(1) )


stockprices['difference'] = stockprices['close'] - stockprices['20d']

stockprices['long'] = np.where(stockprices['difference'] < -2 ,1,np.nan)
stockprices['long'] = np.where(stockprices['difference'] * stockprices['difference'].shift(1) < 0, 0, stockprices['long'])
stockprices['long'] = stockprices['long'].ffill().fillna(0)

stockprices['gain_loss'] = stockprices['long'].shift(1) * stockprices['return']
stockprices = stockprices.dropna(subset=['20d'])

stockprices['total'] =  stockprices['gain_loss'].cumsum()
print(stockprices.tail(30))

1 thought on “Backtesting Mean Reversion Strategy with Python

Comments are closed.