Quant BuffetRelax, Not Over Thinking

Days to Cover Strategy

Log in to collect

Academic paper

Days to Cover and Stock Returns

AuthorsHarrison Hong; Frank Weikai Li; Sophie Xiaoyan Ni; José Scheinkman; Philip Yan

Institute
  • Columbia University
  • National Bureau of Economic Research
  • ?Columbia University, Graduate School of Arts and Sciences, Department of Economics
  • ?National Bureau of Economic Research (NBER)
  • SGSingapore Management University
  • ?Singapore Management University - Lee Kong Chian School of Business
  • HKHong Kong Baptist University
  • ?Hong Kong Baptist University (HKBU)
  • Princeton University
  • ?Princeton University - Department of Economics
  • Goldman Sachs (United States)
  • ?Quantitative Investment Strategies, Goldman Sachs Asset Management

Strategy in a nutshell

This strategy trades NYSE, AMEX, and NASDAQ stocks using days-to-cover (DTC) as a signal. Stocks are ranked monthly by DTC, with a long position in the lowest decile and short in the highest, value-weighted and rebalanced monthly to exploit potential mispricing.

Economic rationale

DTC reflects the ease of short-selling, with high DTC indicating greater trading costs and potential overvaluation. The metric captures market sentiment and arbitrageur confidence, providing a signal for identifying inefficiencies in short-interest and trading turnover.

Backtest performance

Annualised return8.34%
Volatility17.87%
Beta0.223
Sharpe ratio0.45
Win rate49%

Full Python code

import numpy as np
from AlgorithmImports import *
class ShortInterestEffect(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2015, 1, 1)
self.SetCash(100000)
# NOTE: We use only s&p 100 stocks so it's possible to fetch short interest data from quandl.
self.symbols = ['AAPL','MSFT','AMZN','FB','BRK.B','GOOGL','GOOG','JPM','JNJ','V','PG','XOM','UNH','BAC','MA','T','DIS','INTC','HD','VZ','MRK','PFE','CVX','KO','CMCSA','CSCO','PEP','WFC','C','BA','ADBE','WMT','CRM','MCD','MDT','BMY','ABT','NVDA','NFLX','AMGN','PM','PYPL','TMO','COST','ABBV','ACN','HON','NKE','UNP','UTX','NEE','IBM','TXN','AVGO','LLY','ORCL','LIN','SBUX','AMT','LMT','GE','MMM','DHR','QCOM','CVS','MO','LOW','FIS','AXP','BKNG','UPS','GILD','CHTR','CAT','MDLZ','GS','USB','CI','ANTM','BDX','TJX','ADP','TFC','CME','SPGI','COP','INTU','ISRG','CB','SO','D','FISV','PNC','DUK','SYK','ZTS','MS','RTN','AGN','BLK']
self.period = 21
self.fine = []

self.symbol = self.AddEquity('SPY', Resolution.Daily).Symbol

self.last_month = -1
self.selection_flag = False
self.AddUniverseSelection(FineFundamentalUniverseSelectionModel(self.SelectCoarse, self.SelectFine))
 
for symbol in self.symbols:
    self.AddData(QuandlFINRA_ShortVolume, 'FINRA/FNSQ_' + symbol, Resolution.Daily)

def SelectCoarse(self, coarse):
if not self.selection_flag:
    return Universe.Unchanged
    
return [Symbol.Create(x, SecurityType.Equity, Market.USA) for x in self.symbols]
def SelectFine(self, fine):
self.fine = [f.Symbol for f in fine] 
return self.fine

def OnSecuritiesChanged(self, changes):
for security in changes.AddedSecurities:
    security.SetLeverage(10)
    security.SetFeeModel(CustomFeeModel())

def OnData(self, data):
# Rebalance once a month.
if self.last_month != self.Time.month:
    self.selection_flag = True
    self.last_month = self.Time.month
    return
        
if not self.selection_flag:
    return
self.selection_flag = False

# Days to cover calc.
days_to_cover = {}
for symbol in self.fine:
    ticker = symbol.Value
    
    hist = self.History(symbol, self.period, Resolution.Daily)
    if len(hist) == self.period:
        mean_volume = np.mean(hist.loc[symbol]['volume'])
        shares_outstanding = self.Securities[symbol].Fundamentals.EarningReports.BasicAverageShares.ThreeMonths
        if mean_volume != 0 and shares_outstanding != 0:
            daily_turnover = mean_volume / shares_outstanding
        
            if self.Securities.ContainsKey('FINRA/FNSQ_' + ticker):
                data = self.Securities['FINRA/FNSQ_' + ticker].GetLastData()
                if data != None:
                    short_vol = data.GetProperty("SHORTVOLUME")
                    total_vol = data.GetProperty("TOTALVOLUME")
                    if short_vol != 0 and total_vol != 0:
                        short_ratio = short_vol / total_vol
                        days_to_cover[symbol] = short_ratio / daily_turnover
    
# Sorting by DTC.
sorted_by_dtc = sorted(days_to_cover.items(), key = lambda x: x[1], reverse = True)
decile = int(len(sorted_by_dtc) / 10)
long = [x[0] for x in sorted_by_dtc[-decile:]]
short = [x[0] for x in sorted_by_dtc[:decile]]
        
# Trade execution and rebalance.
long_count = len(long)
short_count = len(short)

stocks_invested = [x.Key for x in self.Portfolio if x.Value.Invested]
for symbol in stocks_invested:
    if symbol not in long + short:
        self.Liquidate(symbol)
for symbol in long:
    if self.Securities[symbol].Price != 0:
        self.SetHoldings(symbol, 1 / long_count)
for symbol in short:
    if self.Securities[symbol].Price != 0:
        self.SetHoldings(symbol, -1 / short_count)
    
class QuandlFINRA_ShortVolume(PythonQuandl):
def __init__(self):
self.ValueColumnName = 'SHORTVOLUME'    # also 'TOTALVOLUME' is accesible
# Custom fee model
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))