Quant Buffet放轻松,别过度思虑

内部人士的沉默

登录后收藏

学术论文

The Sound of Silence: What Do We Know When Insiders Do Not Trade?

作者沉默的声音:当内部人士不交易时,我们知道什么?[点击查看论文]

机构
  • Cornell University
  • ?T. Rowe Price Group (United States)
  • ?Cornell University - Samuel Curtis Johnson Graduate School of Management
  • ?T. Rowe Price Group
  • California State University, Chico

策略概要

该策略专注于纽约证券交易所(NYSE)、美国股票交易所(Amex)和纳斯达克(Nasdaq)股票,每月根据前一个月的空头仓位将股票分为五个等级。在空头仓位最高的五分位内,识别出过去六个月内没有内幕交易活动的股票。投资者对这些股票进行做空,并持仓一年,同时在股市上建立多头仓位。该投资组合采用等权重,并每月进行再平衡,旨在利用在高空头仓位股票中缺乏内幕人士信心的情况。

II. 策略合理性

研究确定了两种解释内部交易模式的假设。诉讼风险假设认为,对于面临较高诉讼风险的公司,内部人士的沉默与较弱的负面未来回报相关。第二个假设认为,内部人士的投资组合约束推动了结果,因为他们不能卖空,必须保留股份以维持控制权,或者持有不可出售的受限股份。大量的内部人士卖出,随后交易减少,向市场发出潜在的坏信息信号,反映了内部人士由于约束或法律顾虑而不愿交易,最终影响市场认知和未来股票表现。

回测表现

波动率64.22%
夏普比率0.17
胜率41%

完整 Python 代码

from AlgorithmImports import *
from collections import deque
import pandas as pd
from io import StringIO
from numpy import floor
#endregion
class InsidersSilence(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2013, 1, 1)
self.SetCash(100000)
self.symbols = [
                'AAPL','MSFT','AMZN','FB','BRKB','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'
                ]
                
# Create custom universe.
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverseSelection(FineFundamentalUniverseSelectionModel(self.SelectCoarse, self.SelectFine))

self.period = 21
self.holding_period = 12
self.quantile = 5
self.max_SI_missing_days = 5
self.max_missing_insider_days = 3 * 31
self.trading_activity_period = 6 * 31

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

# Dataframe with insider trades for every stock.
self.insiders_trading = {}

self.short_interest = {}

for symbol in self.symbols:
    # Import insiders trading data.
    csv_string_file = self.Download(f'data.quantpedia.com/backtesting_data/economic/insiders_trading/{symbol}.csv')
    if csv_string_file == "": continue
    parser = lambda x: pd.datetime.strptime(x, "%Y-%m-%d")
    self.insiders_trading[symbol] = pd.read_csv(StringIO(csv_string_file), sep=';', parse_dates=['Tran.Date'], date_parser=parser)
    # Import short interest daily data.
    self.AddData(NasdaqCustomColumns, 'FINRA/FNSQ_' + symbol, Resolution.Daily)
    self.short_interest[symbol] = deque(maxlen = self.period)

self.selection_flag = True
self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.BeforeMarketClose(self.symbol), self.Selection)
def OnSecuritiesChanged(self, changes):
for security in changes.AddedSecurities:
    security.SetFeeModel(CustomFeeModel())
    security.SetLeverage(10)
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):
fine = [x for x in fine if x.Symbol.Value in self.insiders_trading]

short_interest = {}

for stock in fine:
    symbol = stock.Symbol
    ticker = symbol.Value

    # Last month's short_interest data is ready.
    if len(self.short_interest[ticker]) == self.short_interest[ticker].maxlen:
        if self.Securities['FINRA/FNSQ_' + ticker].GetLastData() and (self.Time.date() - self.Securities['FINRA/FNSQ_' + ticker].GetLastData().Time.date()).days > self.max_SI_missing_days:
            self.short_interest[ticker].clear()
            continue
        # Calculate monthly short interest.
        short_interest[symbol] = sum([x[0] for x in self.short_interest[ticker]]) / sum([x[1] for x in self.short_interest[ticker]])

