The strategy trades 25 commodity futures using signal spreads, taking long/short year spreads based on moving averages, equally weighting positions, rolling contracts near expiry, and avoiding leverage for risk management.

I. 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.

II. 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.

III. SOURCE PAPER

Trend Following Strategies in Commodity Markets and the Impact of Financialization [Click to Open PDF]

Andreas Neuhierl, Northwestern University – Kellogg School of Management; Andrew Thompson, Northwestern University – Department of Economics

<Abstract>

This paper studies the returns to a simple trend following strategy in commodity markets and their potential drivers. We find that the strategy delivers low annualized returns in the period from 1990 to 2004 of 2.1% that show a significant increase from 2005 to 2013 to 6.5%, yielding Sharpe ratios of up to 1.8. This rise in returns coincides with the increase in participation in these markets by financial investors. Commodity markets entail particular features not shared by other assets classes, mainly physical delivery and the non-availability of infinitely lived contracts. For commodity funds that wish to maintain constant exposure, this means that they have to roll their positions to create an infinitely lived asset. This need to roll creates predictable demand for liquidity and predictable steepening and attening of the futures curve, which can be exploited by trend following strategies. We find that the strategies returns are positively correlated to the rebalancing demand of funds thereby providing novel evidence of limits of arbitrage in these markets.

IV. BACKTEST PERFORMANCE

Annualised Return3.73%
Volatility4.09%
Beta0.004
Sharpe Ratio0.91
Sortino RatioN/A
Maximum DrawdownN/A
Win Rate49%

V. 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"))

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading