Stochastic Oscillator Trading Strategy Backtest in Python

I thought for this post I would just continue on with the theme of testing trading strategies based on signals from some of the classic “technical indicators” that many traders incorporate into their decision making; the last post dealt with Bollinger Bands and for this one I thought I’d go for a Stochastic Oscillator Trading Strategy Backtest in Python.

Let’s start with what the Stochastic Oscillator actually is; Investopedia describes it as follows:

“What is the ‘Stochastic Oscillator’
The stochastic oscillator is a momentum indicator comparing the closing price of a security to the range of its prices over a certain period of time. The sensitivity of the oscillator to market movements is reducible by adjusting that time period or by taking a moving average of the result.

BREAKING DOWN ‘Stochastic Oscillator’
The stochastic oscillator is calculated using the following formula:

%K = 100(C – L14)/(H14 – L14)

Where:

C = the most recent closing price

L14 = the low of the 14 previous trading sessions

H14 = the highest price traded during the same 14-day period

%K= the current market rate for the currency pair

%D = 3-period moving average of %K

The general theory serving as the foundation for this indicator is that in a market trending upward, prices will close near the high, and in a market trending downward, prices close near the low. Transaction signals are created when the %K crosses through a three-period moving average, which is called the %D.”

I want to test two different implementations of the Stochastic Oscillator:

1) A sell entry signal is given when the %K line crosses down through the %D line, and the %K line is above 80. The exit signal for this short position is given as soon as the %K line crosses back up through the %D line, irrespective of the actual value of the %K line when this happens. A buy entry signal is given when the %K line passes up through the %D line, and the %K line is under 20 at that time. The exit signal for this long position is given as soon as the %K line crosses back down through the %D line, irrespective of the actual value of the %K line when this happens. In this implementation there are 3 possible states – long, short, flat (i.e. no position).

2) In this implementation there are only 2 possible states – long or short. Once a position is entered into, the position is held until an opposite signal is given, at which point the position is reversed (i.e from long to short or from short to long). For example, if a buy entry position is signalled by the %K line crossing up through the %D line whilst the %K line is below 20, the position is held until the %K line crosses down through the %D line whilst the %K line is above 80.

So lets get to some code and try out the strategy on Apple Inc. stock.

#import relevant modules
import pandas as pd
import numpy as np
from pandas_datareader import data
import matplotlib.pyplot as plt
 
#download data into DataFrame and create moving averages columns
df = data.DataReader('AAPL', 'yahoo',start='1/1/2000')
 
#print out first 5 rows of data DataFrame to check in correct format
df.head()

#Create the "L14" column in the DataFrame
df['L14'] = df['Low'].rolling(window=14).min()
 
#Create the "H14" column in the DataFrame
df['H14'] = df['High'].rolling(window=14).max()
 
#Create the "%K" column in the DataFrame
df['%K'] = 100*((df['Close'] - df['L14']) / (df['H14'] - df['L14']) )
 
#Create the "%D" column in the DataFrame
df['%D'] = df['%K'].rolling(window=3).mean()

Now let’s create a plot (with 2 subplots) showing the Apple price over time, along with a visual representation of the Stochastic Oscillator.

fig, axes = plt.subplots(nrows=2, ncols=1,figsize=(20,10))
 
df['Close'].plot(ax=axes[0]); axes[0].set_title('Close')
df[['%K','%D']].plot(ax=axes[1]); axes[1].set_title('Oscillator')

#Create a column in the DataFrame showing "TRUE" if sell entry signal is given and "FALSE" otherwise.
#A sell is initiated when the %K line crosses down through the %D line and the value of the oscillator is above 80
df['Sell Entry'] = ((df['%K'] < df['%D']) & (df['%K'].shift(1) > df['%D'].shift(1))) & (df['%D'] > 80)
 
