Quant Buffet放轻松,别过度思虑

股份发行效应

登录后收藏

学术论文

股票回报横截面中的股权发行效应

作者兰开斯特

机构
  • ?博恩霍尔特,澳大利亚储备银行,格里菲斯大学
论文摘要

之前的研究将美国股票中的净股票发行异常描述为普遍存在的,无论是在基于规模的排序中,还是在横截面回归中。为了进一步检验其普遍性,本文对澳大利亚股票市场中的股票发行效应进行了深入研究。该异常在所有规模的股票中都得到了观察,唯独微型股票除外。例如,1990年至2009年间,不发行股票的大型股票的等权重组合表现超过了高发行股票的大型股票组合,平均每月超额收益为0.84%。这一超额收益在风险调整后仍然存在,并且似乎涵盖了澳大利亚股票回报中的资产增长效应。

策略概要

策略专注于澳大利亚证券交易所的股票,主要选择市值排名前30%的股票。每年12月底,股票根据净股权发行量被分为八个投资组合,净股权发行量计算方式为:用调整后股份的自然对数(六月 t-1)减去六月 t-2。负发行量的股票被分为NegLow(最负)和NegHigh组合,而零发行量的股票组成Zeros组合。正发行量的股票按五分位数划分(从PosLow到PosHigh)。投资者会在Zeros组合中做多,在PosHigh组合中做空,所有头寸按等权重分配,并每年重新平衡一次。

策略合理性

新股发行意味着现有股东的股份稀释,因此股价对这种行为做出反应是可以理解的。投资者对股权发行信息的滞后反应与行为偏差有关。

回测表现

波动率12.25%
夏普比率0.54
索提诺比率-0.045
胜率66%

完整 Python 代码

from AlgorithmImports import *
from math import isnan
class ShareIssuanceEffect(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)

self.long:List[Symbol] = []
self.short:List[Symbol] = []
self.shares_number:Dict[Symbol, RollingWindow] = {}
self.leverage:int = 5
self.min_share_price:float = 5.

market:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol

self.fundamental_count:int = 500
self.fundamental_sorting_key = lambda x: x.DollarVolume
self.record_shares_flag = False
self.record_shares_flag_month:int = 6
self.selection_flag = False
self.selection_flag_month:int = 11
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.FundamentalSelectionFunction)
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.Schedule.On(self.DateRules.MonthEnd(market), self.TimeRules.BeforeMarketClose(market), self.Selection)
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)

def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
if not self.selection_flag and not self.record_shares_flag:
    return Universe.Unchanged
selected:List[Fundamental] = [
    x for x in fundamental if x.HasFundamentalData and x.Market == 'usa' and x.Price >= self.min_share_price and \
    not isnan(x.FinancialStatements.BalanceSheet.OrdinarySharesNumber.ThreeMonths) and x.FinancialStatements.BalanceSheet.OrdinarySharesNumber.ThreeMonths > 0
]

if len(selected) > self.fundamental_count:
    selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
if self.record_shares_flag:
    for stock in selected:
        symbol:Symbol = stock.Symbol
    
        if symbol not in self.shares_number:
            self.shares_number[symbol] = RollingWindow[float](2)
        
        shares_number:float = stock.FinancialStatements.BalanceSheet.OrdinarySharesNumber.ThreeMonths
        self.shares_number[symbol].Add(shares_number)
    
    # NOTE: Get rid of old shares number records so we work with latest values.
    del_symbols:List[Symbol] = []
    for symbol in self.shares_number:
        if symbol not in [x.Symbol for x in selected]:
            del_symbols.append(symbol)
    for symbol in del_symbols:
        del self.shares_number[symbol]
    
    self.record_shares_flag = False
    
elif self.selection_flag:
    net_issuance:Dict[Symbol, float] = {}
    
    for stock in selected:
        symbol:Symbol = stock.Symbol
        
        if symbol in self.shares_number and self.shares_number[symbol].IsReady:
            shares_values:List[float] = list(self.shares_number[symbol])
            net_issuance[symbol] = shares_values[0] / shares_values[-1] - 1
        
    if len(net_issuance) != 0:
        zero_net_issuance:List[float] = [x[0] for x in net_issuance.items() if x[1] == 0]
        
        pos_net_issuance:List = [x for x in net_issuance.items() if x[1] > 0]
        sorted_pos_by_net_issuance:List = sorted(pos_net_issuance, key = lambda x: x[1], reverse = True)
        quantile:int = int(len(sorted_pos_by_net_issuance)/5)
        pos_high:List[Symbol] = [x[0] for x in sorted_pos_by_net_issuance[:quantile]]
        
        neg_net_issuance:List = [x for x in net_issuance.items() if x[1] < 0]
        sorted_neg_by_net_issuance:List = sorted(neg_net_issuance, key = lambda x: x[1], reverse = False)
        half:int = int(len(sorted_neg_by_net_issuance) / 2)
        neg_high:List[Symbol] = [x[0] for x in sorted_neg_by_net_issuance[:half]]
        
        #self.long = zero_net_issuance
        self.long = neg_high 
        self.short = pos_high
    
return self.long + self.short

def OnData(self, data: Slice) -> None:
if not self.selection_flag:
    return
self.selection_flag = False
# Trade execution and rebalance
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()

def Selection(self) -> None:
if self.Time.month == self.record_shares_flag_month:
    self.record_shares_flag = True
elif self.Time.month == self.selection_flag_month:
    self.selection_flag = True
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))