Quant BuffetRelax, Not Over Thinking

Commodities Timing based on a Monetary Conditions

Log in to collect

Academic paper

Strategy in a nutshell

Adjust asset allocation between stocks, bonds, and commodities based on FED rate changes. Expansionary: mean-variance efficient stock/bond portfolio. Restrictive: add commodities and recalculate weights for optimal risk-return.

Economic rationale

Rising rates signal inflationary pressure; commodities hedge inflation while stocks and bonds underperform. Allocating to commodities during restrictive monetary policy exploits this inflation-protection effect.

Backtest performance

Annualised return20.46%
Volatility13.86%
Beta0.453
Sharpe ratio1.48
Win rate51%

Full Python code

from math import isnan
from AlgorithmImports import *
import pandas as pd
import numpy as np
from scipy.optimize import minimize
class CommoditiesTimingbasedonaMonetaryConditions(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2004, 1, 1)
self.SetCash(100000)

self.data = {}  # daily closes
period = 12 * 21
self.SetWarmUp(period)

self.symbols = ["SPY", "EFA", "IEF", "LQD"]
self.commodities = ["DBC"]
for symbol in self.symbols + self.commodities:
    self.AddEquity(symbol, Resolution.Daily)
    self.data[symbol] = RollingWindow[float](period)
# changes in FED policy from restrictive to expansive
dates_str = ["24.08.1999", "03.01.2001", "30.06.2004", "18.09.2007", "14.12.2016", "31.7.2019"]
self.dates = [datetime.strptime(x, "%d.%m.%Y").date() for x in dates_str] # datetime type

# import quandl federal rate data
self.target_rate = self.AddData(QuandlValue, 'FRED/DFEDTARU', Resolution.Daily).Symbol
self.restrictive_flag = True    # from start of the algorithm
self.last_target_rate = None
self.external_restrictive_flag = None
self.recent_month = -1
def OnData(self, data):
if self.target_rate in data and data[self.target_rate]:
    curr_target_rate = data[self.target_rate].Value
    
    restrictive_flag = None
    
    # switch to external data source, when self.dates ends
    if self.Time.date() > self.dates[-1]:
        if self.last_target_rate:
            if curr_target_rate > self.last_target_rate:
                restrictive_flag = True
            elif curr_target_rate < self.last_target_rate:
                restrictive_flag = False
    
    if restrictive_flag is not None:
        self.external_restrictive_flag = restrictive_flag
            
    self.last_target_rate = curr_target_rate

# store daily price data
for symbol in self.symbols + self.commodities:
    if symbol in data and data[symbol]:
        price = data[symbol].Value
        self.data[symbol].Add(price)

if self.recent_month == self.Time.month:
    return
self.recent_month = self.Time.month

self.Liquidate()
# if external data is present, trade out of it
if self.external_restrictive_flag is not None:
    restrictive_flag = self.external_restrictive_flag
    
    # stop backtest once quandl target rate data stops
    if self.Securities[self.target_rate].GetLastData() and (self.Time.date() - self.Securities[self.target_rate].GetLastData().Time.date()).days > 5:
        self.Liquidate()
        return
else:
    restrictive_flag = self.restrictive_flag
symbols = [x for x in self.symbols]

if restrictive_flag:
    symbols += self.commodities
# construct dataframe
data = {}
for symbol in symbols:
    if self.data[symbol].IsReady:
        data[symbol] = [x for x in self.data[symbol]][::-1]
        
if len(data) != 0:
    df_price = pd.DataFrame(data,columns=data.keys()) 
    daily_return = (df_price / df_price.shift(1) - 1).dropna()
    a = PortfolioOptimization(daily_return, 0, len(data))
    opt_weight = a.opt_portfolio()
    
    if isnan(sum(opt_weight)): return
    
    for i in range(len(data)):
        if opt_weight[i] >= 0.001:
            self.SetHoldings(df_price.columns[i], opt_weight[i])
else:
    if self.Portfolio.Invested:
        self.Liquidate()
    
class PortfolioOptimization(object):
def __init__(self, df_return, risk_free_rate, num_assets):
self.daily_return = df_return
self.risk_free_rate = risk_free_rate
self.n = num_assets # numbers of risk assets in portfolio
self.target_vol = 0.05
def annual_port_return(self, weights):
# calculate the annual return of portfolio
return np.sum(self.daily_return.mean() * weights) * 252
def annual_port_vol(self, weights):
# calculate the annual volatility of portfolio
return np.sqrt(np.dot(weights.T, np.dot(self.daily_return.cov() * 252, weights)))
def min_func(self, weights):
# method 1: maximize sharp ratio
return - self.annual_port_return(weights) / self.annual_port_vol(weights)

# method 2: maximize the return with target volatility
#return - self.annual_port_return(weights) / self.target_vol
def opt_portfolio(self):
# maximize the sharpe ratio to find the optimal weights
cons = ({'type': 'eq', 'fun': lambda x: np.sum(x) - 1})
bnds = tuple((0, 1) for x in range(2)) + tuple((0, 0.25) for x in range(self.n - 2))
opt = minimize(self.min_func,                               # object function
               np.array(self.n * [1. / self.n]),            # initial value
               method='SLSQP',                              # optimization method
               bounds=bnds,                                 # bounds for variables 
               constraints=cons)                            # constraint conditions
              
opt_weights = opt['x']
 
return opt_weights
# Quandl "value" data
class QuandlValue(PythonQuandl):
def __init__(self):
self.ValueColumnName = 'Value'