“该策略通过按账面市值比强度对最高规模五分位数股票进行排序,交易纽约证券交易所、美国证券交易所和纳斯达克股票,做多最低股票,做空最高股票,并按季度重新平衡。”

I. 策略概要

该策略针对纽约证券交易所、美国证券交易所和纳斯达克股票,不包括金融股票和季度末价格低于1美元的股票。股票被分为五个规模五分位数,重点关注最高五分位数。在该组中,股票根据其账面市值比(B/M)强度分为五分位数,该强度计算为相对于过去八个季度标准差的超额B/M。该策略做多B/M强度最低的投资组合,做空B/M强度最高的投资组合。投资组合按价值加权,并按季度重新平衡,通过系统性的B/M比率分析优化回报。

II. 策略合理性

账面市值比(B/M)强度的相关性与B/M及其历史波动率均较低,表明其具有独特的信息内容。高B/M通常与高波动率相关,因此需要通过历史波动率缩放B/M的增长,以捕捉有意义的变化。基于这种转换的强度度量,可以有效地分类和预测未来的股票回报。它还显示出与中大型投资组合中的股票回报呈强烈的负相关关系,以及与过去的累计回报呈高相关性。这种方法为理解和预测股票表现提供了一个强大的工具。

III. 来源论文

Growth Stocks Are More Risky: New Evidence on Cross-Sectional Stock Returns [点击查看论文]

<摘要>

传统观点认为,成长型股票风险更高,因此应获得更高的溢价。然而,实证证据表明,基于账面市值比分类的价值型股票往往具有更高的溢价。为了解决这一矛盾,本文将账面市值比分解为两个组成部分:趋势组成部分和临时(创新)组成部分。经济解释和实证结果均表明,即使在控制包括账面市值比在内的主要回报预测因子后,临时组成部分与未来横截面股票回报之间仍存在强烈的负相关关系,而趋势组成部分与价值溢价呈正相关。因此,与传统观点一致,我们的结果证实,账面市值比的临时组成部分捕获了成长溢价。

IV. 回测表现

年化回报4.14%
波动率15.89%
β值-0.077
夏普比率0.26
索提诺比率-0.12
最大回撤N/A
胜率53%

V. 完整的 Python 代码

import numpy as np
from AlgorithmImports import *
from numpy import isnan
class UsingIntensityofBooktoMarkettoIdentifyGrowthPremium(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)
        self.rebalance_month:int = 4
        self.quantile:int = 5
        self.leverage:int = 5
        self.exchange_codes:List[str] = ['NYS', 'NAS', 'ASE']
        
        market:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
        self.weight:Dict[Symbol, float] = {}
        self.bm_data:Dict[Symbol, RollingWindow] = {}
        self.bm_period:int = 8
        self.fundamental_count:int = 3000
        self.fundamental_sorting_key = lambda x: x.MarketCap
        self.month:int = 12
        self.selection_flag:bool = False
        self.UniverseSettings.Resolution = Resolution.Daily
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
        self.AddUniverse(self.FundamentalSelectionFunction)
        self.Schedule.On(self.DateRules.MonthStart(market), self.TimeRules.AfterMarketOpen(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:
            return Universe.Unchanged
        selected:List[Fundamental] = [x for x in fundamental if x.HasFundamentalData and x.Market == 'usa' and x.SecurityReference.ExchangeId in self.exchange_codes and \
            not isnan(x.ValuationRatios.PBRatio) and x.ValuationRatios.PBRatio != 0 and not isnan(x.MarketCap) and x.MarketCap != 0
            ]
        if len(selected) > self.fundamental_count:
            selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
        # BM intensity.
        bm_intensity:Dict[Fundamental, float] = {}
        for stock in selected:
            symbol:Symbol = stock.Symbol
            # BM ratio calc
            if symbol not in self.bm_data:
                self.bm_data[symbol] = RollingWindow[float](self.bm_period)
            
            bm:float = 1. / stock.ValuationRatios.PBRatio
            
            if self.bm_data[symbol].IsReady:
                # Intensity calc.
                bms:List[float] = list(self.bm_data[symbol])
                avg_bm:float = np.mean(bms)
                std_bm:float = np.std(bms)
                intensity:float = (bm - avg_bm) / std_bm
                
                bm_intensity[stock] = intensity
                
            self.bm_data[symbol].Add(bm)
        
        if len(bm_intensity) >= self.quantile ** 2:
            # Market cap sorting
            sorted_by_market_cap = sorted(bm_intensity.items(), key = lambda x: x[0].MarketCap, reverse = True)
            quantile:int = int(len(sorted_by_market_cap) / self.quantile)
            top_by_market_cap:List = [x for x in sorted_by_market_cap[:quantile]]
            # Intensity sorting
            sorted_by_intesity:List = sorted(top_by_market_cap, key = lambda x: x[1], reverse = True)
            quantile = int(len(sorted_by_intesity) / self.quantile)
            short:List[Fundamental] = [x[0] for x in sorted_by_intesity[:quantile]]
            long:List[Fundamental] = [x[0] for x in sorted_by_intesity[-quantile:]]
            # Market cap weighting.
            for i, portfolio in enumerate([long, short]):
                mc_sum:float = sum(map(lambda x: x.MarketCap, portfolio))
                for stock in portfolio:
                    self.weight[stock.Symbol] = ((-1) ** i) * stock.MarketCap / mc_sum
        return list(self.weight.keys())
    def OnData(self, data: Slice) -> None:
        if not self.selection_flag:
            return
        self.selection_flag = False
        
        # Trade execution.
        portfolio:List[PortfolioTarget] = [PortfolioTarget(symbol, w) for symbol, w in self.weight.items() if symbol in data and data[symbol]]
        self.SetHoldings(portfolio, True)
        self.weight.clear()
    def Selection(self) -> None:
        self.selection_flag = True
        if self.month % 3 == 0:
            self.selection_flag = True
    
        self.month += 1
        if self.month > 12:
            self.month = 1
# 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 的更多信息

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

继续阅读