Intraday Stock Mean Reversion Trading Backtest in Python With Short Selling

Intraday Stock Mean Reversion Trading Backtest in Python With Short Selling

Carrying on from the last post which outlined an intra-day mean reversion stock trading strategy, I just wanted to expand on that by adapting the backtest to allow short selling too. So as well as buying stocks that have gapped down, we will be allowing the strategy to short sell stocks that have gapped up.

I was interested as to how that would effect our returns and Sharpe Ratio; generally speaking there is more market inefficiency on the short side of the market for various reasons (including for example the inability of large pension funds to short sell stocks due to investment mandate restrictions among other things) .

This increased market inefficiency should theoretically lead to higher returns for those market participants who are able to take advantage of the short selling opportunities.

I’ll begin by backtesting across the same stock universe we used in the last post – the NSYE stock list. The list can be downloaded by clicking the link below:

NYSE Stock List

 

So let’s begin the code:

#import the relevant modules
import pandas as pd
import numpy as np
from pandas_datareader import data
import requests
from math import sqrt
import matplotlib.pyplot as plt
plt.style.use('seaborn-whitegrid')
%matplotlib inline
 
 
#read the stock tickers and names into a DataFrame
stocks = pd.read_csv('NYSE.txt',delimiter="\t")
 
stocks_list = []
 
#iterate through stock list and append tickers into our empty list
for symbol in stocks['Symbol']:
    stocks_list.append(symbol)
 
 
#create empty list to hold our return series DataFrame for each stock
frames = []
 
 
for stock in stocks_list:
 
    try:
 
        #download stock data and place in DataFrame
        df = data.DataReader(stock, 'yahoo',start='1/1/2000')
 
 
        #create column to hold our 90 day rolling standard deviation
        df['Stdev'] = df['Close'].rolling(window=90).std()
 
        #create a column to hold our 20 day moving average
        df['Moving Average'] = df['Close'].rolling(window=20).mean()
 
        #create a column which holds a TRUE value if the gap down from previous day's low to next 
        #day's open is larger than the 90 day rolling standard deviation
        df['Buy1'] = (df['Open'] - df['Low'].shift(1)) < -df['Stdev'] 
        #create a column which holds a TRUE value if the opening price of the stock is above the 20 day moving average 
        df['Buy2'] = df['Open'] > df['Moving Average']
 
        #create a column that holds a TRUE value if both buy criteria are also TRUE
        df['BUY'] = df['Buy1'] & df['Buy2']
 
        #create a column which holds a TRUE value if the gap up from previous day's high to next 
        #day's open is larger than the 90 day rolling standard deviation
        df['Sell1'] = (df['Open'] - df['High'].shift(1)) > df['Stdev'] 
 
        #create a column which holds a TRUE value if the opening price of the stock is below the 20 day moving average 
        df['Sell2'] = df['Open'] < df['Moving Average']
 
        #create a column that holds a TRUE value if both sell criteria are also TRUE
        df['SELL'] = df['Sell1'] & df['Sell2']
 
        #calculate daily % return series for stock
        df['Pct Change'] = (df['Close'] - df['Open']) / df['Open']
 
        #create a strategy return series by using the daily stock returns mutliplied by 1 if we are long and -1 if we are short
        df['Rets'] = np.where(df['BUY'],df['Pct Change'], 0)
        df['Rets'] = np.where(df['SELL'],-df['Pct Change'], df['Rets'])
 
        #append the strategy return series to our list
        frames.append(df['Rets'])
 
 
 
    except:
        pass
 
 
 
 
#concatenate the individual DataFrames held in our list- and do it along the column axis
masterFrame = pd.concat(frames,axis=1)
 
#create a column to hold the sum of all the individual daily strategy returns
masterFrame['Total'] = masterFrame.sum(axis=1)
 
#fill 'NaNs' with zeros to allow our "count" function below to work properly
masterFrame.fillna(0,inplace=True)
 
 
#create a column that hold the count of the number of stocks that were traded each day
#we minus one from it so that we dont count the "Total" column we added as a trade.
masterFrame['Count'] = (masterFrame != 0).sum(axis=1) - 1
 
#create a column that divides the "total" strategy return each day by the number of stocks traded that day to get equally weighted return.
masterFrame['Return'] = masterFrame['Total'] / masterFrame['Count']
 
#plot the strategy returns
masterFrame['Return'].dropna().cumsum().plot()

This last line produces the below chart

with a Sharpe Ratio of

#risk free element excluded for simplicity
(masterFrame['Return'].mean() *252) / (masterFrame['Return'].std() * (sqrt(252)))
1.147048581128388

and an annual return of

days = (masterFrame.index[-1] - masterFrame.index[0]).days
 
(masterFrame['Return'].dropna().cumsum()[-1]+1)**(365.0/days) - 1
0.12087198786174858

Ok so the Sharpe Ratio has decreased from just over 2, to 1.15 when we add the short selling ability, and the annual return has increased from around 8.8% to 12%.

I guess it depends on which statistic is more important to you, straight return or risk adjusted return. I’ll leave that one up to you!

Just for a bit of fun, let’s run this strategy across another investment universe of stocks, this time the London Stock Exchange (LSE) tickers.

That list can be downloaded by clicking the button below:

LSE Stock List

 

This backtest generates the following results:

With a Sharpe Ratio of 1.289 and an annual return of 25.21%…well that’s a pretty hefty return!

If you are running the backtest for yourself, remember that there are around 6000 stocks in that list so it can take a little while to complete the backtest, just be patient.

Although the results of the LSE backtest look very promising, I think if anyone tried to actually trade it for real they would quickly find out that many of the stocks the strategy short sells, wouldn’t actually be available for short sale in the live market and the transaction costs would be high enough to eat into a large proportion of the returns.

Anyway, it’s still fun to investigate these things and look for ideas to at least begin our search for a profitable strategy.

Until next time folks…

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