Backtesting RSI Momentum Strategies using Python

During this article, we are going to learn how to backtest RSI momentum strategies using Python. This strategy will be exactly the opposite to the mean reversion strategy that we backtested in my previous post using Python.

Backtesting Momentum Strategies using RSI with Python
Photo by eberhard grossgasteiger on Pexels.com

Note that the content in codingandfun.com 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 Momentum Strategy?

A momentum strategy basically bets on the continuation of an existing market trend. For example, if the market is or starts going up, we will bet that the trend will continue for a while and we will try to make our trading strategy based on this.

Momentum indicators can help us locate the entry and exit points to follow momentum strategies. One of this type of indicators is the Relative Strength Index (RSI). This indicator is the one that we will use in order to define our backtesting strategy with Python.

Relative Strength Index (RSI)

The Relative Strength Index (RSI) is a technical indicator that measures the speed and change of price movements. It was developed by J. Welles Wilder.

The RSI moves from 0 to 100. To set up a trading strategy following RSI, it is common to open a long position (buy the stock) if the RSI indicator goes above the level 30 from below. At that point, the stock is seen as oversold.

On the other side, when the RSI indicator on a stock goes above 70, the stock is considered to be overbought. That is, if the RSI on the stock crosses the level 70 from above, we should enter into a short position.

Since I am not a big fan of going short, we will keep our strategy to only enter long positions.

As it was suggested by the RSI developer, J. Welles Wilder , we will use a length of 14 periods to calculate the indicator.

