Without a doubt the Stochastic Oscillator is a universally used indicator. It uses such a simple yet powerful formula in order to detect rapid oversold and overbought conditions. This article discusses a certain strategy based on this indicator.
My newest book with O’Reilly Media on candlestick pattern recognition is now available on Amazon! The book features a huge number of classic and modern candlestick patterns as it dwelves into the realm of technical analysis with different trading strategies. The book comes with its own GitHub and is dynamic in nature as it is continuously updated and questions are answered on the O’Reilly platform.
Mastering Financial Pattern Recognition
Amazon.com: Mastering Financial Pattern Recognition eBook : Kaabar, Sofien: Kindle Storewww.amazon.com
The Stochastic Oscillator
This great technique allows us to trap the values between 0 and 1 (or 0 and 100 if we wish to multiply by 100). The concept revolves around subtracting the minimum value in a certain lookback period from the current value and dividing by the maximum value in the same lookback period minus the minimum value (the same in the nominator).
To be able to manipulate the data, we first need to have an OHLC array (not a data frame) and define the following three small manipulation functions:
# The function to add a certain number of columns
def adder(Data, times):
for i in range(1, times + 1):
z = np.zeros((len(Data), 1), dtype = float)
Data = np.append(Data, z, axis = 1)
return Data
# The function to deleter a certain number of columns
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 certain number of rows from the beginning
def jump(Data, jump):
Data = Data[jump:, ]
return Data
The Stochastic Oscillator seeks to find oversold and overbought zones by incorporating the highs and lows using the normalization formula as shown below:
An overbought level is an area where the market is perceived to be extremely bullish and is bound to consolidate. An oversold level is an area where market is perceived to be extremely bearish and is bound to bounce. Hence, the Stochastic Oscillator is a contrarian indicator that seeks to signal reactions of extreme movements.
We will create the below function that calculates the Stochastic on an OHLC data:
def stochastic(Data, lookback, high, low, close, where, genre = 'High-Low'):
# Adding a column
Data = adder(Data, 1)
if genre == 'High-Low':
for i in range(len(Data)):
try:
Data[i, where] = (Data[i, close] - min(Data[i - lookback + 1:i + 1, low])) / (max(Data[i - lookback + 1:i + 1, high]) - min(Data[i - lookback + 1:i + 1, low]))
except ValueError:
pass
if genre == 'Normalization':
for i in range(len(Data)):
try:
Data[i, where] = (Data[i, close] - min(Data[i - lookback + 1:i + 1, close])) / (max(Data[i - lookback + 1:i + 1, close]) - min(Data[i - lookback + 1:i + 1, close]))
except ValueError:
pass
Data[:, where] = Data[:, where] * 100
Data = jump(Data, lookback)
return Data
The above plot shows the EURUSD values plotted with a 21-period Stochastic Oscillator. Notice that the indicator will always be bounded between 0 and 100 due to the nature of its normalization function that traps values between the minimum and the maximum.
Creating & Evaluating the Strategy
The strategy is pretty clear, we will initiate position based on the exit of extremes. Therefore, the trading conditions are:
A buy (Long) signal is generated whenever the current 21-period Stochastic Oscillator is above 5 while the previous reading is below 5.
A sell (Short) signal is generated whenever the current 21-period Stochastic Oscillator is below 95 while the previous reading is above 95.
The below Python function shows the code for the signal generation process.
# Indicator Parameters
lookback = 21
upper_barrier = 95
lower_barrier = 5
my_data = adder(my_data, 10)
my_data = stochastic(my_data, lookback, 1, 2, 3, 4)
def signal(Data, indicator, buy, sell):
Data = adder(Data, 10)
for i in range(len(Data)):
if Data[i, indicator] > lower_barrier and Data[i - 1, indicator] < lower_barrier:
Data[i, buy] = 1
elif Data[i, indicator] < upper_barrier and Data[i - 1, indicator] > upper_barrier:
Data[i, sell] = -1
return Data
my_data = signal(my_data, 4, 6, 7)
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 = 13
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 = 53.05%
A signal quality of 53.05% means that on 100 trades, we tend to see a profitable result in 53 of the cases without taking into account transaction costs.
Summary
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.