Insiders’ Silence
Log in to collectAcademic paper
The Sound of Silence: What Do We Know When Insiders Do Not Trade?
George Gao; Qingzhong Ma
- 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
Strategy in a nutshell
The strategy invests in ~90 futures/ETFs across equities, bonds, commodities, and REITs. Trend signals determine uptrend or downtrend allocations, with top performers within asset classes selected and portfolios rebalanced monthly.
Economic rationale
Trend following reduces behavioral biases by cutting losers and letting winners run. It removes negative fat tails and exploits momentum effects, improving performance through systematic, rules-based execution.
Backtest performance
Annualised return11.22%
Volatility64.22%
Beta0.2
Sharpe ratio0.17
Win rate41%
Full Python code
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