Trading the RSI the Smart Way — A Study in Python.
Adding Filters to RSI Signals to Improve the Quality of the Trades.
Sometimes, tweaks in technical indicators can provide us with more insight and better signals. This article discusses a small modification in the way we use the Relative Strength Index and then compares a sample of a result with the regular Relative Strength Index.
I have just published a new book after the success of my previous one “New Technical Indicators in Python”. It features a more complete description and addition of structured trading strategies with a GitHub page dedicated to the continuously updated code. If you feel that this interests you, feel free to visit the below link, or if you prefer to buy the PDF version, you could contact me on LinkedIn.
The Relative Strength Index
The RSI is without a doubt the most famous momentum indicator out there, and this is to be expected as it has many strengths especially in ranging markets. It is also bounded between 0 and 100 which makes it easier to interpret. Also, the fact that it is famous, contributes to its potential.
This is because the more traders and portfolio managers look at the RSI, the more people will react based on its signals and this in turn can push market prices. Of course, we cannot prove this idea, but it is intuitive as one of the basis of Technical Analysis is that it is self-fulfilling.
The RSI is calculated using a rather simple way. We first start by taking price differences of one period. This means that we have to subtract every closing price from the one before it. Then, we will calculate the smoothed average of the positive differences and divide it by the smoothed average of the negative differences. The last calculation gives us the Relative Strength which is then used in the RSI formula to be transformed into a measure between 0 and 100.
To calculate the Relative Strength Index, we need an OHLC array (not a data frame). This means that we will be looking at an array of 4 columns. The function for the Relative Strength Index is therefore:
# The function to add a number of columns inside an array def adder(Data, times): for i in range(1, times + 1): new_col = np.zeros((len(Data), 1), dtype = float) Data = np.append(Data, new_col, axis = 1) return Data
# The function to delete a number of columns starting from an index def deleter(Data, index, times): for i in range(1, times + 1): Data = np.delete(Data, index, axis = 1) return Data # The function to delete a number of rows from the beginning def jump(Data, jump): Data = Data[jump:, ] return Data
# Example of adding 3 empty columns to an array my_ohlc_array = adder(my_ohlc_array, 3)
# Example of deleting the 2 columns after the column indexed at 3 my_ohlc_array = deleter(my_ohlc_array, 3, 2)
# Example of deleting the first 20 rows my_ohlc_array = jump(my_ohlc_array, 20)
# Remember, OHLC is an abbreviation of Open, High, Low, and Close and it refers to the standard historical data file
def ma(Data, lookback, close, where): Data = adder(Data, 1) for i in range(len(Data)): try: Data[i, where] = (Data[i - lookback + 1:i + 1, close].mean()) except IndexError: pass # Cleaning Data = jump(Data, lookback) return Data
def ema(Data, alpha, lookback, what, where): alpha = alpha / (lookback + 1.0) beta = 1 - alpha # First value is a simple SMA Data = ma(Data, lookback, what, where) # Calculating first EMA Data[lookback + 1, where] = (Data[lookback + 1, what] * alpha) + (Data[lookback, where] * beta) # Calculating the rest of EMA for i in range(lookback + 2, len(Data)): try: Data[i, where] = (Data[i, what] * alpha) + (Data[i - 1, where] * beta) except IndexError: pass return Data
def rsi(Data, lookback, close, where, width = 1, genre = 'Smoothed'): # Adding a few columns Data = adder(Data, 7) # Calculating Differences for i in range(len(Data)): Data[i, where] = Data[i, close] - Data[i - width, close] # Calculating the Up and Down absolute values for i in range(len(Data)): if Data[i, where] > 0: Data[i, where + 1] = Data[i, where] elif Data[i, where] < 0: Data[i, where + 2] = abs(Data[i, where]) # Calculating the Smoothed Moving Average on Up and Down absolute values if genre == 'Smoothed': lookback = (lookback * 2) - 1 # From exponential to smoothed Data = ema(Data, 2, lookback, where + 1, where + 3) Data = ema(Data, 2, lookback, where + 2, where + 4) if genre == 'Simple': Data = ma(Data, lookback, where + 1, where + 3) Data = ma(Data, lookback, where + 2, where + 4) # Calculating the Relative Strength Data[:, where + 5] = Data[:, where + 3] / Data[:, where + 4] # Calculate the Relative Strength Index Data[:, where + 6] = (100 - (100 / (1 + Data[:, where + 5]))) # Cleaning Data = deleter(Data, where, 6) Data = jump(Data, lookback)
Introduction to Correlation
Correlation is the degree of linear relationship between two or more variables. It is bounded between -1 and 1 with one being a perfectly positive correlation, -1 being a perfectly negative correlation, and 0 as an indication of no linear relationship between the variables (they relatively go in random directions). The measure is not perfect and can be biased by outliers and non-linear relationships, it does however provide quick glances to statistical properties. Two famous types of correlation exist and are commonly used:
Spearman correlation measures the relationship between two continuous or ordinal variables. Variables may tend to change together, but not necessarily at a constant rate. It is based on the ranks of values rather than the raw data.
Pearson correlation measures the linear relationship between two continuous variables. A relationship can be considered linear when a change in one is accompanied with a proportional change in the other.
The measure is not perfect and can be biased by outliers and non-linear relationships, it does however provide quick glances to statistical properties.
We can code the correlation function between two variables in Python using the below. Note that it has to be an array and not a data frame:
from scipy.stats import pearsonr
def rolling_correlation(Data, first_data, second_data, lookback, where): # Adding an extra column Data = adder(Data, 1) for i in range(len(Data)): try: Data[i, where] = pearsonr(Data[i - lookback + 1:i + 1, first_data], Data[i - lookback + 1:i + 1, second_data]) except ValueError: pass Data = jump(Data, lookback) return Data
If you are also interested by more technical indicators and using Python to create strategies, then my best-selling book on Technical Indicators may interest you:
The Correlation-Adjusted Relative Strength Index
The basic idea of the indicator is that whenever the correlation is high between the indicator and the market price, then it is time to take its signals more seriously. Therefore, we will follow the below steps to create the strategy:
Calculate a 2-period RSI on the market price.
Calculate a 3-period rolling correlation between the market price and its 2-period RSI. This is done to see how well the two move together.
Calculate a 3-period RSI on the rolling correlation measure we have found in the previous step.
Note that the choice of lookback periods is very subjective and can be anything. You can use a 13-period RSI if you are trading manually.
# Indicator Parameters lookback_corr = 3 lookback_rsi = 2
# Calculating the 2-period RSI on the market price my_data = rsi(my_data, lookback_rsi, 3, 4)
# Calculating the rolling 3-period correlation between the market price and the 2-period RSI my_data = rolling_correlation(my_data, 3, 4, lookback_corr, 5)
# Calculating the 3-period RSI of the 3-period correlation my_data = rsi(my_data, lookback_corr, 5, 6)
The trading conditions we will be making can be summarized as below:
A buy (Long) signal is generated whenever the 2-period RSI is at or below 5 while the 3-period RSI of the correlation is at or above 50.
A sell (Short) signal is generated whenever the 2-period RSI is at or above 95 while the 3-period RSI of the correlation is at or above 50.
The condition of the high correlation is created to filter the signals and is basically telling us that the current RSI is more predictive when the correlation is high between it and the market price. On the other hand, whenever the correlation is low between the two, we tend to say that there will likely not be a reaction in parallel of the expected outcome. The signal function can be written down as follows.
def signal(Data, rsi_col, corr_rsi, buy, sell):
Data = adder(Data, 10) for i in range(len(Data)): if Data[i, rsi_col] <= 5 and Data[i, corr_rsi] > 50 and Data[i - 1, buy] == 0 and Data[i - 2, buy] == 0 and Data[i - 3, buy] == 0 and Data[i - 4, buy] == 0: Data[i, buy] = 1
elif Data[i, rsi_col] >= 95 and Data[i, corr_rsi] > 50 and Data[i - 1, sell] == 0 and Data[i - 2, sell] == 0 and Data[i - 3, sell] == 0 and Data[i - 4, sell] == 0: Data[i, sell] = -1
There are two ways to check for the validity of the idea:
Compare the above strategy with a regular 2-period RSI strategy without any correlation. Therefore, it will be an apples-to-apples comparison albeit the lack of the correlation filter. This can give us the added value of this technique.
Compare the above strategy with the same one only the condition is inverted. This means that we will be looking to take the signals only if the RSI of the correlation is below 50 instead of above 50.
The first test is done on the hourly values of the EURUSD where we simply use a 1-hour holding period as we expect the short-term RSI to be a timing indicator. Therefore, for each signal, we will buy or sell short and hold it until the next bar. We can see that the correlation filter works better. However, this does not say that we have a good strategy because the below equity curve is based on simple rules with no risk management and no accounting for transaction costs.
The second test compares the correlation-adjusted RSI with a change in the condition of the correlation. The results also show that our intuition may be in the right path.
Remember to always do your back-tests. You should always believe that other people are wrong. My indicators and style of trading may work for me but maybe not for you.
I am a firm believer of not spoon-feeding. I have learnt by doing and not by copying. You should get the idea, the function, the intuition, the conditions of the strategy, and then elaborate (an even better) one yourself so that you back-test and improve it before deciding to take it live or to eliminate it. My choice of not providing specific Back-testing results should lead the reader to explore more herself the strategy and work on it more.
To sum up, are the strategies I provide realistic? Yes, but only by optimizing the environment (robust algorithm, low costs, honest broker, proper risk management, and order management). Are the strategies provided only for the sole use of trading? No, it is to stimulate brainstorming and getting more trading ideas as we are all sick of hearing about an oversold RSI as a reason to go short or a resistance being surpassed as a reason to go long. I am trying to introduce a new field called Objective Technical Analysis where we use hard data to judge our techniques rather than rely on outdated classical methods.