High-to-Price Factor in Commodities
Log in to collectAcademic paper
Commodity Momentum Decomposition
Yasuhiro Iwanaga; Ryuta Sakemoto
- JPHiroshima Shudo University
- JPKeio University
- JPOkayama University
Strategy in a nutshell
The strategy trades 27 commodities on the futures market using the High-to-Price (HTP) momentum factor. HTP is calculated as the log ratio of the highest price during the formation period over the initial price, excluding the final month to avoid reversal effects. Commodities are sorted into terciles (High, Middle, Low) based on their HTP values. The strategy goes long on high-HTP commodities and short on low-HTP commodities. All positions are equally weighted, and the portfolio is rebalanced monthly.
Economic rationale
The strategy exploits investor underreaction and slow diffusion of information in commodity markets. Investors often react sluggishly to new price information, delaying purchases in bullish trends and sales in bearish trends. The High-to-Price factor captures this behavioral bias, producing superior momentum returns compared to traditional momentum measures. Mispricing caused by investor inaction creates profitable opportunities for systematically trading high-HTP and low-HTP commodities.
Backtest performance
Full Python code
from AlgorithmImports import *
from functools import reduce
from data_tools import CustomFeeModel, QuantpediaFutures, SymbolData
# endregion
class HighToPriceFactorInCommodities(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.leverage:int = 5
self.quantile:int = 3
self.month_period:int = 21
self.period:int = self.month_period * 12
self.total_months_in_year:int = 12
self.max_missing_days:int = 5
self.min_prices:int = 15
self.recent_month:int = -1
self.data:dict[Symbol, SymbolData] = {}
tickers:list[str] = [
"CME_S1", # Soybean Futures, Continuous Contract #1
"CME_W1", # Wheat Futures, Continuous Contract #1
"CME_SM1", # Soybean Meal Futures, Continuous Contract #1
"CME_BO1", # Soybean Oil Futures, Continuous Contract #1
"CME_C1", # Corn Futures, Continuous Contract #1
"CME_O1", # Oats Futures, Continuous Contract #1
"CME_LC1", # Live Cattle Futures, Continuous Contract #1
"CME_FC1", # Feeder Cattle Futures, Continuous Contract #1
"CME_LN1", # Lean Hog Futures, Continuous Contract #1
"CME_GC1", # Gold Futures, Continuous Contract #1
"CME_SI1", # Silver Futures, Continuous Contract #1
"CME_PL1", # Platinum Futures, Continuous Contract #1
"CME_CL1", # Crude Oil Futures, Continuous Contract #1
"CME_HG1", # Copper Futures, Continuous Contract #1
"CME_LB1", # Random Length Lumber Futures, Continuous Contract #1
"CME_NG1", # Natural Gas (Henry Hub) Physical Futures, Continuous Contract #1
"CME_PA1", # Palladium Futures, Continuous Contract #1
"CME_RR1", # Rough Rice Futures, Continuous Contract #1
"CME_CU1", # Chicago Ethanol (Platts) Futures
"CME_DA1", # Class III Milk Futures
"ICE_CC1", # Cocoa Futures, Continuous Contract #1
"ICE_CT1", # Cotton No. 2 Futures, Continuous Contract #1
"ICE_KC1", # Coffee C Futures, Continuous Contract #1
"ICE_O1", # Heating Oil Futures, Continuous Contract #1
"ICE_OJ1", # Orange Juice Futures, Continuous Contract #1
"ICE_SB1" # Sugar No. 11 Futures, Continuous Contract #1
]
for ticker in tickers:
security = self.AddData(QuantpediaFutures, ticker, Resolution.Daily)
security.SetFeeModel(CustomFeeModel())
security.SetLeverage(self.leverage)
symbol:Symbol = security.Symbol
self.data[symbol] = SymbolData(self.period)
self.SetWarmup(self.period)
def OnData(self, data: Slice):
curr_date:datetime.date = self.Time.date()
for symbol, symbol_data in self.data.items():
if symbol in data and data[symbol] and data[symbol].Value != 0:
price:float = data[symbol].Value
symbol_data.update_monthly_prices(price)
symbol_data.set_last_update_date(curr_date)
if self.recent_month == self.Time.month:
return
self.recent_month = self.Time.month
htp:dict[Symbol, float] = {}
for symbol, symbol_data in self.data.items():
if not symbol_data.monthly_prices_ready(self.min_prices) or \
not symbol_data.data_still_coming(curr_date, self.max_missing_days):
symbol_data.reset_data()
else:
symbol_data.update_prices(self.total_months_in_year)
if symbol_data.is_ready(self.total_months_in_year):
prices_in_months:list[list[float]] = symbol_data.get_prices_in_months()
prices_in_months = prices_in_months[:-1] # ommit last month
prices:list[float] = list(reduce(lambda x,y: x+y, prices_in_months))
highest_price:float = max(prices)
first_price:float = prices[0]
htp_value:float = np.log(highest_price / first_price)
htp[symbol] = htp_value
symbol_data.reset_monthly_prices()
if self.IsWarmingUp or len(htp) < self.quantile:
self.Liquidate()
return
quantile:int = int(len(htp) / self.quantile)
sorted_by_htp:list[Symbol] = [x[0] for x in sorted(htp.items(), key=lambda item: item[1])]
# investors go long on the commodities with high factor values and short on the commodities with low factor values
long_leg:list[Symbol] = sorted_by_htp[-quantile:]
short_leg:list[Symbol] = sorted_by_htp[:quantile]
# trade execution
invested:list[Symbol] = [x.Key for x in self.Portfolio if x.Value.Invested]
for symbol in invested:
if symbol not in long_leg + short_leg:
self.Liquidate(symbol)
long_length:int = len(long_leg)
short_length:int = len(short_leg)
for symbol in long_leg:
self.SetHoldings(symbol, 1 / long_length)
for symbol in short_leg:
self.SetHoldings(symbol, -1 / short_length)