RSI is calculated as follows:

  • RSI = 100 – (100 / (1 + RS)

Where RS is:

  • RS = Average Gain / Average Loss

Backtesting RSI Momentum Strategies using Python

Our momentum strategy to backtest will be quite easy to build. We will use the last 5 years of Apple stock prices.

As already mentioned before, we will enter a long position if the stock crosses the level 30 RSI indicator from below.

To do this, we will calculate the RSI indicator using the 14 days moving average  (To know more on moving averages in Python have a look at my previous post). Then, based on the RSI indicator and the stock closing prices of the day, we will define if we go long or if we do not hold any position on that stock for each of the days.

Calculating RSI with Python

We will start by calculating the RSI indicator for each of the 5 historical years. To do that, we need first to request from an API the last 5 years historical prices of Apple. I will use financialmodelingprep to get the stock prices.

Note that you need to sign up to financialmodelingprep in order to get your private API key. You can get one for free with up to 250 API requests a month. Alternatively, you can use another data source to get the stock prices for each of the days.

Below you can find the first part of the code. The aim is to get a Pandas DataFrame containing the closing price, return and movement for each of the days.

In addition, this DataFrame will also contain a column named up where we will get an amount only if the price of the stock goes up. The amount will be the daily price movement in absolute terms,. Otherwise, we will have 0 in the column.

Similarly, we will have a column named down where we will get the price movement of the day only if the price of the stock goes down for that day.

As suggested by the creator of the RSI indicator, we will have a 14 day period length. We will use that period length to calculate the 14 days period gain moving average and store in a variable named up. Note that the moving average is only calculated using the last 14 days. Where for the up column, we only have a value if the price movement of the day was positive (if not we will have a 0). Similar approach to calculate the down variable.

Now that we have calculated the last 14 days average gains and the 14 days average loss. We can calculate RS by dividing these two variables.

Finally, we are able to calculate the RSI applying below formula. The RSI variable will contain a Pandas series with the RSI indicator calculated for each of the days.

RSI = 100 – (100 / (1 + RS)

Below is the first part of the code. After running the script, we will end up having a Pandas DataFrame with all stock information and a RSI series containing the RSI indicator for each of the days. In the next section, we will merge the DataFrame and the Series and apply our strategy.

import requests 
import pandas as pd 
import numpy as np 

api_key = 'your api key'

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')

#reverse dates in the index to have more recent days at the end
stockprices = stockprices.iloc[::-1]

#calculate the return of the day and add as new column
stockprices['return'] = np.log(stockprices['close'] / stockprices['close'].shift(1) )

#calculate the movement on the price compared to the previous day closing price
stockprices['movement'] = stockprices['close'] - stockprices['close'].shift(1)

stockprices['up'] = np.where((stockprices['movement'] > 0) ,stockprices['movement'],0)

stockprices['down'] = np.where((stockprices['movement'] < 0) ,stockprices['movement'],0)

window_length = 14
#calculate moving average of the last 14 days  gains
up = stockprices['up'].rolling(window_length).mean()

#calculate moving average of the last 14 days  losses
down = stockprices['down'].abs().rolling(window_length).mean()

RS = up / down

RSI = 100.0 - (100.0 / (1.0 + RS))

RSI = RSI.rename("RSI")
print(RSI)
#outcome
2021-01-07    54.797906
2021-01-08    55.130181
2021-01-11    53.444169
2021-01-12    50.882637
2021-01-13    48.389185
2021-01-14    46.775709
2021-01-15    42.580639

Defining our RSI backtesting strategy with Python

Now that we have calculated our RSI indicator for Apple for each of the days, we can merge them with the stockprices DataFrame. That will let us display all information into a single dataframe in order to define our strategy.

We will store the merged Pandas Dataframe in a variable named new (I know very original name). This will contain our original DataFrame plus the RSI indicator for each of the days.

Finally, we can start defining our strategy. The strategy will be included in the long column. If the RSI of the day is below 30 and we are crossing the 30 from below, we will have 1 in the column. The 1, will indicate that we are long in the stock. Otherwise, we will have a 0. Meaning that we are not holding the stock for that particular day.

We need three lines of code to ensure that our strategy is defined appropriately. E.g. 1 when we need to be long:

  • The first line of code on the long column, ensures that we will have a 1, if RSI is below 30.
  • The second line of code, ensures that we will have 0 if the RSI is above 70.
  • Then, finally, the third line of code, will ‘forward fill’ and propagate the last valid observation forward to get rid of the nan.

Above three lines will ensure that we only go long when we cross the line from below 30.

Finally, we calculate the gain and loss for each of the day and the accumulative return. Below screenshot shows that with this strategy, we would get an accumulative return of 50% if we followed the strategy during the last 5 years.

Note that last performance is not a good indicator of future performance. Therefore, do not use the results and conclusion of this post for your investment decisions.

new = pd.merge(stockprices, RSI, left_index=True, right_index=True)

#If the indicator’s line crosses the level 30 from below, a long position (Buy) is opened.  
new['long'] = np.where((new['RSI'] < 30),1,np.nan)
new['long'] = np.where((new['RSI'] > 70),0,new['long'])

new['long'].ffill(inplace=True)

new['gain_loss'] = new['long'].shift(1) * new['return']

new['total'] =  new['gain_loss'].cumsum()

print(new.tail(600))

#outcome
Backtesting Momentum Strategy Python
Backtesting Momentum Strategy Python

Wrapping Up

Is the 49% return a good return for the 5 years? Well difficult to say without a benchmark. We could compare this with other strategies. For instance, what would be the return if we simply buy the stock 5 years ago and hold until today?

Let’s find out by running below code.

We would get a 353% return by simply buying and holding the stock compared to only 49% by following the RSI momentum strategy.

So in this case, the momentum strategy based on RSI indicator seems to be much worse. And on top of that, we should consider the transaction costs of following an active strategy versus owning an stock for 5 years.

#alternative strategy of buying and hold stock:
return_holding = (new['close'][-1] - new['close'][1]) / new['close'][1]
print(return_holding)

#outcome
3.53

This strategy may work better in periods where the market does not have such clear trend. For example, when market moves sideways.

Hope that you have enjoy the post on backtesting RSI Momentum Strategies using Python

I have a video on this post explaining step by step how to build the RSI momentum strategy with Python. Feel free to have a look below:

Backtesting RSI Momentum Strategy