Quant BuffetRelax, Not Over Thinking

CAPE Sector Picking Strategy

Log in to collect

Academic paper

The Many Colours of CAPE

AuthorsFarouk Jivraj; Robert J. Shiller

Institute
  • Fidelity Investments (United States)
  • ?Fidelity Investments, Inc. - Fidelity Management & Research
  • ?Imperial College Business School
  • National Bureau of Economic Research
  • ?National Bureau of Economic Research (NBER)
  • ?Yale University - Cowles Foundation
  • ?Yale University - International Center for Finance

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.

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.

Backtest performance

Annualised return14.23%
Volatility17.98%
Beta0.251
Sharpe ratio0.79
Maximum drawdown-43.54%
Win rate86%

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