
“该策略投资于10个行业ETF,通过相对CAPE和动量选择四个被低估的行业,每月等权重并重新平衡投资组合以优化回报。”
资产类别: ETF、基金 | 地区: 全球 | 周期: 每月 | 市场: 股票 | 关键词: CAPE
I. 策略概要
该策略投资于10个行业ETF,根据相对CAPE比率选择五个被低估的行业。排除12个月动量最低的行业,剩下四个行业。这些行业等权重,投资组合每月重新平衡。
II. 策略合理性
CAPE通过考虑股价(尽管盈利下降)有效评估股票的低估或高估,从而能够识别具有潜在正回报的低估股票。与其他指标相比,CAPE始终表现出卓越的经济和统计显著性。它仍然是相对估值和资产配置最可靠的工具。CAPE适用于国家、行业和个股轮动策略。对于资产配置,CAPE的当前值有助于预测股票风险溢价,使其成为评估股票投资和指导配置决策的宝贵衡量标准。其可靠性和适应性增强了其在各种投资场景中的实用性。
III. 来源论文
The Many Colours of CAPE [点击查看论文]
- 法鲁克·吉夫拉吉(Farouk Jivraj)、罗伯特·J·席勒(Robert J. Shiller),富达投资公司(Fidelity Investments, Inc.)—富达管理与研究公司;帝国理工商学院,耶鲁大学考尔斯基金会;美国国家经济研究局(NBER);耶鲁大学国际金融中心
<摘要>
坎贝尔和席勒(1988)的周期调整市盈率(CAPE)既有支持者也有批评者。目前,争论的焦点是美国股市高CAPE比率(目前为31.21)预测未来较低回报的有效性。我们从几个不同的角度调查了CAPE的有效性和合理性。首先,我们对CAPE及其同类指标进行多期限可预测性回归,发现CAPE始终显示出远优于任何同类指标的经济和统计显著性。其次,我们根据西格尔(2016)使用NIPA利润的研究发现,探索了基于其他盈利代理的CAPE替代构建方法。我们发现,即使对于NIPA利润,在全面公平地审查其他代理时,原始CAPE仍然是最好的。第三,我们评估了如何在资产配置和相对估值环境中实际使用CAPE。我们展示了CAPE在资产配置程序中的新颖用途,并讨论了国家、行业和个股轮动的相对估值实践。


IV. 回测表现
| 年化回报 | 14.23% |
| 波动率 | 17.98% |
| β值 | 0.251 |
| 夏普比率 | 0.79 |
| 索提诺比率 | N/A |
| 最大回撤 | -43.54% |
| 胜率 | 86% |
V. 完整的 Python 代码
from AlgorithmImports import *
#endregion
class CAPESectorPickingStrategy(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.symbols = [
"XLC", # Community Services
"XLRE", # Real Estate
"XLV", # Health Care
"XLI", # Industrial
"XLY", # Consumer Cyclical
"XLP", # Consumer Defensive
"XLB", # Basic Materials
"XLK", # Technology
"XLU", # Utilities
"XLE", # Energy
"XLF", # Financial Services
]
# Daily prices.
self.data = {}
self.period = 21 * 12
self.max_missing_days = 5
self.min_symbol_count = 5
for symbol in self.symbols:
data = self.AddEquity(symbol, Resolution.Daily)
data.SetFeeModel(CustomFeeModel())
data.SetLeverage(5)
self.data[symbol] = SymbolData(self.period)
self.AddData(QuantpediaSectorCAPE, symbol, Resolution.Daily)
self.recent_month = -1
def OnData(self, data):
for symbol in self.symbols:
if symbol in data and data[symbol]:
self.data[symbol].update(data[symbol].Value)
if self.recent_month == self.Time.month:
return
self.recent_month = self.Time.month
# CAPE data are available since 2010
cape_values = {}
for symbol in self.symbols:
cape_data = self.Securities[symbol + '.QuantpediaSectorCAPE'].GetLastData()
if cape_data and (self.Time.date() - cape_data.Time.date()).days <= self.max_missing_days:
cape_values[symbol] = cape_data['Shiller']
# performance and cape ratio tuple.
performance_cape = { x : (self.data[x].performance(), cape_values[x]) for x in self.symbols if self.data[x].is_ready() and x in cape_values }
long = []
if len(performance_cape) >= self.min_symbol_count:
# sorted by cape, selecting 5 lowest
five_lowest = [(x[0], x[1][0]) for x in sorted(performance_cape.items(), key=lambda item: item[1][1])][:self.min_symbol_count]
# choosing five with highest 12 month momentum
long = [x[0] for x in sorted(five_lowest, key=lambda item: item[1], reverse=True)][:(self.min_symbol_count-1)]
# Trade execution
invested = [x.Key.Value for x in self.Portfolio if x.Value.Invested]
for symbol in invested:
if symbol not in long:
self.Liquidate(symbol)
length = len(long)
for symbol in long:
if symbol in data and data[symbol]:
self.SetHoldings(symbol, 1 / length)
class SymbolData():
def __init__(self, period):
self.Price = RollingWindow[float](period)
def update(self, value):
self.Price.Add(value)
def is_ready(self):
return self.Price.IsReady
def performance(self):
prices = [x for x in self.Price]
return prices[0] / prices[-1] - 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"))
# Quantpedia Sectors
# NOTE: IMPORTANT: Data order must be ascending (datewise)
class QuantpediaSectorCAPE(PythonData):
def GetSource(self, config, date, isLiveMode):
return SubscriptionDataSource("data.quantpedia.com/backtesting_data/economic/sector_cape/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
def Reader(self, config, line, date, isLiveMode):
data = QuantpediaSectorCAPE()
data.Symbol = config.Symbol
if not line[0].isdigit(): return None
split = line.split(';')
# Own formatting based on CSV file structure
data.Time = datetime.strptime(split[0], "%Y-%m-%d") + timedelta(days=1)
if split[1] != '' and split[2] != '':
data['Shiller'] = float(split[1])
data['Regular'] = float(split[2])
data.Value = float(split[1])
else:
return None
return data