
“该策略利用内部交易模式,根据净购买比率(NPR)对股票排序,对最高十分位股票进行做多,最低十分位股票进行做空,并每年重新平衡,以利用纽约证券交易所(NYSE)、美国证券交易所(AMEX)和纳斯达克(NASDAQ)股票的潜在定价错误。”
资产类别:股票 | 区域: 美国 | 频率: 每年 | 市场: 股票市场 | 关键词: 内部人士
策略概述
该策略主要针对NYSE、AMEX和NASDAQ的股票,要求股价高于2美元,不包括封闭式基金、不动产投资信托(REITs)和美国存托凭证(ADRs)。
- 在每年4月底,计算过去6个月的净购买比率(NPR):
NPR = 内部人士买入 – 卖出 / 总交易量
根据NPR对股票排序:
- 持有NPR最高的十分位(正向净购买)股票,做多。
- 持有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"))