Quant BuffetRelax, Not Over Thinking

Timing Commodities and S&P500 with COT Report

Log in to collect

Academic paper

How to Time the Commodity Market

AuthorsDevraj Basu; Roel C. A. Oomen; Alexander Stremme

Institute
  • SKEMA Business School
  • ?SKEMA Business School - Lille Campus
  • London School of Economics and Political Science
  • Deutsche Bank (United Kingdom)
  • ?Deutsche Bank AG (London)
  • ?London School of Economics & Political Science (LSE) - Department of Statistics
  • University of Warwick
  • ?University of Warwick - Finance Group

Strategy in a nutshell

Universe: S&P 500, copper, oil, risk-free asset. Use COT-based hedging pressures as predictors. Compute expected returns and volatilities, optimize portfolio weights to maximize Sharpe ratio, and rebalance weekly.

Economic rationale

COT reports reveal hedger vs. speculator positions. Hedging pressure signals market risk transfer and sentiment, providing predictive insights for return and volatility-based portfolio optimization.

Backtest performance

Annualised return16.8%
Volatility10.3%
Beta0.263
Sharpe ratio1.24
Sortino ratio0.042
Win rate54%

Full Python code

from collections import deque
from AlgorithmImports import *
import numpy as np
import data_tools
import pandas as pd
from scipy.optimize import minimize
class TimingCommoditiesandSP500withCOTReport(QCAlgorithm):

def Initialize(self):
self.SetStartDate(2005, 1, 1)
self.SetCash(100000)
   
# Future symbol with COT symbol.
self.symbols = {
    'CME_ES1' : 'QEP',
    'CME_HG1' : 'QHG',
    'CME_CL1' : 'QCL'
}

# Daily price data.
self.data = {}
self.period = 60
for symbol, cot_symbol in self.symbols.items():
    # Futures data.
    data = self.AddData(data_tools.QuantpediaFutures, symbol, Resolution.Daily)
    data.SetFeeModel(data_tools.CustomFeeModel())
    data.SetLeverage(10)
    
    self.symbol = data.Symbol
    self.data[symbol] = SymbolData(symbol,self.period)

    # COT data.
    self.AddData(data_tools.CommitmentsOfTraders, cot_symbol, Resolution.Daily)
# Cash asset.
self.cash = self.AddEquity('BIL', Resolution.Daily).Symbol
self.data[self.cash.Value] = SymbolData(self.cash.Value, self.period)

# Regression data.
self.period = 12 * 4    # 1 year worth of weekly data.
self.regression_data = deque(maxlen = self.period)

def OnData(self, data):
# Store daily price data.
for symbol in self.data:
    symbol_obj = self.Symbol(symbol)
    if symbol_obj in data and data[symbol_obj]:
        price = data[symbol_obj].Value
        self.data[symbol].update(price)
    
copper_hedging_pressure = 0
oil_hedging_pressure = 0

market_commercial_hedging_pressure = 0
market_nonreportable_hedging_pressure = 0

# Copper and oil COT data.
for cot_symbol in ['QHG', 'QCL']:
    # Store COT data every wednesday.
    if cot_symbol in data and data[cot_symbol]:
        cot_data = self.Securities[cot_symbol].GetLastData()
        if cot_data:
            speculator_long_count = cot_data.GetProperty("LARGE_SPECULATOR_LONG")
            speculator_short_count = cot_data.GetProperty("LARGE_SPECULATOR_SHORT")
            if speculator_long_count != 0 and speculator_short_count != 0:
                hedging_pressure = speculator_long_count / (speculator_long_count + speculator_short_count)
                
                if cot_symbol == 'QHG':
                    copper_hedging_pressure = hedging_pressure
                elif cot_symbol == 'QCL':
                    oil_hedging_pressure = hedging_pressure

# Market COT data.
cot_symbol = 'QEP'
if cot_symbol in data and data[cot_symbol]:
    cot_data = self.Securities[cot_symbol].GetLastData()
    if cot_data:
        # Commercial hedging pressure.
        commercial_long_count = cot_data.GetProperty("COMMERCIAL_HEDGER_LONG")
        commercial_short_count = cot_data.GetProperty("COMMERCIAL_HEDGER_SHORT")
        if commercial_long_count != 0 and commercial_short_count != 0:
            market_commercial_hedging_pressure = commercial_long_count / (commercial_long_count + commercial_short_count)
        # Non-reportable hedging pressure.
        trader_long_count = cot_data.GetProperty("SMALL_TRADER_LONG")
        trader_short_count = cot_data.GetProperty("SMALL_TRADER_SHORT")                            
        if trader_long_count != 0 and trader_short_count != 0:
            market_nonreportable_hedging_pressure = trader_long_count / (trader_long_count + trader_short_count)
            
returns = { x: self.data[x].performance() for x in self.data if self.data[x].is_ready() }
# Store regression data.
if len(returns) == len(self.data) and copper_hedging_pressure != 0 and oil_hedging_pressure != 0 and market_commercial_hedging_pressure != 0 and market_nonreportable_hedging_pressure != 0:
    asset_returns = [ret for symbol, ret in returns.items()]
    self.regression_data.append((asset_returns[0], asset_returns[1], asset_returns[2], asset_returns[3], copper_hedging_pressure, oil_hedging_pressure, market_commercial_hedging_pressure, market_nonreportable_hedging_pressure))

# Regression data is ready.
if len(self.regression_data) == self.regression_data.maxlen:
    
    # check futures and cot data arrival
    for symbol, cot_symbol in self.symbols.items():
        if any([self.securities[x].get_last_data() and self.time.date() > data_tools.LastDateHandler.get_last_update_date()[x] for x in [symbol, cot_symbol]]):
            self.liquidate()
            return

    returns = {}
    for index in range(4):
        symbol = [x for x in self.data.keys()][index]
        # Linear regression calc.
        asset_returns = [x[index] for x in self.regression_data]
        
        copper_hedging_pressures = [x[4] for x in self.regression_data]
        oil_hedging_pressures = [x[5] for x in self.regression_data]
        market_commercial_hedging_pressures = [x[6] for x in self.regression_data]
        market_nonreportable_hedging_pressures = [x[7] for x in self.regression_data]
        
        x = [copper_hedging_pressures[:-1], oil_hedging_pressures[:-1], market_commercial_hedging_pressures[:-1], market_nonreportable_hedging_pressures[:-1]]
        regression_model = data_tools.MultipleLinearRegression(x, asset_returns[1:])
        
        alpha = regression_model.params[0]
        pred_x = [copper_hedging_pressures[-1], oil_hedging_pressures[-1], market_commercial_hedging_pressures[-1], market_nonreportable_hedging_pressures[-1]]
        betas = np.array(regression_model.params[1:])
        return_predicted = alpha + sum(np.multiply(betas, pred_x))
        
        returns[symbol] = asset_returns + [return_predicted]
    
    if len(returns) > 0:
        returns = pd.DataFrame(returns, columns=returns.keys()).dropna()
        
        # The optimization method processes the data frame.
        opt, weights = self.optimization_method(returns)
        
        for symbol in returns.keys():
            w = weights[symbol]
            if w >= 0.001:
                self.SetHoldings(symbol, w)
    else:
        self.Liquidate()
def optimization_method(self, returns):
# '''Minimum variance optimization method'''
# # Objective function
# fun = lambda weights: np.dot(weights.T, np.dot(returns.cov(), weights))
'''Maximum Sharpe Ratio optimization method'''
# Objective function
fun = lambda weights: -self.sharpe_ratio(returns, weights)
# Constraint #1: The weights can be negative, which means investors can short a security.
constraints = [{'type': 'eq', 'fun': lambda w: np.sum(w) - 1}]
# Constraint #2: Portfolio targets a given return
# constraints.append({'type': 'eq', 'fun': lambda weights: np.dot(np.matrix(returns.mean()), np.matrix(weights).T).item() - self.target_return})
size = returns.columns.size
x0 = np.array(size * [1. / size])
# bounds = tuple((self.minimum_weight, self.maximum_weight) for x in range(size))
bounds = tuple((0, 1) for x in range(size))
opt = minimize(fun,                         # Objective function
               x0,                          # Initial guess
               method='SLSQP',              # Optimization method:  Sequential Least SQuares Programming
               bounds = bounds,             # Bounds for variables 
               constraints = constraints)   # Constraints definition
return opt, pd.Series(opt['x'], index = returns.columns)

def sharpe_ratio(self, returns, weights):
annual_return = np.dot(np.matrix(returns.mean()), np.matrix(weights).T).item()
annual_volatility = np.sqrt(np.dot(weights.T, np.dot(returns.cov(), weights)))
return annual_return/annual_volatility        
class QuandlVix(PythonQuandl):
def __init__(self):
self.ValueColumnName = "VIX Close"

class SymbolData():
def __init__(self, symbol, period):
self.Symbol = symbol
self.Price = RollingWindow[float](period)

def is_ready(self):
return self.Price.IsReady

def update(self, close):
self.Price.Add(close)

def performance(self):
closes = [x for x in self.Price]
return (closes[0] / closes[-1] - 1)