When to Own Stocks and When to Own Gold
Log in to collectAcademic paper
Strategy in a nutshell
The strategy allocates between the S&P 500 and gold using the Secular Market Indicator (SMI), defined as the Shiller CAPE ratio divided by the spot price of gold. When SMI exceeds +1, gold is overweighted; when it drops below -1, equities are overweighted. Positions are maintained until the SMI signals the next shift, providing a dynamic allocation based on market conditions.
Economic rationale
The SMI improves on the CAPE ratio by accounting for long-term inflation-hedging and uncertainty through gold. It captures relative economic strength versus weakness, allowing investors to exploit shifts between financial and real assets. This method addresses CAPE’s timing and homogeneity issues, offering a more effective tool for secular market allocation.
Backtest performance
Annualised return10%
Volatility14%
Beta0.186
Sharpe ratio0.43
Sortino ratio0.178
Maximum drawdown-28%
Win rate56%
Full Python code
import numpy as np
from AlgorithmImports import *
from scipy import stats
from typing import List, Dict
import data_tools
class WhentoOwnStocksandWhentoOwnGold(QCAlgorithm):
def Initialize(self) -> None:
self.SetStartDate(1996, 1, 1)
self.SetCash(100000)
self.warmpup_period: int = 100*12*21
data: Securities = self.AddEquity('SPY', Resolution.Daily)
self.spy_symbol: Symbol = data.Symbol
self.SetWarmUp(timedelta(self.warmpup_period), Resolution.Daily) # Load data from 100 years ago.
self.smi: List[float] = [] # SMI indicator historical values.
self.cape_symbol: Symbol = self.AddData(data_tools.QuantpediaMonthlyData, 'SHILLER_PE_RATIO_MONTH', Resolution.Daily).Symbol
data: Security = self.AddData(data_tools.QuantpediaFutures, 'CME_GC1', Resolution.Daily)
data.SetFeeModel(data_tools.CustomFeeModel())
self.gold_symbol: Symbol = data.Symbol
stockPlot: Chart = Chart('SMI')
stockPlot.AddSeries(Series('SMI', SeriesType.Line, 0))
# self.last_month = 0
self.rebalance_flag: bool = False
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.Schedule.On(self.DateRules.MonthStart(self.spy_symbol), self.TimeRules.AfterMarketOpen(self.spy_symbol), self.Rebalance)
self.settings.daily_precise_end_time = False
def OnData(self, data: Slice) -> None:
custom_data_last_update_date: Dict[Symbol, datetime.date] = data_tools.LastDateHandler.get_last_update_date()
if (self.Securities[self.cape_symbol].GetLastData() and self.Time.date() > custom_data_last_update_date[self.cape_symbol]) or \
(self.Securities[self.gold_symbol].GetLastData() and self.Time.date() > custom_data_last_update_date[self.gold_symbol]):
self.Liquidate()
return
# Cape value comes at the start of the month
if self.cape_symbol in data and data[self.cape_symbol] and self.gold_symbol in data and data[self.gold_symbol]:
gold_price: float = data[self.gold_symbol].Value
cape: float = data[self.cape_symbol].Value
if cape == 0:
return
if gold_price == 0:
gold_price = 19.25
smi: float = cape / np.log(gold_price)
self.Plot('SMI', 'SMI', smi)
self.smi.append(smi)
if not self.rebalance_flag:
return
# if self.spy_symbol in data and data[self.spy_symbol] and self.gold_symbol in data and data[self.gold_symbol]:
self.rebalance_flag = False
if len(self.smi) != 0:
data_points = [x for x in range(0, len(self.smi))]
slope, intercept, r_value, p_value, std_err = stats.linregress(data_points, self.smi)
# Linear regression - X = independent, Y = dependent
# y = alpha + beta.x
alpha = intercept
beta = slope
x = data_points[-1]
y = alpha + (beta*x)
smi = self.smi[-1]
if self.spy_symbol in data and data[self.spy_symbol] and self.gold_symbol in data and data[self.gold_symbol]:
if smi > y + 1*std_err:
self.Liquidate(self.spy_symbol)
self.SetHoldings(self.gold_symbol, 1)
elif smi < y - 1*std_err:
self.Liquidate(self.gold_symbol)
self.SetHoldings(self.spy_symbol, 1)
def Rebalance(self) -> None:
self.rebalance_flag = True