Back-testing the RSI divergence strategy on FX in Python. Should it be used as a stand-alone strategy?
This article will deal with a famous technical trading strategy called the divergence. We will define what a divergence is, learn how to code it, and present the results of a back-test over several currency pairs. A reminder, that we can buy the FX pair and short (sell) it whenever we have a trading signal generated by the system we’re about to build together.
There are multiple ways of using an indicator such as the RSI in FX trading, among them:
- Overbought (70) and oversold (30) zones: Buying when the indicator reaches 30 and shorting when the indicator hits 70.
- Crossing neutrality (50): When the indicator surpasses the 50 level, it is seen as a buying opportunity and when the indicator breaks the 50 level, it is seen as a shorting opportunity.
- Drawing supports and resistances on the RSI: Treating the indicator as a price action chart and buying/selling whenever it approaches the levels.
- Divergence: When the price is making higher highs and the RSI is making lower highs, it is seen as an exhaustion measure and a short may be established in anticipation of a correction/reversal. When the price is making lower lows and the RSI is making higher lows, it is seen as an imminent acceleration measure and a long (buy) may be established in anticipation of a rise.
What is the RSI?
The RSI has been created by J. Welles Wilder in 1978 as a momentum indicator with an optimal look-back period of 14 bars. It seeks to find overbought and oversold zones, in a way that fundamental analysts seek to find overvalued and undervalued assets. Below is a chart of the USDCAD and the RSI using a 14-period lookback.
The formula of the RSI can be found below:
As opposed to Wilder’s original smoothed moving average, the exponential moving average has been used in this study (as in most trading packages).
- Taking the spread of each period: Close of current bar - Close of previous bar
- Segregating up and down period where if the current Close is higher, the up period is positive and the down period is zero.
- Calculating a rolling exponential moving average with a period N of the up and down periods.
- Calculating the ratio of the Up to Down periods, also known as the Relative Strength — RS.
- Normalizing the values between 0 and 100 using the RSI formula.
Coding the divergence through simple walk-forward steps.
Let’s first start by applying the code to the modified RSI (the one that uses the exponential moving average rather than the smoother original average)
Assuming you have a pandas OHLC Dataframe downloaded from Metatrader 5 historical data. '''# Get the difference in price from previous step
Data = pd.DataFrame(Data)
delta = Data.iloc[:, 3].diff()
delta = delta[1:]# Make the positive gains (up) and negative gains (down) Series
up, down = delta.copy(), delta.copy()
up[up < 0] = 0
down[down > 0] = 0roll_up = pd.stats.moments.ewma(up, lookback)
roll_down = pd.stats.moments.ewma(down.abs(), lookback)# Calculate the SMA
roll_up = roll_up[lookback:]
roll_down = roll_down[lookback:]
Data = Data.iloc[lookback + 1:,].values# Calculate the RSI based on SMA
RS = roll_up / roll_down
RSI = (100.0 - (100.0 / (1.0 + RS)))
RSI = np.array(RSI)
RSI = np.reshape(RSI, (-1, 1))Data = np.concatenate((Data, RSI), axis = 1)
Now, we have an array with OHLC data and a fifth column that has the RSI in it. Now, let’s add two columns for the signals such that:
- Column 6 Data[:, 5] will be for the bullish divergences and will have values of 0 or 1 (initiate buy).
- Column 7 Data[:, 6] will be for the bearish divergences and will have values of 0 or -1 (initiate short).
Let’s check the full code below and we’ll understand it one step at a time. Remember that the fifth column Data[:, 4] refers to the RSI values calculated above. The variables we will be using are:
lower_barrier = 30
upper_barrier = 70
width = 10
1 # Bullish Divergence
2 for i in range(len(Data)):
5 if Data[i, 4] < lower_barrier:
7 for a in range(i + 1, i + width):
8 if Data[a, 4] > lower_barrier:
10 for r in range(a + 1, a + width):
11 if Data[r, 4] < lower_barrier and \
12 Data[r, 4] > Data[i, 4] and Data[r, 3] < 13 Data[i, 3]:
15 for s in range(r + 1, r + width):
16 if Data[s, 4] > lower_barrier:
17 Data[s + 1, 5] = 1
29 except IndexError:
32 # Bearish Divergence
33 for i in range(len(Data)):
36 if Data[i, 4] > upper_barrier:
38 for a in range(i + 1, i + width):
39 if Data[a, 4] < upper_barrier:
40 for r in range(a + 1, a + width):
41 if Data[r, 4] > upper_barrier and \
42 Data[r, 4] < Data[i, 4] and Data[r, 3] > 43 Data[i, 3]:
44 for s in range(r + 1, r + width):
45 if Data[s, 4] < upper_barrier:
46 Data[s + 1, 6] = -1
56 except IndexError:
- Line 2–6: Scouting for when the RSI goes under the 30 level. This is the first step of any divergence.
- Line 7–9: Starting from where we have an RSI under the 30 level, we will scout for when it resurfaces above it. This is the first trough of any divergence and it is the second step.
- Line 10–14: Starting from the last step, we will scout for whenever the RSI dips again under the 30 while not going lower than the first dip. Simultaneously, the prices should be lower now than they were around the first dip. This is the most important step in the divergence analysis.
- Line 15–19: Starting from the last step, we will scout for whenever the RSI resurfaces and completes the divergence pattern. Upon closing above the 30 level, we will initiate a buy order which is why we have written s + 1. We have to await the closing above the 30 level. Below is a visualization of the steps:
The width variable refers to the window which we’ll calculate the divergence on. For example, on the chart you may see a big divergence spanning over multiple bars (big width) and you can also see small divergence that happen fast over a few bars (small width). Enlarging the width will make the algorithm take into account those big ones. It is up to the coder to tweak it.
Back-testing and presenting the results.
The performance metrics we will be using are:
- Net profit on a starting balance of $1000.
- Hit ratio using a 24-hour holding period.
- Realized risk-reward ratio on a strategy that uses no risk management.
- Expectancy of the trades in USD amount.
The full table below summarized these metrics for each back-test:
In conclusion, it seems that with these parameters and no risk management, divergence trading is not an option. Tweaking, adding stop orders, and fixing the divergence parameters will certainly yield better results. It helps to know what happens if we enlarge the width and narrow the barriers. The conditions of the new below back-test will be:
lower_barrier = 35
upper_barrier = 65
width = 120
holding_period = 30
The slope of the equity curves seems better in the second back-test proving that even with small tweaks, the results can change.