The Direct Reversal Trading Strategy in Python.
Creating & Evaluating the Direct Reversal Trading Strategy Using Python.
The Relative Strength Index is generally the most used bounded technical indicator. There are many strategies that can be formed around it and while not all of them seem to add direct value to profitability or at least they lack enough evidence to prove their usefulness, it should not stop us from thinking about new ways to exploit this simple yet wonderful tool that is the RSI.
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:
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, 5) # 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.
Presenting the Direct Reversal Strategy
Keeping in mind the extremes strategy which is based on the concept of oversold and overbought levels, the direct reversal strategy takes into account violent flips where a reading from the oversold zone goes directly into the overbought zone and vice versa.
This is a way of measuring the strength of the last move relative to normality. The idea is to say that the current extreme overthrew the previous extreme and that we now have to correct it. The trading rules are:
A buy (Long) signal is generated whenever the 2-period RSI goes below 20 while the previous reading is above 80.
A sell (Short) signal is generated whenever the 2-period RSI goes above 80 while the previous reading is below 20.
The syntax for the above conditions is very simple as shown in the below code snippet. Remember to have an OHLC array of historical data ready and named my_data. We have seen how to do this in previous articles.
# Indicator Parameters lookback = 2 lower_barrier = 20 upper_barrier = 80
my_data = adder(my_data, 10) my_data = rsi(my_data, lookback, 3, 4)
def signal(Data, rsi_col, buy, sell): Data = adder(Data, 10) for i in range(len(Data)): if Data[i, rsi_col] <= lower_barrier and Data[i - 1, rsi_col] >= upper_barrier: Data[i, buy] = 1 elif Data[i, rsi_col] >= upper_barrier and Data[i - 1, rsi_col] <= lower_barrier: Data[i, sell] = -1 return Data my_data = signal(my_data, 4, 6, 7)
We need to tell the signal function that is must loop chronologically around the historical OHLC data, find the extremes where their previous values are on the other extremes, and then input 1 in the buy column or -1 in the sell column. This way we will be able to visualize and count the number of signals on the selected charts.
The above chart shows a sample of the generated signals based on the conditions we have imposed. The green arrows point to buy signals while the red arrows point to short selling signals.
Let us now evaluate the signals using one simple neutral prediction metric, the Signal Quality.
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:
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 period = 2 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 EURUSD = 52.43% on ~ 1000 trades # Output Signal Quality USDCHF = 53.51% on ~ 1000 trades
A signal quality of 53.51% means that on 100 trades, we tend to have 53 of them close profitably. In essence, the strategy is interesting but needs more back-testing and more research. It is based on temporarily euphoria or fear and the reaction afterwards. The benefit of using the RSI is that it allows us to quantify how much is too much with regards to the reaction seen.
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.