Coding One of the Best Technical Indicators
How to Code this Complex Indicator to Understand & Trade Squeezes
The squeeze momentum indicator is a volatility-momentum technical oscillator created by John Carter to measure and trade breakouts. It uses multiple indicators fused together to deliver the buy or sell signals. In this article, we will create the indicator from scratch and create the signals in Python. but first, we discuss the main ingredients.
I have released a new book after the success of my previous one “Trend Following Strategies in Python”. It features advanced contrarian indicators and strategies with a GitHub page dedicated to the continuously updated code. If you feel that this interests you, feel free to visit the below Amazon link (which contains a sample), or if you prefer to buy the PDF version, you could check the link at the end of the article.
Contrarian Trading Strategies in Python
Amazon.com: Contrarian Trading Strategies in Python: 9798434008075: Kaabar, Sofien: Bookswww.amazon.com
The Concept of Bollinger Bands
One of the pillars of descriptive statistics or any basic analysis method is the concept of averages. Averages give us a glance of the next expected value given a historical trend. They can also be a representative number of a larger dataset that helps us understand the data quickly. Another pillar is the concept of volatility. Volatility is the average deviation of the values from their mean. Let us create the simple hypothetical table with different random variables.
Let us suppose that the above is a timely ordered time series. If we had to naively guess the next value that comes after 10, then one of the best guesses can be the average the data. Therefore, if we sum up the values {5, 10, 15, 5, 10} and divide them by their quantity (i.e. 5), we get 9. This is the average of the above dataset. In python, we can generate the list or array and then calculate the average easily:
# Importing the required library
import numpy as np
# Creating the array
array = [5, 10, 15, 5, 10]
array = np.array(array)
# Calculating the mean
array.mean()
Now that we have calculated the mean value, we can see that no value in the dataset really equals 9. How do we know that the dataset is generally not close to the dataset? This is measured by what we call the Standard Deviation (Volatility).
The Standard Deviation simply measures the average distance away from the mean by looping through the individual values and comparing their distance to the mean.
# Calculating the mean
array.std()
The above code snippet calculates the Standard Deviation of the dataset which is around 3.74. This means that on average, the individual values are 3.74 units away from 9 (the mean). Now, let us move on to the normal distribution shown in the below curve.
The above curve shows the number of values within a number of standard deviations. For example, the area shaded in red represents around 1.33x of standard deviations away from the mean of zero. We know that if data is normally distributed then:
About 68% of the data falls within 1 standard deviation of the mean.
About 95% of the data falls within 2 standard deviations of the mean.
About 99% of the data falls within 3 standard deviations of the mean.
Presumably, this can be used to approximate the way to use financial returns data, but studies show that financial data is not normally distributed but at the moment we can assume it is so that we can use such indicators. The flawness of the method does not hinder much its usefulness.
Now, with the information below, we are ready to start creating the Bollinger Bands indicator:
Financial time series data can have a moving average that calculates a rolling mean window. For example a 20-period moving average calculates each time a 20-period mean that refreshes each time a new bar is formed.
On this rolling mean window, we can calculate the Standard Deviation of the same lookback period on the moving average.
What are the Bollinger Bands? When prices move, we can calculate a moving average (mean) around them so that we better understand their position regarding their mean. By doing this, we can also calculate where do they stand statistically.
Some say that the concept of volatility is the most important one in the financial markets industry. Trading the volatility bands is using some statistical properties to aid you in the decision making process, hence, you know you are in good hands.
The idea of the Bollinger Bands is to form two barriers calculated from a constant multiplied by the rolling Standard Deviation. They are in essence barriers that give out a probability that the market price should be contained within them. The lower Bollinger Band can be considered as a dynamic support while the upper Bollinger Band can be considered as a dynamic resistance. Hence, the Bollinger bands are simple a combination of a moving average that follows prices and a moving standard deviation(s) band that moves alongside the price and the moving average.
To calculate the two Bands, we use the following relatively simple formulas:
With the constant being the number of standard deviations that we choose to envelop prices with. By default, the indicator calculates a 20-period simple moving average and two standard deviations away from the price, then plots them together to get a better understanding of any statistical extremes.
This means that on any time, we can calculate the mean and standard deviations of the last 20 observations we have and then multiply the standard deviation by the constant. Finally, we can add and subtract it from the mean to find the upper and lower band.
Clearly, the below chart seems easy to understand. Every time the price reaches one of the bands, a contrarian position is most suited and this is evidenced by the reactions we tend to see when prices hit these extremes. So, whenever the EURUSD reaches the upper band, we can say that statistically, it should consolidate and when it reaches the lower band, we can say that statistically, it should bounce.
To create the Bollinger Bands in Python, we need to define the moving average function, the standard deviation function, and then, the Bollinger Bands function which will use the former two functions.
Now, we have to add some columns to the OHLC array, define the moving average and the Bollinger functions, then finally, use them.
# Adding a few columns
Data = adder(Data, 20)def ma(Data, lookback, what, where):
for i in range(len(Data)):
try:
Data[i, where] = (Data[i - lookback + 1:i + 1, what].mean())
except IndexError:
pass
return Data
def volatility(Data, lookback, what, where):
for i in range(len(Data)):
try:
Data[i, where] = (Data[i - lookback + 1:i + 1, what].std())
except IndexError:
pass
return Data
def BollingerBands(Data, boll_lookback, standard_distance, what, where):
# Calculating mean
ma(Data, boll_lookback, what, where)
# Calculating volatility
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])
return Data
# Using the function to calculate a 20-period Bollinger Band with 2 Standard Deviations
Data = BollingerBands(Data, 20, 2, 3, 4)
The Concept of the Keltner Channel
The Keltner Channel is a volatility-based technical indicator that resembles the Bollinger Bands, only it uses an exponential moving average as the mean calculation and the Average True Range as a volatility proxy. Hence, here is the main two differences between the two:
The Bollinger Bands: A simple moving average with bands based on historical Standard Deviation.
The Keltner Channel: An exponential moving average with bands based on the Average True Range.
But what is the Average True Range? Although it is considered as a lagging indicator, it gives some insights as to where volatility is now and where has it been last period (day, week, month, etc.).
First, we should understand how the True Range is calculated (the ATR is just the average of that calculation). Consider an OHLC data composed of an timely arrange Open, High, Low, and Close prices. For each time period (bar), the true range is simply the greatest of the three price differences:
High — Low
High — Previous close
Previous close — Low
Once we have got the maximum out of the above three, we simply take a smoothed average of n periods of the true ranges to get the Average True Range.
def atr(Data, lookback, high, low, close, where, genre = 'Smoothed'):
# Adding the required columns
Data = adder(Data, 1)
# True Range Calculation
for i in range(len(Data)):
try:
Data[i, where] = max(Data[i, high] - Data[i, low],
abs(Data[i, high] - Data[i - 1, close]),
abs(Data[i, low] - Data[i - 1, close]))
except ValueError:
pass
Data[0, where] = 0
if genre == 'Smoothed':
# Average True Range Calculation
Data = ema(Data, 2, lookback, where, where + 1)
if genre == 'Simple':
# Average True Range Calculation
Data = ma(Data, lookback, where, where + 1)
# Cleaning
Data = deleter(Data, where, 1)
Data = jump(Data, lookback)
return Data
Now, we calculate the Keltner Channel using an exponential moving average with the ATR of the price. Here is the formula:
With the Python code to output the Keltner Channel:
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 keltner_channel(Data, ma_lookback, atr_lookback, multiplier, what, where):
Data = ema(Data, 2, ma_lookback, what, where)
Data = atr(Data, atr_lookback, 2, 1, 3, where + 1)
Data[:, where + 2] = Data[:, where] + (Data[:, where + 1] * multiplier)
Data[:, where + 3] = Data[:, where] - (Data[:, where + 1] * multiplier)
return Data
If you are also interested by more technical indicators and strategies, then my book might interest you:
The Book of Trading Strategies
Amazon.com: The Book of Trading Strategies: 9798532885707: Kaabar, Sofien: Bookswww.amazon.com
The Squeeze Momentum Indicator
After discussing the above three indicators, we are now set to create the Squeeze Indicator. Simply put, the main goal of the indicator is to initiate a position when the squeeze becomes OFF after being ON for a specified time. What do we mean by ON and OFF? Basically:
Whenever the upper Bollinger Band is lower than the upper Keltner Channel while the lower Bollinger Band is greater than the lower Keltner Channel, the squeeze is ON.
Whenever any of the above conditions are not true, the squeeze is OFF.
Therefore, we understand that the Squeeze Indicator will be a combination of the following:
A 20-period Bollinger Bands with 2.00 standard deviation.
A 20-period Keltner Channel with 1.50 ATR multiplier.
The steps we will follow the create the indicator are:
Step 1: Calculate the Bollinger Bands on the market price.
Step 2: Calculate the Keltner Channel on the market price.
Step 3: Calculate the highest high in the last 20 periods.
Step 4: Calculate the lowest low in the last 20 periods.
Step 5: Find the mean between the two above results.
Step 6: Calculate a 20-period simple moving average on the closing price
Step 7: Calculate the delta between the closing price and the mean between the result from step 5 and 6.
Alternatively, we will calculate a 20-period simple moving average on the result from step 7 as opposed to the original technique where a linear regression line is drawn. This is to simplify things as the indicator is already complex as it is.
In Python, the full syntax of the above can be as follows.
def squeeze(Data, boll_lookback, boll_vol, kelt_lookback, kelt_vol, close, where):
# Adding Columns
Data = adder(Data, 20)
# Adding Bollinger Bands
Data = BollingerBands(Data, boll_lookback, boll_vol, close, where)
# Adding Keltner Channel
Data = keltner_channel(Data, kelt_lookback, kelt_lookback, kelt_vol, close, where + 2)
# Donchian Middle Point
for i in range(len(Data)):
try:
Data[i, where + 4] = max(Data[i - boll_lookback + 1:i + 1, 1])
except ValueError:
pass for i in range(len(Data)):
try:
Data[i, where + 5] = min(Data[i - boll_lookback + 1:i + 1, 2])
except ValueError:
pass Data[:, where + 6] = (Data[:, where + 4] + Data[:, where + 5]) / 2
Data = deleter(Data, where + 4, 2)
# Calculating Simple Moving Average on the Market
Data = ma(Data, boll_lookback, close, where + 5)
# Calculating Delta
for i in range(len(Data)):
Data[i, where + 6] = Data[i, close] - ((Data[i, where + 4] + Data[i, where + 5]) / 2)
# Final Smoothing
Data = ma(Data, boll_lookback, where + 6, where + 7)
# Cleaning
Data = deleter(Data, where + 4, 3)
# Squeeze Detection
for i in range(len(Data)):
if Data[i, where] < Data[i, where + 2] and Data[i, where + 1] > Data[i, where + 3]:
Data[i, where + 5] = 0.001
return Data
boll_lookback = 20
boll_vol = 2
kelt_lookback = 20
kelt_vol = 1.5
my_data = squeeze(my_data, boll_lookback, boll_vol, kelt_lookback, kelt_vol, 3, 4)
def indicator_plot_squeeze(Data, window = 250): fig, ax = plt.subplots(2, figsize = (10, 5)) Chosen = Data[-window:, ]
for i in range(len(Chosen)):
ax[0].vlines(x = i, ymin = Chosen[i, 2], ymax = Chosen[i, 1], color = 'black', linewidth = 1)
ax[0].grid()
ax[0].plot(my_data[-window:, 4], color = 'blue')
ax[0].plot(my_data[-window:, 5], color = 'blue')
ax[0].plot(my_data[-window:, 6], color = 'pink')
ax[0].plot(my_data[-window:, 7], color = 'pink')
for i in range(len(Chosen)):
if Chosen[i, 8] > 0:
ax[1].vlines(x = i, ymin = 0, ymax = Chosen[i, 8], color = 'black', linewidth = 1)
if Chosen[i, 8] < 0:
ax[1].vlines(x = i, ymin = Chosen[i, 8], ymax = 0, color = 'black', linewidth = 1) if Chosen[i, 8] == 0:
ax[1].vlines(x = i, ymin = Chosen[i, 8], ymax = 0, color = 'black', linewidth = 1)
ax[1].grid()
ax[1].axhline(y = 0, color = 'black', linewidth = 1.0, linestyle = '--')
for i in range(len(Chosen)):
if Chosen[i, 9] == 0.001:
x = i
y = Chosen[i, 8] + (-Chosen[i, 8])
ax[1].annotate(' ', xy = (x, y), arrowprops = dict(width = 5, headlength = 3, headwidth = 3, facecolor = 'red', color = 'red'))
elif Chosen[i, 9] == 0:
x = i
y = Chosen[i, 8] + (-Chosen[i, 8])
ax[1].annotate(' ', xy = (x, y), arrowprops = dict(width = 5, headlength = 3, headwidth = 3, facecolor = 'green', color = 'green'))
The above shows the OHLC values of the USDCHF with the Squeeze Indicator composed of a histogram in black that fluctuates around a zero value. We can notice a straight line in green and sometimes in red, this denotes when the squeeze is ON and when is it OFF, thus helping us visualize the squeeze. From all the above, we will have the below trading rules:
Long (Buy) whenever the squeeze line turns green (after having been red) while simultaneously the histogram in black is above zero.
Short (Sell) whenever the squeeze line turns green (after having been red) while simultaneously the histogram in black is below zero.
def signal(Data):
for i in range(len(Data)):
# Bullish Signal
if Data[i, 9] != 0.001 and Data[i - 1, 9] == 0.001 and Data[i, 8] > 0:
Data[i, 10] = 1
# Bearish Signal
elif Data[i, 9] != 0.001 and Data[i - 1, 9] == 0.001 and Data[i, 8] < 0:
Data[i, 11] = -1
The indicator can be tweaked as desired but is unlikely to produce a significantly different return from other mainstream indicators. This is to say that it can offer a diversification factor but does it provide a full profitable strategy? That is something that needs more research.
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.