Quant BuffetRelax, Not Over Thinking

Not-sold Insider Holdings Effect

Log in to collect

Academic paper

Is ‘Not Trading’ Informative? Evidence from Corporate Insiders’ Portfolios

AuthorsLuke DeVault; Scott Cederburg; Kainan Wang

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

Annualised return5.41%
Volatility10.55%
Beta0.633
Sharpe ratio0.51
Win rate83%

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"))