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.

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 Return5.41%
Volatility10.55%
Beta0.633
Sharpe Ratio0.51
Sortino RatioN/A
Maximum DrawdownN/A
Win Rate83%

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

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading