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:
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)))
and an annual return of
days = (masterFrame.index[-1] - masterFrame.index).days (masterFrame['Return'].dropna().cumsum()[-1]+1)**(365.0/days) - 1
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:
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…