Not-sold Insider Holdings Effect
Log in to collectAcademic paper
Is ‘Not Trading’ Informative? Evidence from Corporate Insiders’ Portfolios
Luke DeVault; Scott Cederburg; Kainan Wang
- Clemson University
- ?Clemson University - Department of Finance
- University of Arizona
- ?University of Arizona - Department of Finance
- University of Toledo
Strategy in a nutshell
This strategy targets NYSE, NYSE Arca, AMEX, and NASDAQ stocks with share codes 10 or 11 and prices above $5, excluding options and derivatives. Using SEC Form 3 and Form 4 filings, insider portfolios are estimated. Each month, a portfolio is created comprising stocks that insiders did not sell that month. Stocks sold by any insider are excluded. The portfolios are equally weighted, held for 12 months, and rebalanced annually, resulting in 12 overlapping portfolios each year.
Economic rationale
Insiders possess superior knowledge of their companies, so stocks they choose not to sell are likely undervalued. This effect is persistent, exhibits low turnover and transaction costs, and remains significant across monthly to annual rebalancing periods. Performance is robust even after controlling for size, book-to-market, and momentum factors, highlighting the unique predictive power of insider “not-sold” holdings.
Backtest performance
Full Python code
from AlgorithmImports import *
import pandas as pd
from io import StringIO
#endregion
class NotSoldInsiderHoldingsEffect(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2010, 1, 1)
self.SetCash(100000)
self.last_date = None
self.insiders_by_symbol = {} # storing list of insiders keyed by symbols for trading
self.managed_queue = []
self.holding_period = 12 # holding each stock for 12 months
self.insiders_trading = {} # list of insiders data keyed by date
sp100_stocks = ['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']
for symbol in sp100_stocks:
data = self.AddEquity(symbol, Resolution.Daily)
data.SetLeverage(10)
data.SetFeeModel(CustomFeeModel())
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")
df = pd.read_csv(StringIO(csv_string_file), sep=';', parse_dates=['Tran.Date'], date_parser=parser)
# this is only possible, because index consists of numbers in sequence
for index in range(df.shape[0]):
date = df.loc[index, 'Tran.Date'].date()
data_dict = df.loc[index, ['Symbol', 'Filer Name', 'Relation', 'Action']].to_dict()
if date not in self.insiders_trading:
self.insiders_trading[date] = []
self.insiders_trading[date].append(data_dict)
if self.last_date is None or self.last_date < date:
self.last_date = date
self.recent_month = -1
self.sold_shares_previous_month = {} # monthly stored sellers
self.is_already_insider_by_symbol = {} # store every insider in any company
def OnData(self, data):
current_date = self.Time.date()
if current_date in self.insiders_trading:
curr_date_data = self.insiders_trading[current_date]
for insider_dict in curr_date_data:
symbol = insider_dict['Symbol']
insider_name = insider_dict['Filer Name']
relation = insider_dict['Relation'].lower()
trade = insider_dict['Action'].lower()
# insider sold his/her holdings
if symbol in self.insiders_by_symbol and trade == 's' and insider_name in self.insiders_by_symbol[symbol]:
# store name of insider who sold shares during current month
if symbol not in self.sold_shares_previous_month:
self.sold_shares_previous_month[symbol] = []
self.sold_shares_previous_month[symbol].append(insider_name)
# not deleting name - insider is still active but he will be counted out from trading for the next month
# self.insiders_by_symbol[symbol].remove(insider_name)
# if len(self.insiders_by_symbol[symbol]) == 0:
# del self.insiders_by_symbol[symbol]
# check if insider is officer or director in this company and made buy or sell
if ('officer' in relation or 'director' in relation) and (trade in ['s', 'b']):
for is_insider_already_symbol in self.is_already_insider_by_symbol:
# check wheter insider is also insider in other company and not counted in trading collection
if is_insider_already_symbol != symbol and insider_name in self.is_already_insider_by_symbol[is_insider_already_symbol]:
if symbol not in self.insiders_by_symbol:
self.insiders_by_symbol[symbol] = []
self.insiders_by_symbol[symbol].append(insider_name)
break
if symbol not in self.is_already_insider_by_symbol:
self.is_already_insider_by_symbol[symbol] = []
# store insider name for one symbol
if insider_name not in self.is_already_insider_by_symbol[symbol]:
self.is_already_insider_by_symbol[symbol].append(insider_name)
if self.Time.month == self.recent_month:
return
self.recent_month = self.Time.month
long = []
long_length = 0
# create new portfolio part for trade
for symbol, insiders_list in self.insiders_by_symbol.items():
if symbol in data and data[symbol]:
last_price = data[symbol].Value
# seller will be counted out from trading for the next month
n_of_sellers_last_month = len(self.sold_shares_previous_month[symbol]) if symbol in self.sold_shares_previous_month else 0
symbol_diff_occurences = len(insiders_list) - n_of_sellers_last_month
long_length += symbol_diff_occurences
long.append((symbol, symbol_diff_occurences, last_price))
# clear previous month's sells
self.sold_shares_previous_month.clear()
# stop trading at the end of the date
if self.last_date <= current_date:
self.insiders_by_symbol.clear()
long_symbol_q = []
if long_length != 0:
for symbol, diff_occ, price in long:
# calculate portfolio weight for curr symbol
weight = self.Portfolio.TotalPortfolioValue / self.holding_period / long_length * diff_occ
long_symbol_q.append((symbol, np.floor(weight / price)))
self.managed_queue.append(RebalanceQueueItem(long_symbol_q))
# rebalance portfolio
remove_item = None
for item in self.managed_queue:
if item.holding_period == self.holding_period: # all portfolio parts are held for n months
for symbol, quantity in item.opened_symbol_q:
self.MarketOrder(symbol, -quantity)
remove_item = item
# trade execution
if item.holding_period == 0: # all portfolio parts are held for n months
opened_symbol_q = []
for symbol, quantity in item.opened_symbol_q:
if symbol in data and data[symbol]:
self.MarketOrder(symbol, quantity)
opened_symbol_q.append((symbol, quantity))
# only opened orders will be closed
item.opened_symbol_q = opened_symbol_q
item.holding_period += 1
# 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)
class RebalanceQueueItem():
def __init__(self, symbol_q):
# symbol/quantity collections
self.opened_symbol_q = 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"))