
“Form a commodity futures trading universe. Monthly, rank last year’s performance by quintiles. Long top momentum quintile, short bottom. Rebalance monthly.”
ASSET CLASS: CFDs, futures | REGION: Global | FREQUENCY: Monthly | MARKET: Commodity | KEYWORD: Momentum
STRATEGY IN A NUTSHELL
Develop a trading universe comprised of commodity futures. Evaluate and categorize the performance of each future over the past 12 months into quintiles. Adopt a long position in the quintile demonstrating the strongest momentum, while taking a short position in the quintile showing the weakest momentum. Ensure to adjust and realign the portfolio monthly based on the latest performance metrics, maintaining a strategy that capitalizes on momentum trends within the commodity futures market. This approach aims to leverage the cyclicality and trends inherent in commodity prices for potential gains.
ECONOMIC RATIONALE
Commodity momentum returns are linked to whether futures markets are in backwardation (prices decrease over time) or contango (prices increase over time), favoring the purchase of backwardated contracts and sale of contangoed ones. This suggests profitability in trading the most extreme backwardated and contangoed contracts, aligning with Keynes and Hicks’ theory of normal backwardation. However, these momentum profits aren’t seen as risk compensation. Additionally, commodity momentum returns show low correlation with traditional asset classes, indicating their potential value in diversified portfolios.
Switzer and Jiang highlight that momentum-based profits from commodity futures align with behavioral finance theories, where market inefficiencies arise from participants’ underreaction to information. These profits also depend on the market’s term structure and hedging pressure. The strategy’s success is further supported by factors like market liquidity, a focus on only 31 commodities, and low transaction costs.
SOURCE PAPER
Momentum in Commodity Futures Markets [Click to Open PDF]
- Joëlle Miffre, Audencia Business School
- Georgios Rallis, City University of London – Sir John Cass Business School
<Abstract>
The article tests for the presence of short-term continuation and long-term reversal in commodity futures prices. While contrarian strategies do not work, the article identifies 13 profitable momentum strategies that generate 9.38% average return a year. A closer analysis of the commodity futures that the momentum strategy recommends trading reveals that we buy backwardated contracts and sell contangoed contracts with high volatilities. The correlation between the momentum returns and the returns of traditional asset classes is also found to be low, making the commoditybased relative-strength portfolios excellent candidates for inclusion in well-diversified portfolios.

BACKTEST PERFORMANCE
| Annualised Return | 14.6% |
| Volatility | 25.6% |
| Beta | -0.104 |
| Sharpe Ratio | 0.047 |
| Sortino Rato | 0.053 |
| Maximum Drawdown | 76.7% |
| Win Rate | 57% |
FULL PYTHON CODE
from AlgorithmImports import *
class CommodityMomentumStrategy(QCAlgorithm):
'''
This class defines a commodity momentum trading strategy.
It initializes trading settings, selects commodities based on momentum,
and manages positions accordingly.
'''
def Initialize(self):
'''Initializes the trading algorithm settings, including start date, initial cash,
commodity tickers to trade, analysis period, and momentum indicators for each commodity.'''
self.SetStartDate(2000, 1, 1) # Set the start date for the algorithm
self.SetCash(100000) # Set the initial cash for the algorithm
# Define the commodity tickers that will be traded
self.commodity_tickers = [
"CME_S1", "CME_W1", "CME_SM1", "CME_BO1", "CME_C1", "CME_O1",
"CME_LC1", "CME_FC1", "CME_LN1", "CME_GC1", "CME_SI1", "CME_PL1",
"CME_CL1", "CME_HG1", "CME_LB1", "CME_NG1", "CME_PA1", "CME_RR1",
"CME_DA1", "ICE_RS1", "ICE_GO1", "CME_RB2", "CME_KW2", "ICE_WT1",
"ICE_CC1", "ICE_CT1", "ICE_KC1", "ICE_O1", "ICE_OJ1", "ICE_SB1",
]
self.analysis_period = 252 # Approx. 12 months of trading days
self.SetWarmUp(self.analysis_period, Resolution.Daily) # Set warm-up period for indicators
self.momentum_indicator = {} # Dictionary to store momentum indicators for each commodity
# Initialize momentum indicators for each commodity ticker
for ticker in self.commodity_tickers:
commodity = self.AddData(QuantpediaFutures, ticker, Resolution.Daily) # Add commodity data
commodity.SetFeeModel(StandardFeeModel()) # Set fee model for the commodity
commodity.SetLeverage(5) # Set leverage for the commodity
# Add a Rate of Change (ROC) indicator for the commodity based on the analysis period
self.momentum_indicator[commodity.Symbol] = self.ROC(ticker, self.analysis_period, Resolution.Daily)
self.previous_month = -1 # Variable to track the last processed month
def OnData(self, data: Slice):
'''Processes new data and manages portfolio positions based on momentum strategy.'''
if self.IsWarmingUp: # Check if the algorithm is still in the warm-up period
return
if self.Time.month == self.previous_month: # Check if this month has already been processed
return
self.previous_month = self.Time.month # Update the last processed month
# Calculate the performance (momentum) for each commodity and filter out the ones not ready
performance = {symbol: indicator.Current.Value for symbol, indicator in self.momentum_indicator.items() if indicator.IsReady and symbol in data}
if len(performance) < 5: # Ensure there are enough commodities to form a strategy
return
# Sort commodities based on their performance in descending order
sorted_commodities = sorted(performance.items(), key=lambda x: x[1], reverse=True)
quintile_size = len(sorted_commodities) // 5 # Determine the size of a quintile
long_positions = [symbol for symbol, _ in sorted_commodities[:quintile_size]] # Commodities to go long
short_positions = [symbol for symbol, _ in sorted_commodities[-quintile_size:]] # Commodities to go short
# Liquidate positions that are not in the current long or short lists
for symbol in self.Portfolio:
if symbol not in long_positions + short_positions:
self.Liquidate(symbol)
# Allocate equal weight to each long position
for symbol in long_positions:
self.SetHoldings(symbol, 1 / len(long_positions))
# Allocate equal weight to each short position, inversely
for symbol in short_positions:
self.SetHoldings(symbol, -1 / len(short_positions))
class QuantpediaFutures(PythonData):
'''
This class is responsible for importing commodity futures data
from a specified source.
'''
def GetSource(self, config, date, isLiveMode):
'''Defines the data source for each commodity futures data.'''
source = f"data.quantpedia.com/backtesting_data/futures/{config.Symbol.Value}.csv" # Data source URL
return SubscriptionDataSource(source, SubscriptionTransportMedium.RemoteFile)
def Reader(self, config, line, date, isLiveMode):
'''Parses each line of the data source and returns a populated data object.'''
if not line.startswith('20'): # Ignore lines that do not start with a year
return None
data = self.__class__() # Create a new instance of the data class
try:
# Split the line by semicolon and assign values
data.Time, back_adjusted, spliced = line.split(';')
# Parse the date and adjust it by one day
data.Time = datetime.strptime(data.Time, '%Y-%m-%d') + timedelta(days=1)
data.Value = float(back_adjusted) # Set the data value
data.Symbol = config.Symbol # Set the data symbol
except ValueError:
return None
return data
class StandardFeeModel(FeeModel):
'''
This class defines a standard fee model for transactions.
'''
def GetOrderFee(self, parameters):
'''Calculates the transaction fee based on the order details.'''
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 # Calculate the fee
return OrderFee(CashAmount(fee, "USD")) # Return the fee as an OrderFee object