Discover more from All About Trading!
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.
Medium is a hub to interesting reads. I read a lot of articles before I decided to start writing. Consider joining Medium using my referral link (at NO additional cost to you).
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)
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 Datadef 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)
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.
Knowledge must be accessible to everyone. This is why, from now on, a purchase of either one of my new books “Contrarian Trading Strategies in Python” or “Trend Following Strategies in Python” comes with free PDF copies of my first three books (Therefore, purchasing one of the new books gets you 4 books in total). The two new books listed above feature a lot of advanced indicators and strategies with a GitHub page. You can use the below link to purchase one of the two books (Please specify which one and make sure to include your e-mail in the note).
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): # First trough 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): # First trough 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 want to see how to create all sorts of algorithms yourself, feel free to check out Lumiwealth. From algorithmic trading to blockchain and machine learning, they have hands-on detailed courses that I highly recommend.
Learn Algorithmic Trading with Python Lumiwealth
Learn how to create your own trading algorithms for stocks, options, crypto and more from the experts at Lumiwealth. Click to learn more
To sum up, what I am trying to do is to simply contribute to the world of objective technical analysis which is promoting more transparent techniques and strategies that need to be back-tested before being implemented. This way, technical analysis will get rid of the bad reputation of being subjective and scientifically unfounded.
I recommend you always follow the the below steps whenever you come across a trading technique or strategy:
Have a critical mindset and get rid of any emotions.
Back-test it using real life simulation and conditions.
If you find potential, try optimizing it and running a forward test.
Always include transaction costs and any slippage simulation in your tests.
Always include risk management and position sizing in your tests.
Finally, even after making sure of the above, stay careful and monitor the strategy because market dynamics may shift and make the strategy unprofitable.