Quant BuffetRelax, Not Over Thinking

Convenience Yield Risk Factor Predicts Commodity Futures Returns

Log in to collect

Academic paper

Convenience Yield Risk

AuthorsMarcel Prokopczuk; Lazaros Symeonidis; Chardin Wese Simen; Robert Wichmann

Institute
  • DELeibniz University Hannover
  • ICMA Centre
  • University of Reading
  • ?Leibniz Universität Hannover - Faculty of Economics and Management
  • ?University of Reading - ICMA Centre
  • University of Essex
  • ?Essex Business School, University of Essex
  • University of Liverpool
  • ?University of Liverpool Management School
  • ?ICMA Centre, University of Reading

Strategy in a nutshell

Monthly commodity futures strategy: compute CYR from differences in convenience yield volatility of nearby contracts, sort 27 commodities by CYR, go long high-CYR, short low-CYR portfolios, equally weighted, rebalanced monthly.

Economic rationale

CYR captures commodity-specific risk unexplained by carry, momentum, or macro factors. The long-short strategy exploits this risk premium, remains profitable after transaction costs, and passes robustness checks.

Backtest performance

Annualised return6.93%
Volatility15.07%
Beta-0.037
Sharpe ratio0.46
Sortino ratio-0.112
Win rate50%

Full Python code

from AlgorithmImports import *
import data_tools
import numpy as np
from typing import List, Dict, Tuple
# endregion

class ConvenienceYieldRiskFactorPredictsCommodityFuturesReturns(QCAlgorithm):

def Initialize(self):
self.SetStartDate(2008, 1, 1)
self.SetCash(100000)

self.period:int = 12
self.filter_period:int = 182

self.tickers:dict[str, str] = {
    "CME_S1"  : Futures.Grains.Soybeans,
    "CME_W1"  : Futures.Grains.Wheat,
    "CME_SM1" : Futures.Grains.SoybeanMeal,
    "CME_BO1" : Futures.Grains.SoybeanOil,
    "CME_C1"  : Futures.Grains.Corn,
    "CME_O1"  : Futures.Grains.Oats,
    "CME_LC1" : Futures.Meats.LiveCattle,
    "CME_FC1" : Futures.Meats.FeederCattle,
    "CME_LN1" : Futures.Meats.LeanHogs,
    "CME_GC1" : Futures.Metals.Gold,
    "CME_SI1" : Futures.Metals.Silver,
    "CME_PL1" : Futures.Metals.Platinum,
    "CME_HG1" : Futures.Metals.Copper,
    "CME_LB1" : Futures.Forestry.RandomLengthLumber,
    "CME_NG1" : Futures.Energies.NaturalGas,
    "CME_PA1" : Futures.Metals.Palladium,
    "CME_CU1" : Futures.Energies.ChicagoEthanolPlatts,
    "CME_DA1" : Futures.Dairy.ClassIIIMilk,
    "ICE_CC1" : Futures.Softs.Cocoa,
    "ICE_CT1" : Futures.Softs.Cotton2,
    "ICE_KC1" : Futures.Softs.Coffee,
    "ICE_O1"  : Futures.Energies.HeatingOil,
    "ICE_OJ1" : Futures.Softs.OrangeJuice,
    "ICE_SB1" : Futures.Softs.Sugar11CME,
}

self.leverage:int = 2
self.data:Dict[Symbol, Tuple[float, float]] = {}
self.cyr_signal:Dict[Symbol, RollingWindow] = {}

self.futures_data:dict[Symbol, data_tools.FuturesData] = {}

for qp_ticker, qc_ticker in self.tickers.items():
    # subscribe Quantpedia data
    security:Security = self.AddData(data_tools.QuantpediaFutures, qp_ticker, Resolution.Daily)
    security.SetFeeModel(data_tools.CustomFeeModel())
    security.SetLeverage(self.leverage)

    qp_symbol:Symbol = security.Symbol

    # QC futures
    future:Future = self.AddFuture(qc_ticker, Resolution.Daily, dataNormalizationMode=DataNormalizationMode.Raw)
    future.SetFilter(0, self.filter_period)
    future_symbol:str = future.Symbol
    self.futures_data[future_symbol] = qp_symbol

self.recent_month:int = -1

def OnData(self, data:Slice):
qp_custom_data_last_update_date:Dict[Symbol, datetime.date] = data_tools.QuantpediaFutures._last_update_date

if all([self.Securities[x].GetLastData() for x in list(self.futures_data.keys())]) and any([self.Time.date() >= qc_custom_data_last_update_date[x] for x in qc_custom_data_last_update_date]):
    self.Liquidate()
    return

# save daily data 
for contract_symbol, chain in data.FutureChains.items():
    if len([i for i in chain]) >= 3:
        sorted_by_date:List[Symbol] = sorted(chain, key=lambda x: x.Expiry)

        first_convenience_yield:float = (365 * (sorted_by_date[0].LastPrice - sorted_by_date[1].LastPrice)) / ((sorted_by_date[1].Expiry - self.Time).days - (sorted_by_date[0].Expiry - self.Time).days)
        second_convenience_yield:float = (365 * (sorted_by_date[1].LastPrice - sorted_by_date[2].LastPrice)) / ((sorted_by_date[2].Expiry - self.Time).days - (sorted_by_date[1].Expiry - self.Time).days)

        if contract_symbol not in self.data:
            self.data[contract_symbol] = []
        self.data[contract_symbol].append((first_convenience_yield, second_convenience_yield))

# monthly rebalance
if self.Time.month == self.recent_month:
    return
self.recent_month = self.Time.month

if len(self.data) == 0:
    self.Liquidate()
    return

# save convenience yield risk values
for contract_symbol, cyr_values in self.data.items():
    if len(cyr_values) != 0:
        first_std:float = np.std([i[0] for i in cyr_values])
        second_std:float = np.std([i[1] for i in cyr_values])

        if contract_symbol not in self.cyr_signal:
            self.cyr_signal[contract_symbol] = RollingWindow[float](self.period)
        
        self.cyr_signal[contract_symbol].Add(first_std - second_std)

self.data.clear()

if len(self.cyr_signal) == 0:
    self.Liquidate()
    return

cyr_mean:Dict[Symbol, float] = {}

# mean of 12 months convenience yield risk values
for contract_symbol, cyr_signals in self.cyr_signal.items():
    if cyr_signals.IsReady:
        if contract_symbol not in cyr_mean:
            cyr_mean[contract_symbol] = np.mean(list(cyr_signals))

# sort and divide
if len(cyr_mean) != 0:
    sorted_cyr:List[Symbol] = sorted(cyr_mean.items(), key=lambda x:x[1])
    CYR_median:float = np.median([i[1] for i in sorted_cyr])
    high:List[Symbol] = [symbol for symbol, cyr_value in sorted_cyr if cyr_value > CYR_median]
    low:List[Symbol] = [symbol for symbol, cyr_value in sorted_cyr if cyr_value <= CYR_median]

    # trade execution
    invested:List[Symbol] = [x.Key for x in self.Portfolio if x.Value.Invested]
    for symbol in invested:
        if symbol not in high + low:
            self.Liquidate(symbol)

    for symbol in high:
        if self.futures_data[symbol] in data and data[self.futures_data[symbol]]:
            self.SetHoldings(self.futures_data[symbol], 1 / len(high))
    
    for symbol in low:
        if self.futures_data[symbol] in data and data[self.futures_data[symbol]]:
            self.SetHoldings(self.futures_data[symbol], -1 / len(low))