Quant BuffetRelax, Not Over Thinking

Insiders’ Silence

Log in to collect

Academic paper

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

AuthorsGeorge Gao; Qingzhong Ma

Institute
  • 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