Catching Market Reversals in Python.
How to Create a Simple Contrarian Trading Strategy in Python.
Combining indicators is the first key to finding good strategies. In this article an indicator based on moving averages will be combined with the well-known Relative Strength Index to find interesting trading signals.
I have just released a new book after the success of the previous book. It features advanced trend following indicators and strategies with a GitHub page dedicated to the continuously updated code. Also, this book features the original colors after having optimized for printing costs. If you feel that this interests you, feel free to visit the below Amazon link, or if you prefer to buy the PDF version, you could contact me on LinkedIn.
The Moving Average Distance Index
This is a plain simple indicator where we analyze the distance of the market’s price relative to its moving average. Here are the steps to do so:
Calculate a 20-period moving average on the market’s price.
Subtract each closing price from the 20-period moving average.
Calculate a 60-period standard deviation of the result from step 2.
Calculate a 60-period moving average of the result from step 2.
Calculate the bounds using the Bollinger Band’s intuition.
The above chart shows hourly values on the USDCAD with the Moving Average Distance Index — MADI in the second panel, we have chosen a 20-lookback period of the moving average with a 60-period volatility bands and a 3 standard-deviation factor.
# 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 BollingerBands(Data, boll_lookback, standard_distance, what, where):
# Adding a few columns
Data = adder(Data, 2)
# Calculating means
Data = ma(Data, boll_lookback, what, where)Data = volatility(Data, boll_lookback, what, where + 1)
Data[:, where + 2] = Data[:, where] + (standard_distance * Data[:, where + 1])
Data[:, where + 3] = Data[:, where] - (standard_distance * Data[:, where + 1])
Data = jump(Data, boll_lookback)
Data = deleter(Data, where, 2)
return Data
def volatility(Data, lookback, what, where):
# Adding an extra column
Data = adder(Data, 1)
for i in range(len(Data)):
try:
Data[i, where] = (Data[i - lookback + 1:i + 1, what].std())
except IndexError:
pass
# Cleaning
Data = jump(Data, lookback)
return Data
def moving_average_distance_index(Data, close, lookback_ma, lookback_volatility_bands, std, where):
# Adding a few columns
Data = adder(Data, 1)
# Calculating the moving average
Data = ma(Data, lookback_ma, close, where)
# Calculating the distance between the closing price and its moving average
Data[:, where + 1] = Data[:, close] - Data[:, where]
# Calculating the volatility bands of the distance to find boundaries
Data = BollingerBands(Data, lookback_volatility_bands, std, where + 1, where + 2)
# Cleaning
Data = deleter(Data, where, 1)
return Data
What the function above basically do is take in an OHLC data array, calculate a moving average based on a lookback period we choose (Represented by the variable lookback_ma), then a moving average and standard deviation will be calculated on the difference between the closing price and the first moving average calculated.
This is basically a Bollinger Bands applied on the difference between the market’s price and its moving average.
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 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
If you are also interested by more technical indicators and strategies, then my book might interest you:
Creating the Strategy
We can combine the two indicators to create trading rules. The below chart is a visualization of the two indicators calculated on the hourly values of the USDCHF.
def indicator_plot_triple(Data,
opening,
high,
low,
close,
indicator_one,
indicator_two,
upper_vol_band,
lower_vol_band,
window = 250):
fig, ax = plt.subplots(3, figsize = (10, 5)) Chosen = Data[-window:, ]
for i in range(len(Chosen)):
ax[0].vlines(x = i, ymin = Chosen[i, low], ymax = Chosen[i, high], color = 'black', linewidth = 1)
if Chosen[i, close] > Chosen[i, opening]:
color_chosen = 'green'
ax[0].vlines(x = i, ymin = Chosen[i, opening], ymax = Chosen[i, close], color = color_chosen, linewidth = 2)
if Chosen[i, close] < Chosen[i, opening]:
color_chosen = 'red'
ax[0].vlines(x = i, ymin = Chosen[i, close], ymax = Chosen[i, opening], color = color_chosen, linewidth = 2)
if Chosen[i, close] == Chosen[i, opening]:
color_chosen = 'black'
ax[0].vlines(x = i, ymin = Chosen[i, close], ymax = Chosen[i, opening], color = color_chosen, linewidth = 2)
ax[0].grid()
ax[1].plot(Chosen[:, indicator_one], color = 'royalblue', linewidth = 1)
ax[1].axhline(y = upper_barrier, color = 'black', linestyle = 'dashed')
ax[1].axhline(y = lower_barrier, color = 'black', linestyle = 'dashed')
ax[1].grid() ax[2].plot(Chosen[:, indicator_two], color = 'green', linewidth = 1)
ax[2].plot(Chosen[:, upper_vol_band], color = 'red', linewidth = 1)
ax[2].plot(Chosen[:, lower_vol_band], color = 'red', linewidth = 1)
ax[2].grid()
We can create the following trading rules which without a doubt need optimization and tweaks to find the right balance:
A buy (Long) signal is generated whenever the 8-period RSI is at or below 20 while the MADI is below the lower volatility band.
A sell (Short) signal is generated whenever the 8-period RSI is at or above 80 while the MADI is above the upper volatility band.
# Indicator Parameters
lookback = 8
lookback_ma = 20
lookback_volatility_bands = 60
std = 3upper_barrier = 80
lower_barrier = 20
def signal(Data, indicator_one, indicator_two, upper_barrier, lower_barrier, upper_vol, lower_vol, buy, sell):
Data = adder(Data, 10)
for i in range(len(Data)):
if Data[i, indicator_one] <= lower_barrier and Data[i, indicator_two] <= Data[i, lower_vol]:
Data[i, buy] = 1
elif Data[i, indicator_one] >= upper_barrier and Data[i, indicator_two] >= Data[i, upper_vol]:
Data[i, sell] = -1
return Data
my_data = rsi(my_data, lookback, 3, 4)
my_data = moving_average_distance_index(my_data, 3, lookback_ma, lookback_volatility_bands, std, 5)
my_data = signal(my_data, 4, 5, upper_barrier, lower_barrier, 6, 7, 8, 9)
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.
One Last Word
I have recently started an NFT collection that aims to support different humanitarian and medical causes. The Society of Light is a set of limited collectibles which will help make the world slightly better as each sale will see a percentage of it sent directly to the charity attributed to the avatar. As I always say, nothing better than a bullet list to outline the benefits of buying these NFT’s:
High-potential gain: By concentrating the remaining sales proceedings on marketing and promoting The Society of Light, I am aiming to maximize their value as much as possible in the secondary market. Remember that trading in the secondary market also means that a portion of royalties will be donated to the same charity.
Art collection and portfolio diversification: Having a collection of avatars that symbolize good deeds is truly satisfying. Investing does not need to only have selfish needs even though there is nothing wrong with investing to make money. But what about investing to make money, help others, and collect art?
Donating to your preferred cause(s): This is a flexible way of allocating different funds to your charities.
A free copy of my book in PDF: Any buyer of any NFT will receive a free copy of my latest book shown in the link of the article.