投资范围包括NYSE、AMEX和Nasdaq中销售额大于1000万美元的股票。这些股票首先按市值分为两半,然后根据资产回报率(ROA)进行十分位分组。ROA通过季度收益(Compustat的IBQ项)除以上一季度的资产(ATQ项)计算。投资者做多市值较大和较小组中资产回报率最高的前三个十分位股票,做空最低的后三个十分位股票。策略每月再平衡,股票等权重配置。

策略概述

投资范围包括NYSE、AMEX和Nasdaq中销售额大于1000万美元的股票。股票按市值分为两半,随后根据资产回报率(ROA)进行十分位分组。ROA按季度收益(Compustat季度项IBQ – 非常项目前收入)除以上一季度的资产(项ATQ – 总资产)计算。投资者做多市值较大和较小两组中资产回报率最高的前三个十分位的股票,并做空资产回报率最低的后三个十分位的股票。策略每月再平衡,股票等权重配置。

策略合理性

研究表明,拥有高生产性资产的公司应该比不具生产性资产的公司产生更高的平均收益。对于生产性公司,投资者要求更高的平均回报,而这些公司定价类似于投资者要求较低回报的非生产性公司。因此,生产率的变化有助于识别投资者要求的回报率的变化。因此,盈利能力较强的公司产生的平均收益高于盈利能力较差的公司(因为生产力帮助识别这种变化——更高的盈利能力表明更高的要求回报率)。这就是资产回报率因子的动机。

论文来源

An Alternative Three-Factor Model [点击浏览原文]

<摘要>

一个由市场因子、投资因子和股本回报率因子组成的新模型是理解股票预期收益横截面的良好起点。当公司盈利能力高且资本成本低时,它们将大量投资。因此,控制盈利能力时,投资应与预期收益负相关;控制投资时,盈利能力应与预期收益正相关。新的三因素模型减少了基于异常现象的广泛交易策略的异常回报幅度,通常到不显著的程度。该模型的表现及其经济直觉表明,它可用于在实践中获得预期收益估计。

回测表现

年化收益率12.15%
波动率13.36%
Beta-0.176
夏普比率-0.05
索提诺比率-0.054
最大回撤47.6%
胜率51%

完整python代码

from AlgoLib import *

class ROAEffectWithinStocks(XXX):

    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000) 

        market:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
        self.quantile:int = 10
        self.leverage:int = 5
        self.sales_threshold:float = 1e7
        self.exchange_codes:List[str] = ['NYS', 'NAS', 'ASE']
        
        self.long:List[Symbol] = []
        self.short:List[Symbol] = []

        self.fundamental_count:int = 500
        self.fundamental_sorting_key = lambda x: x.DollarVolume

        self.selection_flag:bool = False
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.FundamentalSelectionFunction)
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
        self.Schedule.On(self.DateRules.MonthEnd(market), self.TimeRules.AfterMarketOpen(market), self.Selection)

    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 \
            x.ValuationRatios.SalesPerShare * x.EarningReports.DilutedAverageShares.Value > self.sales_threshold and \
            not np.isnan(x.OperationRatios.ROA.ThreeMonths) and x.OperationRatios.ROA.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]]
                    
        # Sorting by market cap.
        sorted_by_market_cap = sorted(selected, key = lambda x: x.MarketCap, reverse=True)
        half:int = int(len(sorted_by_market_cap) / 2)
        top_mc = [x for x in sorted_by_market_cap[:half]]
        bottom_mc = [x for x in sorted_by_market_cap[half:]]
        
        if len(top_mc) >= self.quantile and len(bottom_mc) >= self.quantile:
            # Sorting by ROA.
            sorted_top_by_roa:List[Fundamental] = sorted(top_mc, key = lambda x:(x.OperationRatios.ROA.Value), reverse=True)
            quantile:int = int(len(sorted_top_by_roa) / self.quantile)
            long_top:List[Symbol] = [x.Symbol for x in sorted_top_by_roa[:quantile*3]]
            short_top:List[Symbol] = [x.Symbol for x in sorted_top_by_roa[-(quantile*3):]]
            
            sorted_bottom_by_roa:List[Fundamental] = sorted(bottom_mc, key = lambda x:(x.OperationRatios.ROA.Value), reverse=True)
            quantile = int(len(sorted_bottom_by_roa) / self.quantile)
            long_bottom:List[Symbol] = [x.Symbol for x in sorted_bottom_by_roa[:quantile*3]]
            short_bottom:List[Symbol] = [x.Symbol for x in sorted_bottom_by_roa[-(quantile*3):]]
            
            self.long = long_top + long_bottom 
            self.short = short_top + short_bottom

        return self.long + self.short
    
    def OnData(self, data: Slice) -> None:
        if not self.selection_flag:
            return
        self.selection_flag = False

        # order execution
        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:
        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