
“该策略按盈利能力和价值对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"))