if len(short_interest) < self.quantile:
    return Universe.Unchanged
    
# Sorting by short interest.
sorted_by_short_interest = sorted(short_interest.items(), key = lambda x: x[1], reverse = True)
quantile = int(len(sorted_by_short_interest) / self.quantile)
top_by_short_interest = [x[0] for x in sorted_by_short_interest[:quantile]]
    
short = []
    
# Find stocks which have not been traded 6 months prior to this moment.
for symbol in top_by_short_interest:
    ticker = symbol.Value
    if (self.Time.date() - self.insiders_trading[ticker].iloc[0]['Tran.Date'].date()).days > self.max_missing_insider_days:
        continue
    trades = [row['Tran.Date'] for index, row in self.insiders_trading[ticker].iterrows() if row['Symbol'] == ticker and row['Tran.Date'] >= (self.Time - timedelta(days = self.trading_activity_period)) and row['Tran.Date'] <= self.Time]
    if len(trades) == 0:
        short.append(symbol)

if len(short) != 0:
    # append market to short leg
    short.append(self.symbol)
    short_w = self.Portfolio.TotalPortfolioValue / self.holding_period / len(short)
    
    # symbol/quantity collection
    short_symbol_q = []
    
    for symbol in short:
        # We need last price for market order trade.
        history = self.History(symbol, 1, Resolution.Daily)
        if history.empty:
            continue
        closes = history.loc[symbol].close
        for time, close in closes.iteritems():
            if symbol == self.symbol:
                # long market
                short_symbol_q.append( (self.symbol, floor((self.Portfolio.TotalPortfolioValue / self.holding_period)) / close) )
            else:
                short_symbol_q.append( (symbol, -floor(short_w / close)) ) 
        
    self.managed_queue.append(RebalanceQueueItem(short_symbol_q))

return short

def OnData(self, data):
# Store short interest data.
for symbol in self.symbols:
    look_up_symbol = 'FINRA/FNSQ_' + symbol
    if look_up_symbol in data and data[look_up_symbol]:
        short_vol = data[look_up_symbol].GetProperty("SHORTVOLUME")
        total_vol = data[look_up_symbol].GetProperty("TOTALVOLUME")
        
        if symbol in self.short_interest:
            self.short_interest[symbol].append((short_vol, total_vol))
if not self.selection_flag:
    return
self.selection_flag = False

remove_item = None
# Rebalance portfolio
for item in self.managed_queue:
    if item.holding_period == self.holding_period:
                    
        # Buying short on Liquidate
        for symbol, quantity in item.short_symbol_q:
            self.MarketOrder(symbol, -quantity)
        
        remove_item = item
    
    # Trade execution    
    if item.holding_period == 0: 
        open_short_symbol_q = []
        
        for symbol, quantity in item.short_symbol_q:
            if symbol in data and data[symbol]:
                self.MarketOrder(symbol, quantity)
                open_short_symbol_q.append((symbol, quantity))
        
        # Only opened orders will be closed        
        item.short_symbol_q = open_short_symbol_q
        
    item.holding_period += 1
    
# We need to remove closed part of portfolio after loop. Otherwise it will miss one item in self.managed_queue.
if remove_item:
    self.managed_queue.remove(remove_item)

def Selection(self):
self.selection_flag = True

class RebalanceQueueItem():
def __init__(self, short_symbol_q):
# symbol/quantity collections
self.short_symbol_q = short_symbol_q
self.holding_period = 0

# Custom fee model
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))
# Quandl short interest data.
class NasdaqCustomColumns(NasdaqDataLink):
def __init__(self) -> None:
self.ValueColumnName = 'shortvolume'    # also 'TOTALVOLUME' is accesible