“该策略投资于10个行业ETF,通过相对CAPE和动量选择四个被低估的行业,每月等权重并重新平衡投资组合以优化回报。”

I. 策略概要

该策略投资于10个行业ETF,根据相对CAPE比率选择五个被低估的行业。排除12个月动量最低的行业,剩下四个行业。这些行业等权重,投资组合每月重新平衡。

II. 策略合理性

CAPE通过考虑股价(尽管盈利下降)有效评估股票的低估或高估,从而能够识别具有潜在正回报的低估股票。与其他指标相比,CAPE始终表现出卓越的经济和统计显著性。它仍然是相对估值和资产配置最可靠的工具。CAPE适用于国家、行业和个股轮动策略。对于资产配置,CAPE的当前值有助于预测股票风险溢价,使其成为评估股票投资和指导配置决策的宝贵衡量标准。其可靠性和适应性增强了其在各种投资场景中的实用性。

III. 来源论文

The Many Colours of CAPE [点击查看论文]

<摘要>

坎贝尔和席勒(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

发表评论

了解 Quant Buffet 的更多信息

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

继续阅读