该策略利用内部交易模式,根据净购买比率(NPR)对股票排序,对最高十分位股票进行做多,最低十分位股票进行做空,并每年重新平衡,以利用纽约证券交易所(NYSE)、美国证券交易所(AMEX)和纳斯达克(NASDAQ)股票的潜在定价错误。

策略概述

该策略主要针对NYSE、AMEX和NASDAQ的股票,要求股价高于2美元,不包括封闭式基金、不动产投资信托(REITs)和美国存托凭证(ADRs)。

根据NPR对股票排序:

组合持有期为一年,并在每年重新平衡,通过内部交易模式捕捉潜在的股票定价错误。

经济基础

行为大量研究表明,内部人士拥有关于公司未来前景的非公开特殊信息,并利用这些信息进行交易时机选择。这些信号在小市值股票中尤为显著,因此,为此类系统正确实施交易策略并非易事。内部人士通常采取逆向投资策略,但他们能够比简单的逆向策略更好地预测市场走势,尤其是在小市值公司中。此外,与内部卖出相比,内部买入提供的信息更加具有指导性。

论文来源

Are Insiders’ Trades Informative? [点击浏览原文]

作者: Josef Lakonishok 和 Immoo Lee

<摘要>

我们记录了1975年至1995年期间在NYSE、AMEX和NASDAQ上市的所有公司内部交易活动。内部交易普遍存在,在超过一半的样本公司中,每年都会有一定的内部交易活动。通常,当内部人士交易及向SEC报告其交易时,市场波动较小。然而,内部人士整体表现为逆向投资者,并且其市场预测能力优于简单的逆向策略。此外,内部人士能够更好地预测横截面股票回报,但这一结果主要归因于其对小市值公司回报的预测能力。此外,与内部卖出相比,内部买入提供的信息更具参考价值。

回测表现

年化收益率7.7%
波动率N/A
Beta-0.106
夏普比率N/A
索提诺比率-0.223
最大回撤N/A
胜率46%

完整python代码



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

Leave a Reply

Discover more from Quant Buffet

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

Continue reading