Quant BuffetRelax, Not Over Thinking

Insiders Trading Effect in Stocks

Log in to collect

Academic paper

Strategy in a nutshell

This strategy invests in NYSE, AMEX, and NASDAQ stocks priced above $2, excluding closed-end funds, REITs, and ADRs. At each April month-end, the Net Purchase Ratio (NPR)—insider purchases minus sales divided by total insider transactions over the prior six months—is computed. Stocks are sorted into deciles, with the top decile (heavy insider buying) held long and the bottom decile (heavy insider selling) held short. Positions are held for one year and rebalanced annually, seeking to exploit mispricing driven by insider activity.

Economic rationale

Research shows insiders often act on non-public information about firms’ future performance, and their trades provide predictive signals of stock mispricing. Insider buying typically signals undervaluation, while insider selling suggests overvaluation. These effects are strongest among small-cap stocks, though implementation challenges exist due to liquidity and execution constraints.

Backtest performance

Annualised return7.7%
Beta-0.106
Sortino ratio-0.223
Win rate46%

Full Python code

from AlgorithmImports import *
from typing import List, Dict, Tuple
from collections import deque
#endregion
class InsidersTradingEffectInStocks(QCAlgorithm):
def Initialize(self) -> None:
self.SetStartDate(2020, 1, 1)
self.SetCash(1_000_000)
self.exchange_codes:List[str] = ['NYS', 'NAS', 'ASE']
self.leverage:int = 10
self.quantile:int = 10
self.min_share_price:int = 2
self.period:int = 6
self.selection_month:int = 5
self.months_to_collect_data:List[int] = [11, 12, 1, 2, 3, 4]
self.insider_data:Dict[str, Tuple[str, float, float]] = {}
self.last_shares_owned:Dict[str, deque(float, float)] = {}
self.insider_buys:Dict[str, List[float]] = {}
self.insider_sells:Dict[str, List[float]] = {}
self.long:List[Symbol] = []
self.short:List[Symbol] = []
self.selection_flag:bool = False
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.FundamentalSelectionFunction)
self.recent_month:int = -1
self.settings.daily_precise_end_time = False
def OnSecuritiesChanged(self, changes:SecurityChanges) -> None:
for security in changes.AddedSecurities:
    security.SetFeeModel(CustomFeeModel())
    security.SetLeverage(self.leverage)
    symbol:Symbol = security.Symbol
    dataset_symbol:Symbol = self.AddData(QuiverInsiderTrading, symbol).Symbol
def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
# selection in beginning of May
if self.Time.month == self.recent_month:
    return Universe.Unchanged
self.recent_month = self.Time.month
if self.Time.month != self.selection_month:
    return Universe.Unchanged
self.selection_flag = True
selected:List[Symbol] = [x for x in fundamental if x.HasFundamentalData and x.Price > self.min_share_price and x.MarketCap != 0 
                     and x.SecurityReference.ExchangeId in self.exchange_codes and not x.CompanyReference.IsREIT]
net_purchase_ratio:Dict[Symbol, float] = {}
for stock in selected:
    symbol:Symbol = stock.Symbol
    ticker:str = symbol.Value
    if ticker in set(list(self.insider_buys.keys()) + list(self.insider_sells.keys())):
        total_buys:float = self.insider_buys.get(ticker, [0])
        total_sells:float = self.insider_sells.get(ticker, [0])
        net_purchase_ratio_calculation:float = (len(total_buys) - len(total_sells)) / len(total_buys + total_sells)
        if net_purchase_ratio_calculation != 0:
            net_purchase_ratio[symbol] = net_purchase_ratio_calculation
self.insider_buys.clear()
self.insider_sells.clear()
if len(net_purchase_ratio) >= self.quantile:
    sorted_ratio:List[Symbol] = sorted(net_purchase_ratio, key=net_purchase_ratio.get)
    quantile:int = len(sorted_ratio) // self.quantile
    self.long = sorted_ratio[-quantile:]
    self.short = sorted_ratio[:quantile]

return list(map(lambda x:x.Symbol, selected))
def OnData(self, data:Slice) -> None:
# store data of insider trades and calculate if trade was bought/sold
for insider_trades in data.Get(QuiverInsiderTrading).values():
    for insider_trade in insider_trades:
        insider_name:str = insider_trade.Name
        stock_ticker:str = insider_trade.Symbol.Value
        
        if insider_name not in self.insider_data:
            self.insider_data[insider_name] = deque(maxlen=2)
        
        if insider_trade.SharesOwnedFollowing is not None:      
            self.insider_data[insider_name].append((stock_ticker, insider_trade.SharesOwnedFollowing, insider_trade.Shares))
        
        if len(self.insider_data[insider_name]) == self.insider_data[insider_name].maxlen and self.Time.month in self.months_to_collect_data:
            if self.insider_data[insider_name][0][1] < self.insider_data[insider_name][-1][1]:
                if stock_ticker not in self.insider_buys:
                    self.insider_buys[stock_ticker] = []
                self.insider_buys[stock_ticker].append(self.insider_data[insider_name][-1][2])
        
            elif self.insider_data[insider_name][0][1] > self.insider_data[insider_name][-1][1]:
                if stock_ticker not in self.insider_sells:
                    self.insider_sells[stock_ticker] = []
                self.insider_sells[stock_ticker].append(self.insider_data[insider_name][-1][2])
# yearly rebalance
if not self.selection_flag:
    return
self.selection_flag = False

# order execution
targets:List[PortfolioTarget] = []
for i, portfolio in enumerate([self.long, self.short]):
    for symbol in portfolio:
        if symbol in data and data[symbol]:
            targets.append(PortfolioTarget(symbol, ((-1) ** i) / len(portfolio)))

self.SetHoldings(targets, True)
self.long.clear()
self.short.clear()
# Custom fee model
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))