Double Top / Double Bottom RSI Trading Strategy.
Creating a Pattern Recognition Strategy Using the RSI in Python.
Classical patterns such as double tops and double bottoms have been around for decades. We tend to apply them on the market price as we like to look at pure price action. What if we try them on transformations of prices? In this article, we will apply the pattern recognition tools of such configurations on a contrarian indicator named the 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 Book of Trading Strategies
Amazon.com: The Book of Trading Strategies: 9798532885707: Kaabar, Sofien: Bookswww.amazon.com
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:
def adder(Data, times):
for i in range(1, times + 1):
new = np.zeros((len(Data), 1), dtype = float)
Data = np.append(Data, new, axis = 1)
return Data
def deleter(Data, index, times):
for i in range(1, times + 1):
Data = np.delete(Data, index, axis = 1)
return Data
def jump(Data, jump):
Data = Data[jump:, ]
return Data
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)
return Data
The Relative Strength Index is known for the extremes strategy (Oversold and Overbought levels) where we initiate contrarian positions when the RSI is close to the extremes in an attempt to fade the current trend.
The Double Top / Double Bottom Pattern
Patterns are everywhere around us. We like them because we have an idea on what they mean or what to expect from them. In its pure form, a pattern is a recurring configuration familiar to the mind or the algorithm. In the financial markets industry, we can add that it gives us a clue on the possible direction of the market after it appears. This is to say that financial patterns are desired when analyzing the charts in order to forecast the direction of the market. Among patterns are what we call Double Tops and Double Bottoms.
A Double Top is a bearish configuration characterized by having two peaks before the market breaks the neckline between them and heads lower.
A Double Bottom is a bullish configuration characterized by having two troughs before the market surpasses the neckline between them and heads higher.
The neckline is the support or resistance level between the two peaks/troughs that must be broken to validate the pattern. The chart above shows the weekly values on the AUDUSD. The two circles on the left show a Double Top configuration with the neckline in black. Once the neckline is broken, the pattern is validated and we can expect a bearish reaction. Note that the neckline is most likely the low between the two peaks.
The two circles on the right also show a Double Top pattern but a less obvious one. Some might even say it resembles other reversal patterns (Which will be covered in future articles).
The chart above shows the weekly values on the GBPUSD. The two circles on the left show a Double Top configuration with the neckline in black. The two circles on the right represent a double bottom pattern.
In the article, we will use the pattern recognition algorithm to search for the two above patterns but not on the market price, but rather on the RSI.
Creating the Strategy
Let us review the trading conditions that we will be making for the patterns to be valid on the RSI:
For a Double Bottom: The RSI must dip below the oversold area (30), then it must surpass it, followed by another dip below it but above the first dip, and finally, it must surpass the oversold area (30).
For a Double Top: The RSI must surpass the overbought area (70), then it must dip below it, followed by another surpass above it but below the first surpass, and finally, it must dip below the overbought area (70).
I have chosen to simplify the rules and not include the neckline but consider the re-integration of the extreme levels as simply the final condition. You can try to code the original condition by simply following the intuition that for a Double Bottom, the final condition must be higher than the maximum level between the two troughs and as for the Double Top, the final condition must be lower than the minimum level between the two peaks.
The above chart shows the weekly EURUSD values with the 13-period RSI. Let us apply the following signal function to find the patterns.
# Indicator Parameters
lookback = 13
lower_barrier = 30
upper_barrier = 70
width = 60
def signal(Data, indicator, width, buy, sell):
Data = adder(Data, 10)
for i in range(len(Data)):
try:
if Data[i, indicator] < lower_barrier:
for a in range(i + 1, i + width):
if Data[a, indicator] > lower_barrier:
for r in range(a + 1, a + width):
if Data[r, indicator] < lower_barrier and \
Data[r, indicator] >= Data[i, indicator] and Data[r, indicator] - Data[i, indicator] < 3:
for s in range(r + 1, r + width):
# Second trough
if Data[s, indicator] > lower_barrier:
Data[s, buy] = 1
break
else:
break
else:
break
else:
break
else:
break
except IndexError:
pass
for i in range(len(Data)):
try:
if Data[i, indicator] > upper_barrier:
for a in range(i + 1, i + width):
if Data[a, indicator] < upper_barrier:
for r in range(a + 1, a + width):
if Data[r, indicator] > upper_barrier and \
Data[r, indicator] <= Data[i, indicator] and Data[i, indicator] - Data[r, indicator] < 3:
for s in range(r + 1, r + width):
# Second trough
if Data[s, indicator] < upper_barrier:
Data[s, sell] = -1
break
else:
break
else:
break
else:
break
else:
break
except IndexError:
pass
return Data
my_data = adder(my_data, 10)
my_data = rsi(my_data, lookback, 3, 4)
my_data = signal(my_data, 4, width, 6, 7)
The above signal chart shows the trades generated on the EURUSD weekly values following the conditions given.
The function can be optimized to reflect the trader’s preferences of the patterns. The width variables is how far the algorithm will search within two peaks or troughs.
Evaluating the Strategy
Having had the signals, we now know when the algorithm would have placed its buy and sell orders, meaning, that we have an approximate replica of the past where can can control our decisions with no hindsight bias. We have to simulate how the strategy would have done given our conditions. This means that we need to calculate the returns and analyze the performance metrics. Let us see a neutral metric that can give us somewhat a clue on the predictability of the indicator or the strategy. For this study, we will use the Signal Quality metric.
The signal quality is a metric that resembles a fixed holding period strategy. It is simply the reaction of the market after a specified time period following the signal. Generally, when trading, we tend to use a variable period where we open the positions and close out when we get a signal on the other direction or when we get stopped out (either positively or negatively).
Sometimes, we close out at random time periods. Therefore, the signal quality is a very simple measure that assumes a fixed holding period and then checks the market level at that time point to compare it with the entry level. In other words, it measures market timing by checking the reaction of the market after a specified time period.
# Choosing a Holding Period for a trend-following strategy
period = 3
def signal_quality(Data, closing, buy, sell, period, where):
Data = adder(Data, 1)
for i in range(len(Data)):
try:
if Data[i, buy] == 1:
Data[i + period, where] = Data[i + period, closing] - Data[i, closing]
if Data[i, sell] == -1:
Data[i + period, where] = Data[i, closing] - Data[i + period, closing]
except IndexError:
pass
return Data
# Applying the Signal Quality Function
my_data = signal_quality(my_data, 3, 6, 7, period, 8)
positives = my_data[my_data[:, 8] > 0]
negatives = my_data[my_data[:, 8] < 0]
# Calculating Signal Quality
signal_quality = len(positives) / (len(negatives) + len(positives))
print('Signal Quality = ', round(signal_quality * 100, 2), '%')
# Output Signal Quality USDCAD = 53.67%
A signal quality of 53.67% means that on 100 trades, we tend to see a profitable result in 53 of the cases without taking into account transaction costs.
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:
New Technical Indicators in Python
Amazon.com: New Technical Indicators in Python: 9798711128861: Kaabar, Mr Sofien: Bookswww.amazon.com
Conclusion
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.