该策略交易澳大利亚大型股票,每年根据净发行量对股票进行排序,建立投资组合。投资者做多零股发行股票,同时做空高股发行股票,并且每年等权重新平衡头寸。

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

发表评论

了解 Quant Buffet 的更多信息

立即订阅以继续阅读并访问完整档案。

继续阅读