“该策略按盈利能力和价值对500家最大的非金融股票进行排名,买入前150名,卖空后150名,并每年进行再平衡,以利用盈利能力和价值的综合机会。”

I. 策略概要

该策略专注于拥有可用总利润/资产和账面市值比的500家最大的非金融股票。每年,股票按盈利能力和价值进行排名,并将排名合并。在六月底,该策略买入综合排名最高的150只股票各一美元,并卖空综合排名最低的150只股票各一美元。该投资组合等权重,并每年进行再平衡,利用盈利能力和价值指标的组合,通过多空方法捕捉系统性机会,同时降低风险。

II. 策略合理性

传统的价值策略买入廉价资产,卖出昂贵资产,而盈利能力策略则专注于收购生产性资产,卖出非生产性资产。研究表明,生产性公司应产生高于非生产性公司的平均回报。然而,要求高回报的生产性公司,其定价通常与要求较低回报的非生产性公司相似。这种差异源于生产率的变化,这表明投资者所需回报率的差异。较高的盈利能力表明较高的所需回报,使盈利公司更具吸引力。因此,利用盈利能力的策略通过捕捉生产性、盈利公司产生的较高平均回报而跑赢大盘。

III. 来源论文

价值的另一面:总盈利能力溢价 [点击查看论文]

<摘要>

以总利润/资产衡量的盈利能力,在预测平均回报的横截面方面,与账面市值比具有大致相同的能力。盈利公司产生的回报明显高于亏损公司,尽管其估值比率明显更高。控制盈利能力也显著提高了价值策略的绩效,尤其是在最大、流动性最强的股票中。这些结果很难与价值溢价的流行解释相符,因为盈利公司不太容易陷入困境,现金流持续时间更长,并且经营杠杆水平更低。控制总盈利能力可以解释大多数与收益相关的异常现象,以及各种看似无关的盈利交易策略。

IV. 回测表现

年化回报7.7%
波动率9.82%
β值-0.123
夏普比率0.38
索提诺比率0.807
最大回撤N/A
胜率57%

V. 完整的 Python 代码

from AlgorithmImports import *
from typing import Dict, List
from numpy import isnan
class ProfitabilityCombinedWithValue(QCAlgorithm):
    def Initialize(self) -> None:
        self.SetStartDate(2001, 1, 1)
        self.SetCash(100000)            
        market:Symbol = self.AddEquity("SPY", Resolution.Daily).Symbol
        
        self.fundamental_count:int = 3000
        self.fundamental_sorting_key = lambda x: x.MarketCap
        
        self.traded_count:int = 150
        self.leverage:int = 3
        self.min_share_price:int = 5
        
        self.long:List[Symbol] = []
        self.short:List[Symbol] = []
        
        self.rebalance_month:int = 6
        self.selection_flag:bool = True
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.FundamentalSelectionFunction)
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
        self.settings.daily_precise_end_time = False
        self.Schedule.On(self.DateRules.MonthEnd(market), self.TimeRules.BeforeMarketClose(market), self.Rebalance)
    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.Price > self.min_share_price and \
            x.AssetClassification.MorningstarSectorCode != MorningstarSectorCode.FinancialServices and not \
            # isnan(x.EarningReports.BasicAverageShares.ThreeMonths) and x.EarningReports.BasicAverageShares.ThreeMonths != 0 and not \
            isnan(x.FinancialStatements.IncomeStatement.GrossProfit.ThreeMonths) and x.FinancialStatements.IncomeStatement.GrossProfit.ThreeMonths != 0 and not \
            isnan(x.FinancialStatements.BalanceSheet.TotalAssets.ThreeMonths) and x.FinancialStatements.BalanceSheet.TotalAssets.ThreeMonths != 0 and not \
            isnan(x.ValuationRatios.PBRatio) and x.ValuationRatios.PBRatio != 0
        ]
        if len(selected) > self.fundamental_count:
            selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
        # sorted stocks in the top market-cap list by BM
        top_bm:List[Fundamental] = sorted(selected, key = lambda x: 1 / x.ValuationRatios.PBRatio)
        
        # sorted stocks in the top market-cap list by profits-to-assets
        top_pa:List[Fundamental] = sorted(selected, key = lambda x: x.FinancialStatements.IncomeStatement.GrossProfit.ThreeMonths / x.FinancialStatements.BalanceSheet.TotalAssets.ThreeMonths)
        a:int = 0
        b:int = 0 
        for i in top_bm:
            a += 1
            i.bmrank = a 
        for i in top_pa:
            b += 1
            i.parank = b
        if len(selected) >= self.traded_count*2:
            sorted_by_pv:List[Fundamental] = sorted(selected, key = lambda x: x.bmrank + x.parank, reverse = True)
            
            self.long = [x.Symbol for x in sorted_by_pv[:self.traded_count]]
            self.short = [x.Symbol for x in sorted_by_pv[-self.traded_count:]]
        
        return self.long + self.short
        
    def Rebalance(self) -> None:
        if self.Time.month == self.rebalance_month:
            self.selection_flag = True
    def OnData(self, data: Slice) -> None:
        if not self.selection_flag:
            return
        self.selection_flag = False
        # Trade execution
        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)
        
        for i, portfolio in enumerate([self.long, self.short]):
            for symbol in portfolio:
                if symbol in data and data[symbol]:
                    self.SetHoldings(symbol, ((-1) ** i) / len(portfolio))
        self.long.clear()
        self.short.clear()
# 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 的更多信息

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

继续阅读