
“该策略交易澳大利亚大型股票,每年根据净发行量对股票进行排序,建立投资组合。投资者做多零股发行股票,同时做空高股发行股票,并且每年等权重新平衡头寸。“
资产类别: 股票 | 地区: 美国 | 周期: 每年 | 市场: 股票 | 关键词: 股权,发行效应
I. 策略概要
策略专注于澳大利亚证券交易所的股票,主要选择市值排名前30%的股票。每年12月底,股票根据净股权发行量被分为八个投资组合,净股权发行量计算方式为:用调整后股份的自然对数(六月 t-1)减去六月 t-2。负发行量的股票被分为NegLow(最负)和NegHigh组合,而零发行量的股票组成Zeros组合。正发行量的股票按五分位数划分(从PosLow到PosHigh)。投资者会在Zeros组合中做多,在PosHigh组合中做空,所有头寸按等权重分配,并每年重新平衡一次。
II. 策略合理性
新股发行意味着现有股东的股份稀释,因此股价对这种行为做出反应是可以理解的。投资者对股权发行信息的滞后反应与行为偏差有关。
III. 来源论文
股票回报横截面中的股权发行效应 [点击查看论文]
- 兰开斯特,博恩霍尔特,澳大利亚储备银行,格里菲斯大学
<摘要>
之前的研究将美国股票中的净股票发行异常描述为普遍存在的,无论是在基于规模的排序中,还是在横截面回归中。为了进一步检验其普遍性,本文对澳大利亚股票市场中的股票发行效应进行了深入研究。该异常在所有规模的股票中都得到了观察,唯独微型股票除外。例如,1990年至2009年间,不发行股票的大型股票的等权重组合表现超过了高发行股票的大型股票组合,平均每月超额收益为0.84%。这一超额收益在风险调整后仍然存在,并且似乎涵盖了澳大利亚股票回报中的资产增长效应。


IV. 回测表现
| 年化回报 | 10.56% |
| 波动率 | 12.25% |
| β值 | -0.133 |
| 夏普比率 | 0.54 |
| 索提诺比率 | -0.045 |
| 最大回撤 | N/A |
| 胜率 | 66% |
V. 完整的 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"))