
The strategy invests in 10 sector ETFs, selecting four undervalued sectors via relative CAPE and momentum, equally weighting and rebalancing the portfolio monthly for optimized returns.
ASSET CLASS: ETFs, funds | REGION: Global | FREQUENCY:
Monthly | MARKET: equities | KEYWORD: CAPE
I. STRATEGY IN A NUTSHELL
Invests in 10 sector ETFs, selecting the five most undervalued sectors by relative CAPE and excluding the sector with lowest 12-month momentum. Remaining sectors are equally weighted and rebalanced monthly.
II. ECONOMIC RATIONALE
CAPE identifies undervalued sectors by adjusting for earnings fluctuations, guiding allocation to sectors with potential positive returns. Its robustness and predictive power make it a reliable tool for rotation and asset allocation strategies.
III. SOURCE PAPER
The Many Colours of CAPE [Click to Open PDF]
Farouk Jivraj, Fidelity Investments, Inc. – Fidelity Management & Research, Imperial College Business School; Robert J. Shiller, Yale University – Cowles Foundation, National Bureau of Economic Research (NBER), Yale University – International Center for Finance
<Abstract>
Campbell & Shiller’s [1988] Cyclically-Adjusted Price to Earnings ratio (CAPE) has both its advocates and critics. Currently, the debate is on the validity of the high CAPE ratio for US stock markets in forecasting lower future returns, with CAPE currently at 31.21. We investigate the efficacy and validity of CAPE from several different perspectives. First, we run multiple-horizon predictability regressions for CAPE versus its peers and find that CAPE consistently displays economic and statistical significance far better than any of its peers. Second, we explore alternative constructions of CAPE based on other proxies for earnings motivated by the work of findings by Siegel [2016] using NIPA profits. We find that original CAPE is still best when comprehensively and fairly reviewing the other proxies, even for NIPA profits. Third, we assess how to practically use CAPE in both an asset allocation and relative valuation setting. We demonstrate a novel use of CAPE for asset allocation programmes as well as discuss relative valuation exercises for country, sector and single stock rotation.


IV. BACKTEST PERFORMANCE
| Annualised Return | 14.23% |
| Volatility | 17.98% |
| Beta | 0.251 |
| Sharpe Ratio | 0.79 |
| Sortino Ratio | N/A |
| Maximum Drawdown | -43.54% |
| Win Rate | 86% |
V. FULL PYTHON CODE
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
VI. Backtest Performance