“Form a commodity futures trading universe. Monthly, rank last year’s performance by quintiles. Long top momentum quintile, short bottom. Rebalance monthly.”

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]

<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 Return14.6%
Volatility25.6%
Beta-0.104
Sharpe Ratio0.047
Sortino Rato0.053
Maximum Drawdown76.7%
Win Rate57%

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

Leave a Reply

Discover more from Quant Buffet

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

Continue reading