
The investment universe consists of NYSE, NYSE Arca (exchange code 1,2,3 and 4), AMEX, and NASDAQ stocks with share codes 10 or 11. Stocks with share prices less than 5 dollars, options, and derivatives are excluded.
ASSET CLASS: stocks | REGION: United States | FREQUENCY:
Monthly | MARKET: equities | KEYWORD: Holdings Effect
I. 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.
II. 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.
III. SOURCE PAPER
Is ‘Not Trading’ Informative? Evidence from Corporate Insiders’ Portfolios [Click to Open PDF]
Luke DeVault, Clemson University – Department of Finance;
Scott Cederburg, University of Arizona – Department of Finance;
Kainan Wang, University of Toledo
<Abstract>
Some individuals, e.g., those holding multiple directorships, are insiders at multiple firms. When they execute an insider trade at one firm, they may reveal information about the value of all—both the traded insider position and not-traded insider position(s)—the securities held in their “insider portfolio.” We find that insider “not-sold” stocks outperform “not-bought” stocks. Implementable trading strategies that buy not-sold stocks following the disclosure of a sale earn alphas up to 4.8% per year after trading costs. The results suggest that even insider sales that are motivated by liquidity and diversification needs can provide value-relevant information about insider holdings.


IV. BACKTEST PERFORMANCE
| Annualised Return | 5.41% |
| Volatility | 10.55% |
| Beta | 0.633 |
| Sharpe Ratio | 0.51 |
| Sortino Ratio | N/A |
| Maximum Drawdown | N/A |
| Win Rate | 83% |
V. 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"))