Home Trading Strategy Backtest Stochastic Oscillator Trading Strategy Backtest in Python

Stochastic Oscillator Trading Strategy Backtest in Python

by s666

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 populate 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 buy 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!

You may also like

18 comments

publiclunchresearch October 10, 2017 - 8:45 pm

Thanks very much man, these are all helpful, if purely for the coding lessons.

Reply
jerrickng October 15, 2017 - 4:36 am

Actually this is alpha, basically the logic can be inverted to buy when the oscillator did not give a signal

Reply
ibhar a rojas sanchez November 25, 2017 - 3:16 pm

hello, on your code what is lt, amp and gt
thank you

Reply
S666 November 25, 2017 - 9:01 pm

The code was unfortunately formatted incorrectly… “lt” means “greater than” and can be replaced by “” and amp means “ampersand” and can be replaced by “&”

Reply
S666 November 25, 2017 - 9:03 pm

Please ignore the previous reply, I’m on my phone and it is not formatting correctly. I shall reply from my laptop when I am home from travelling early next week. Apologies for that

Reply
s666 March 26, 2019 - 4:26 pm

This should be fixed now FYI

Reply
10basetom June 21, 2018 - 12:55 am

” 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.” — Thanks for putting this together. However, I would warn everyone to stay as clear away from this as possible — in other words, don’t even begin to look for a stock where this would work. Even if you can somehow find one that gives you killer returns, remember that it’s still back data — future returns could end up literally killing you instead.

Reply
Leo March 26, 2019 - 11:12 am

Thank you very much for this invaluable material. I am wondering if you could comment on the need for calling the dataframe.shift() methods. I understand that it shifts the values ‘down’ by 1, but why is this needed?

Reply
s666 March 26, 2019 - 4:29 pm

Hi Leo – there two main reasons we use the shift method – 1) At the end of the script to apply the position we had at the end of yesterday to the market returns we see today….so that we are correctly assigning the right day’s returns to the right held position.

Secondly – it is used in the creation of the buy and sell signals as we want sometimes to compare yesterdays price to todays price so we have to use shift to bring it forward for comparison in a vectorised manner.

Is that clear? If not let me know and I will try to elaborate.

Reply
Sean Fallon April 29, 2019 - 5:58 pm

Am getting a error at Buy Entry

htable.PyObjectHashTable.get_item
KeyError: ‘Buy Entry’

Am I missing something in the code??

Reply
s666 April 29, 2019 - 6:07 pm

Hi Sean – you are not missing something rather it is me who missed somthing!! There should be another line which seems to have disappeared!!

After the [‘Sell Entry’] line add this following line below and see if that works:

df['Buy Entry'] = ((df['%K'] > df['%D']) & (df['%K'].shift(1) < df['%D'].shift(1))) & (df['%D'] < 20)
Reply
s666 April 29, 2019 - 6:10 pm

Actually there seem to be a couple of formatting issues which will cause errors – please give me 5 mins and I shall clean it up – will reply once fixed!!

Reply
s666 April 29, 2019 - 6:21 pm

Hi Sean – should hopefully be sorted now!! Please just comment again if you are still facing issues. Cheers 😉

Reply
Adam July 28, 2019 - 7:49 pm

Amazing tutorial, thank you so much. I am going through it and I’ve noticed for some odd reason, that the current days stochastic readings are off by like 0.02 points or so on any random ticker. I checked it on symbol, “TLRY” and “SHOP”, in ‘SHOP’ there are like 2 days in the last 14 days where the stochastic doesn’t exactly match, it’ll be off by like 0.02 points or so… I am curious as to why this might be?

p.s, the price data i am getting is from (IEX) and is 100% identical to the charting data i am using on (TC2000)

Reply
s666 July 28, 2019 - 7:58 pm

Could be this: “Yahoo rounds the adjusted price to 2 decimals even though dividend amounts often have 3 decimal places. Since they apply the adjustment formula to adjusted prices, if you go far enough back in time, the value they give for Adjusted Price will be different than it would be if there were no rounding.”

https://quant.stackexchange.com/questions/942/any-known-bugs-with-yahoo-finance-adjusted-close-data

Strange that its always 0.02 though…

Reply
Adam July 28, 2019 - 8:12 pm

Sorry it is not always off by 0.02, values vary randomly…

https://ibb.co/3myqKpQ in this image from index 13-19 all values are exact but for index 20 value 17.48 is off compared to the value of 17.33 in TC2000. Although, the stochastic 14,1,1 has a value of 17.34 for TLRY on tradingview.com

I am getting my financial data from https://iexcloud.io/
I’ve went over the ‘open’,’low’,’high’,’close’ values for 1-2 months from IEX data and compared it to the ‘open’,’low’,’high’,’close’ values of TC2000 and i’ve noticed one situation where, the ‘low’ price was off by 0.01 cents on one day only. So, assuming that the data is 99.99% accurate, espically since the stochastic values from all the other days match 100% it shouldn’t be off by this much. For some odd reason, I have a feeling that tomorrow when a new days data is available, when I run my code, it will probably show the correct value of today, but it wont show it today until tomorrow and tomorrows data will be off a little until the next days data comes about..

The reason I am guessing that might be an issue is because something similar was happening to me when I was trying to make the stochastic code myself on my own before landing on your page. doubt it but is it possible that the formula isn’t including all the data points of the current days price data in its 14 day calculation?

Reply
Adam July 28, 2019 - 8:19 pm

Excuse my long reply, awaiting approval, hopefully admins delete it because I realized what was wrong. I got it sorted it, thank you.

p.s it seems that the big names, aapl, googl, tsla… all the stochastic readings are 100% accurate, so it must be something to do with companies that are not that closely followed….i’m not sure but it’s clear its not the code. THANK YOU AGAIN

Reply
s666 July 28, 2019 - 8:46 pm

Not a problem, glad you got the issue sorted, to a degree 😉

Reply

Leave a Reply

%d bloggers like this: