Quant BuffetRelax, Not Over Thinking

Market Timing Using Lumber/Gold Ratio

Log in to collect

Academic paper

Strategy in a nutshell

This strategy shifts between small-cap equities and treasury bonds based on the relative 13-week performance of Lumber versus Gold. Weekly evaluations adjust the portfolio only when leadership changes, allowing dynamic allocation between aggressive and defensive positions.

Economic rationale

Lumber reflects cyclical economic growth tied to housing and construction, while Gold signals risk aversion. Comparing these assets provides a framework to gauge economic conditions and adjust portfolio risk according to shifts in growth expectations and investor sentiment.

Backtest performance

Annualised return13.9%
Volatility11.8%
Beta0.042
Sharpe ratio0.84
Sortino ratio-0.15
Maximum drawdown-20.8%
Win rate61%

Full Python code

import numpy as np
from AlgorithmImports import *
class LumberGoldRatio(QCAlgorithm):
def Initialize(self):
 self.SetStartDate(2000, 1, 1)
 self.SetCash(100000)
 self.etfs = ['IWM', 'IEF']
 self.symbols = ['CME_LB1', 'CME_GC1']
 self.data = {}
 ret_period = 13 * 5
 self.SetWarmUp(ret_period)
 
 for symbol in self.etfs:
     self.AddEquity(symbol, Resolution.Daily, leverage=4)
 
 for symbol in self.symbols:
     data = self.AddData(QuantpediaFutures, symbol, Resolution.Daily)
     data.SetLeverage(5)
     self.data[symbol] = SymbolData(ret_period)
 
 self.rebalance_flag: bool = False
 self.Schedule.On(self.DateRules.Every(DayOfWeek.Monday), self.TimeRules.AfterMarketOpen(self.etfs[0]), self.Rebalance)
def OnData(self, data):
 for symbol in self.symbols:
     if self.securities[symbol].get_last_data() and self.time.date() > QuantpediaFutures.get_last_update_date()[symbol]:
         self.liquidate()
         return
     if symbol in data and data[symbol]:
         self.data[symbol].update(data[symbol].Value)
 if not self.rebalance_flag:
     return
 self.rebalance_flag = False
 lumber_data = self.data[self.symbols[0]]
 gold_data = self.data[self.symbols[1]]
 if all([data.contains_key(symbol) and data[symbol] for symbol in self.etfs]):
     if lumber_data.is_ready() and gold_data.is_ready():
         if lumber_data.performance() > gold_data.performance():
             if self.Portfolio['IEF'].Invested:
                 self.Liquidate('IEF')
             self.SetHoldings('IWM', 1)
         else:
             if self.Portfolio['IWM'].Invested:
                 self.Liquidate('IWM')
             self.SetHoldings('IEF', 1)
def Rebalance(self):
 self.rebalance_flag = True
class SymbolData:
def __init__(self, ret_lookback):
 self.history = RollingWindow[float](ret_lookback)
 self.price = 0.0
def is_ready(self):
 return self.history.IsReady
 
def update(self, value):
 self.price = float(value)
 self.history.Add(float(value))
def performance(self):
 prices = np.array([x for x in self.history])
 return (prices[-1]-prices[0])/prices[0]
# Quantpedia data.
# NOTE: IMPORTANT: Data order must be ascending (datewise)
class QuantpediaFutures(PythonData):
_last_update_date:Dict[Symbol, datetime.date] = {}
@staticmethod
def get_last_update_date() -> Dict[Symbol, datetime.date]:
return QuantpediaFutures._last_update_date
def GetSource(self, config, date, isLiveMode):
 return SubscriptionDataSource("data.quantpedia.com/backtesting_data/futures/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
def Reader(self, config, line, date, isLiveMode):
 data = QuantpediaFutures()
 data.Symbol = config.Symbol
 
 if not line[0].isdigit(): return None
 split = line.split(';')
 
 data.Time = datetime.strptime(split[0], "%d.%m.%Y") + timedelta(days=1)
 data['back_adjusted'] = float(split[1])
 data['spliced'] = float(split[2])
 data.Value = float(split[1])
 if config.Symbol.Value not in QuantpediaFutures._last_update_date:
     QuantpediaFutures._last_update_date[config.Symbol.Value] = datetime(1,1,1).date()
 if data.Time.date() > QuantpediaFutures._last_update_date[config.Symbol.Value]:
     QuantpediaFutures._last_update_date[config.Symbol.Value] = data.Time.date()
 return data