Quant Buffet放轻松,别过度思虑

卖空回补天数策略

登录后收藏

学术论文

Days to Cover and Stock Returns

作者覆盖天数与股票回报 [点击查看论文]

机构
  • 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

策略概要

该策略针对纽约证券交易所、美国证券交易所和纳斯达克公司,将覆盖天数(DTC)计算为股票的空头比率除以其平均每日周转率。每月,计算DTC并将股票分为十分位数。通过做多最低DTC十分位数和做空最高十分位数来形成多空投资组合,捕捉与空头利息和交易活动相关的潜在错误定价。投资组合中的股票按价值加权,并且每月重新平衡投资组合,以保持与更新的DTC排名一致。这种方法利用DTC动态来利用空头利息和交易周转率指标的潜在低效率。

II. 策略合理性

学术研究强调,覆盖天数(DTC)是卖空者的关键指标,反映了在不主导每日交易量的情况下平仓的难易程度。卖空者更喜欢流动性强的股票,即DTC较低的股票,以便于交易执行,因为高DTC表示更高的边际交易成本。空头利息高而DTC低的股票不一定被高估,但更容易交易。然而,当股票的DTC较高时,这表明套利者对其高估有信心,尽管卖空流动性差的股票存在更高的成本和风险。DTC是评估市场情绪和潜在错误定价的有用信号。

回测表现

波动率17.87%
夏普比率0.45
胜率49%

完整 Python 代码

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"))