该投资策略涵盖NYSE、Amex和Nasdaq的所有股票,数据来源包括CRSP和Compustat。策略每年6月底根据市值和收益分为高低组,并按等权重配置。公司根据市值分为两组,按收益分为三个组。H-L组合在每个市值组内做多高ROE公司、做空低ROE公司。组合持有一年,次年6月底重新评估并再平衡。

策略概述

投资范围包括在纽约证券交易所(NYSE)、美国证券交易所(Amex)和纳斯达克(Nasdaq)交易的所有股票,代码为10和11。股票的标准数据(例如月度回报、股息、价格和流通普通股数量)可以从证券价格研究中心(CRSP)收集,年度和季度的会计信息则来自Compustat,具体包括项目CEQ作为账面权益,Y(RD)为“毛利减去利息支出减去100%的销售和管理费用”(此策略的变体中的收益衡量标准)。

<投资方法>

组合每年重新平衡,组合中的股票按等权重配置。

在每年6月底,根据纽约证券交易所公司市值的中位数,将公司分配为两类市值(ME)组。

根据纽约证券交易所公司在上一日历年(t-1年)报告的年度收益,将公司独立分配到三个盈利能力组(30%和70%的分位数)。

H-L组合即在每个市值组内做多高ROE组,做空低ROE组。

组合持有一年,并在下一年6月底重新评估。

策略合理性

表5的结果表明,增加30%的销售和管理费用可以提高无形资产调整因子的解释力。Fama和French(2015)提出了一个捕捉公司盈利能力冲击的广泛经济风险因子,即RMW(强者减弱者)。构建该因子需要衡量公司的盈利能力。Fama和French(2015)使用的是公司财务报表中按公认会计准则(GAAP)报告的税前和折旧前的收益。尽管研发和部分SG&A支出应视为投资,但根据GAAP规定,它们在整个样本期间被计入支出。作者提出并成功证明了调整这些差异的新方法,并选择了一个简单的策略来在现实世界中验证他们的发现。

论文来源

An Intangibles-Adjusted Profitability Factor [点击浏览原文]

<摘要>

我们将创造无形资产的支出视为投资,而不是将其计入支出,在衡量公司股本回报率时将其加回到收益中,用于构建Fama和French(2015)五因子模型中的盈利因子。我们构建的盈利因子相对于许多现有的多因子资产定价模型(包括标准的Fama-French五因子模型)具有显著的超额收益。当将Fama和French(2015)五因子模型中的盈利因子替换为无形资产调整后的盈利因子时,该模型在解释股票回报的横截面和文献中记录的多个异常现象方面表现得更好。利用价格动量、收益动量和经营杠杆的投资组合不再具有显著的超额收益。

这一改进与股权估值的股息折现模型一致。通过将创造无形资产的支出视为投资来调整构建的收益,在预测股票的未来现金股息和经营现金流的横截面时,尤其是在更长的时间跨度上,表现更好。采用我们的调整方法在Hou等人(2015)四因子模型中构建月度重新平衡的盈利因子也提高了其表现。与标准的盈利因子相比,我们的无形资产调整后的盈利因子具有更小的左尾风险和与市场的共尾风险。

回测表现

年化收益率4.16%
波动率8.47%
Beta-0.139
夏普比率0.49
索提诺比率N/A
最大回撤N/A
胜率54%

完整python代码

from AlgorithmImports import *
# endregion
class IntangiblesAdjustedProfitabilityFactor(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)
        self.market:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
        self.leverage:int = 3
        self.quantile:int = 3
        self.coarse_count:int = 1000
        self.selection_month:int = 6
        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.CoarseSelectionFunction, self.FineSelectionFunction)
        self.Schedule.On(self.DateRules.MonthEnd(self.market), self.TimeRules.BeforeMarketClose(self.market), self.Selection)
        
    def OnSecuritiesChanged(self, changes:SecurityChanges) -> None:
        for security in changes.AddedSecurities:
            security.SetFeeModel(CustomFeeModel())
            security.SetLeverage(self.leverage)
    
    def CoarseSelectionFunction(self, coarse:List[CoarseFundamental]) -> List[Symbol]:
        if not self.selection_flag:
            return Universe.Unchanged
        selected:List[Symbol] = [x.Symbol
            for x in sorted([x for x in coarse if x.HasFundamentalData and x.Market == 'usa'],
                key = lambda x: x.DollarVolume, reverse = True)[:self.coarse_count]]
        # selected = [x.Symbol for x in coarse if x.HasFundamentalData and x.Market == 'usa']
        return selected
    def FineSelectionFunction(self, fine:List[FineFundamental]) -> List[Symbol]:
        fine = [x for x in fine if x.MarketCap != 0 and \
                (x.SecurityReference.ExchangeId == 'NYS') or (x.SecurityReference.ExchangeId == 'NAS') or (x.SecurityReference.ExchangeId == 'ASE')]
        # if len(fine) > self.coarse_count:
        #     fine = sorted(fine, key=lambda x: x.MarketCap, reverse=True)[:self.coarse_count]
        # profitability calculation
        profitability:Dict[Symbol, float] = {
            stock.Symbol : stock.FinancialStatements.IncomeStatement.GrossProfit.TwelveMonths - stock.FinancialStatements.IncomeStatement.InterestExpense.TwelveMonths - stock.FinancialStatements.IncomeStatement.GeneralAndAdministrativeExpense.TwelveMonths \
            for stock in fine
        }
        
        # sort and divide into quantiles
        if len(profitability) >= self.quantile:
            sorted_roe:List[Symbol] = sorted(profitability, key=profitability.get, reverse=True)
            quantile:int = len(sorted_roe) // self.quantile
            self.long = sorted_roe[:quantile]
            self.short = sorted_roe[-quantile:]
            self.rebalance_flag = True
        return self.long + self.short
    def OnData(self, data: Slice):
        # yearly rebalance
        if not self.selection_flag:
            return
        self.selection_flag = False
        invested:List[Symbol] = [x.Key for x in self.Portfolio if x.Value.Invested]
        for symbol in invested:
            if symbol not in self.long + self.short:
                self.Liquidate(symbol)
        
        # trade execution
        for symbol in self.long:
            if symbol in data and data[symbol]:
                self.SetHoldings(symbol, 1/len(self.long))
        
        for symbol in self.short:
            if symbol in data and data[symbol]:
                self.SetHoldings(symbol, -1/len(self.long))
    def Selection(self) -> None:
        # selection on end of June
        if self.Time.month != self.selection_month:
            return
        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"))

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading