Quant BuffetRelax, Not Over Thinking

Trend Following in Commodity Calendar Spreads

Log in to collect

Academic paper

Strategy in a nutshell

This strategy trades 25 commodity futures using the 50-day moving average of the “signal spread” (nearby vs. next-out contract) to guide positions in the “trading spread” (nearby vs. one-year-out). Positions are equally weighted, unleveraged, and contracts are rolled as front-month contracts near expiration, systematically exploiting spread dynamics.

Economic rationale

Periodic rebalancing by mutual funds creates predictable demand shocks in calendar spreads, increasing autocorrelation in commodity prices. Trend-following strategies can leverage this behavior to identify trading opportunities and capture returns from systematic spread patterns.

Backtest performance

Annualised return3.73%
Volatility4.09%
Beta0.004
Sharpe ratio0.91
Win rate49%

Full Python code

import numpy as np
class TrendFollowinginCommodityCalendarSpreads(QCAlgorithm):

def Initialize(self):
self.SetStartDate(2010, 1, 1)
self.SetCash(100000)
   
# quandl contract symbol and contract number approximately 12 months away
self.futures_last_contract = {
                "CHRIS/CME_S" :  "8",   # Soybean Futures, Continuous Contract
                "CHRIS/CME_W"  : "6",   # Wheat Futures, Continuous Contract
                "CHRIS/CME_SM" : "9",   # Soybean Meal Futures, Continuous Contract
                "CHRIS/CME_BO" : "9",   # Soybean Oil Futures, Continuous Contract
                "CHRIS/CME_C" :  "6",   # Corn Futures, Continuous Contract
                # "CHRIS/CME_O" :  "6",   # Oats Futures, Continuous Contract
                "CHRIS/CME_LC" : "7",   # Live Cattle Futures, Continuous Contract
                "CHRIS/CME_FC" : "7",   # Feeder Cattle Futures, Continuous Contract
                "CHRIS/CME_LN" : "9",   # Lean Hog Futures, Continuous Contract
                "CHRIS/CME_GC" : "9",   # Gold Futures, Continuous Contract
                "CHRIS/CME_SI" : "9",   # Silver Futures, Continuous Contract
                # "CHRIS/CME_PL" : "7",   # Platinum Futures, Continuous Contract
                "CHRIS/CME_CL" : "12",  # Crude Oil Futures, Continuous Contract
                "CHRIS/CME_HG" : "13",  # Copper Futures, Continuous Contract
                # "CHRIS/CME_LB" : "7",   # Random Length Lumber Futures, Continuous Contract
                # "CHRIS/CME_PA" : "7",   # Palladium Futures, Continuous Contract 
                # "CHRIS/CME_RR" : "7",   # Rough Rice Futures, Continuous Contract
                # "CHRIS/CME_DA" : "13",  # Class III Milk Futures
                
                "CHRIS/ICE_CC" : "6",   # Cocoa Futures, Continuous Contract 
                "CHRIS/ICE_CT" : "6",   # Cotton No. 2 Futures, Continuous Contract
                "CHRIS/ICE_KC" : "6",   # Coffee C Futures, Continuous Contract
                "CHRIS/ICE_O" :  "3",   # Heating Oil Futures, Continuous Contract
                "CHRIS/ICE_OJ" : "7",   # Orange Juice Futures, Continuous Contract
                "CHRIS/ICE_SB" : "5"    # Sugar No. 11 Futures, Continuous Contract
                }
self.period:int = 50
self.SetWarmUp(self.period, Resolution.Daily)

# spread data - MA is calculated out of signal spread
self.signal_spread:dict = {}
        
for c_sym, last_c_num in self.futures_last_contract.items():
    # add #1 and #2 and one approx. year away contracts
    for c_num in [1, 2, last_c_num]:
        data = self.AddData(QuandlFutures, c_sym+str(c_num), Resolution.Daily)
        data.SetFeeModel(CustomFeeModel(self))
        data.SetLeverage(5)
    self.signal_spread[c_sym] = RollingWindow[float](self.period)

def OnData(self, data):
for c_sym, last_c_num in self.futures_last_contract.items():
    
    front_contract_sym:str = c_sym + '1'
    further_contract_sym:str = c_sym + '2'
    contract_1y_away_sym:str = c_sym + last_c_num
    
    # calculate #1 and #2 spread
    if front_contract_sym in data and further_contract_sym in data and contract_1y_away_sym in data and \
        data[front_contract_sym] and data[further_contract_sym] and data[contract_1y_away_sym]:
        front_price:float = data[front_contract_sym].Value
        further_price:float = data[further_contract_sym].Value
        
        if front_price > 0 and further_price > 0:
            current_spread:float = front_price - further_price
            self.signal_spread[c_sym].Add(current_spread)
            if self.signal_spread[c_sym].IsReady:
                spread_ma:float = np.mean([x for x in self.signal_spread[c_sym]])
                weight:float = 1. / len(self.futures_last_contract)
                
                if current_spread > spread_ma:
                    # long trading spread
                    self.SetHoldings(contract_1y_away_sym, weight)
                    self.SetHoldings(front_contract_sym, -weight)
                else:
                    # short trading spread
                    self.SetHoldings(front_contract_sym, weight)
                    self.SetHoldings(contract_1y_away_sym, -weight)
        else:
            self.Debug(f"Price bellow 0: {front_contract_sym}:{front_price}, {further_contract_sym}:{further_price}")
    else:
        # check if quandl data is still comming in
        if not all(self.Securities[symbol].GetLastData() and (self.Time.date() - self.Securities[symbol].GetLastData().Time.date()).days <= 5 for symbol in [front_contract_sym, further_contract_sym, contract_1y_away_sym] ):
            # liquidate spread position once quandl data stopped comming in
            if self.Portfolio[front_contract_sym].Invested and self.Portfolio[further_contract_sym].Invested:
                self.Liquidate(front_contract_sym)
                self.Liquidate(contract_1y_away_sym)
# Quandl free data
class QuandlFutures(PythonQuandl):
def __init__(self):
self.ValueColumnName = "settle"
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))