#Create a column in the DataFrame showing "TRUE" if sell exit signal is given and "FALSE" otherwise.
#A sell exit signal is given when the %K line crosses back up through the %D line
df['Sell Exit'] = ((df['%K'] > df['%D']) & (df['%K'].shift(1) < df['%D'].shift(1))) #create a placeholder column to polulate with short positions (-1 for short and 0 for flat) using boolean values created above df['Short'] = np.nan df.loc[df['Sell Entry'],'Short'] = -1 df.loc[df['Sell Exit'],'Short'] = 0 #Set initial position on day 1 to flat df['Short'][0] = 0 #Forward fill the position column to represent the holding of positions through time df['Short'] = df['Short'].fillna(method='pad') #Create a column in the DataFrame showing "TRUE" if buy entry signal is given and "FALSE" otherwise. #A sbuy is initiated when the %K line crosses up through the %D line and the value of the oscillator is below 20 df['Buy Entry'] = ((df['%K'] > df['%D']) & (df['%K'].shift(1) < df['%D'].shift(1))) & (df['%D'] < 20)
 
#Create a column in the DataFrame showing "TRUE" if buy exit signal is given and "FALSE" otherwise.
#A buy exit signal is given when the %K line crosses back down through the %D line
df['Buy Exit'] = ((df['%K'] < df['%D']) & (df['%K'].shift(1) > df['%D'].shift(1)))
 
#create a placeholder column to polulate with long positions (1 for long and 0 for flat) using boolean values created above
df['Long'] = np.nan 
df.loc[df['Buy Entry'],'Long'] = 1 
df.loc[df['Buy Exit'],'Long'] = 0 
 
#Set initial position on day 1 to flat
df['Long'][0] = 0 
 
#Forward fill the position column to represent the holding of positions through time
df['Long'] = df['Long'].fillna(method='pad')
 
#Add Long and Short positions together to get final strategy position (1 for long, -1 for short and 0 for flat)
df['Position'] = df['Long'] + df['Short']

Let’s plot the position through time to get an idea of when we are long and when we are short:

df['Position'].plot(figsize=(20,10))

#Set up a column holding the daily Apple returns
df['Market Returns'] = df['Close'].pct_change()
 
#Create column for Strategy Returns by multiplying the daily Apple returns by the position that was held at close
#of business the previous day
df['Strategy Returns'] = df['Market Returns'] * df['Position'].shift(1)
 
#Finally plot the strategy returns versus Apple returns
df[['Strategy Returns','Market Returns']].cumsum().plot()

So we see that our returns are indeed positive at least, but we could have done much better by just buying and holding Apple stock, which is slightly disappointing.

So, on to our second implementation of the strategy – the one where we are either long or short. I will just paste the whole code in one go and present the equity curve at the end:

df = data.DataReader('AAPL', 'yahoo',start='1/1/2010')
 
df['L14'] = df['Low'].rolling(window=14).min()
df['H14'] = df['High'].rolling(window=14).max()
 
df['%K'] = 100*((df['Close'] - df['L14']) / (df['H14'] - df['L14']) )
df['%D'] = df['%K'].rolling(window=3).mean()
 
df['Sell Entry'] = ((df['%K'] < df['%D']) & (df['%K'].shift(1) > df['%D'].shift(1))) & (df['%D'] > 80)
df['Buy Entry'] = ((df['%K'] > df['%D']) & (df['%K'].shift(1) < df['%D'].shift(1))) & (df['%D'] < 20)
 
#Create empty "Position" column
df['Position'] = np.nan 
 
#Set position to -1 for sell signals
df.loc[df['Sell Entry'],'Position'] = -1 
 
#Set position to -1 for buy signals
df.loc[df['Buy Entry'],'Position'] = 1 
 
#Set starting position to flat (i.e. 0)
df['Position'].iloc[0] = 0 
 
#Forward fill the position column to show holding of positions through time
df['Position'] = df['Position'].fillna(method='ffill')
 
#Set up a column holding the daily Apple returns
df['Market Returns'] = df['Close'].pct_change()
 
#Create column for Strategy Returns by multiplying the daily Apple returns by the position that was held at close
#of business the previous day
df['Strategy Returns'] = df['Market Returns'] * df['Position'].shift(1)
 
#Finally plot the strategy returns versus Apple returns
df[['Strategy Returns','Market Returns']].cumsum().plot(figsize=(20,10))

So unfortunately this implementation give us a worse outcome, with the overall return being pretty strongly negative.

So, once again we have shown that using a simple technical indicator such as the Stochastic Oscillator isn’t enough to generate superior returns (shock horror!), at least for Apple over the back-tested period. I would imagine that stocks for which this strategy worked in a robust enough fashion to actually rely on would be few and far between. Doesn’t hurt to look though…

Until next time!

It's only fair to share...Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePin on PinterestShare on Reddit
Written by s666