Quant BuffetRelax, Not Over Thinking

Front-Running S&P GSCI Index

Log in to collect

Academic paper

Is the Supply Curve for Commodity Futures Contracts Upward Sloping?

AuthorsLei Yan; Scott H. Irwin; Dwight R. Sanders

Institute
  • Yale University
  • University of Illinois Urbana-Champaign
  • ?University of Illinois at Urbana-Champaign
  • Southern Illinois University System
  • ?Southern Illinois University - Agribusiness Economics

Strategy in a nutshell

The investment universe consists of commodity futures from the S&P GSCI Index. In November, new weights are determined, and by the 5th business day of January, the investor shorts commodities with decreased weights. Portfolio weights are proportional to the changes in S&P GSCI Index weights. Short positions are covered by the 10th business day. The strategy is executed based on annual weight adjustments in the index.

Economic rationale

The changes in weights during the S&P GSCI Index rebalancing cause uninformed order flows in commodity futures markets. If markets are frictionless and contract supply is elastic, prices remain unaffected. However, with limits to arbitrage, the supply of futures contracts is upward sloping. This results in the uninformed order flow from the rebalancing creating price pressure, as the market cannot immediately absorb these orders. This price distortion occurs due to the limits on arbitrage, where the supply of contracts is not perfectly elastic and prices are affected by the rebalancing process.

Backtest performance

Annualised return11.1%
Volatility18.05%
Beta-0.005
Sharpe ratio0.61
Win rate48%

Full Python code

from AlgorithmImports import *
from io import StringIO
import pandas as pd
#endregion
class FrontRunningSAndPGSCIIndex(QCAlgorithm):

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

self.symbols = {}
self.trading_day = 0
self.symbol = self.AddEquity('SPY', Resolution.Daily).Symbol

csv_string_file = self.Download(f'data.quantpedia.com/backtesting_data/economic/future_comodities_RPWD.csv')
# does not take first row as a header
separated_file = pd.read_csv(StringIO(csv_string_file), sep=';', header=None)

first_row = True   
for row in separated_file.itertuples():
    if first_row: # first row includes symbols of future comodities
        first_row = False
        for symbol in row[2:]:
            data = self.AddData(QuantpediaFutures, symbol, Resolution.Daily)
            data.SetFeeModel(CustomFeeModel())
            self.symbols[symbol] = {}
    else:
        for (value), (symbol) in zip(row[2:], self.symbols):
            self.symbols[symbol][int(row[1])] = value # second element in row is year

def OnData(self, data):
if self.Time.month == 1:
    if self.trading_day == 5:
        short = {}
        total_weight_changed = 0
        
        for symbol in self.symbols:
            year = self.Time.year # get current year
            # check if future comodity has weight for current year and year before
            if (year in self.symbols[symbol]) and ((year - 1) in self.symbols[symbol]):
                weight_change = float(self.symbols[symbol][year]) - float(self.symbols[symbol][year - 1])
                if weight_change < 0: # if weight drops from last year, then go short on this future comodity
                    total_weight_changed += (-weight_change) # calculating weight change for short weighting
                    short[symbol] = weight_change
        
        if len(short) != 0:
            for symbol, weight_change in short.items():
                w = weight_change / total_weight_changed
                self.SetHoldings(symbol, w) # weight_change is already minus, so this will go short
            
    elif self.trading_day == 10:
        self.Liquidate()
        
    self.trading_day += 1
else:
    self.trading_day = 0
    
# Custom fee model
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))

# Quantpedia data
# NOTE: IMPORTANT: Data order must be ascending (datewise)
class QuantpediaFutures(PythonData):
def GetSource(self, config, date, isLiveMode):
return SubscriptionDataSource("data.quantpedia.com/backtesting_data/futures/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
def Reader(self, config, line, date, isLiveMode):
data = QuantpediaFutures()
data.Symbol = config.Symbol

if not line[0].isdigit(): return None
split = line.split(';')

data.Time = datetime.strptime(split[0], "%d.%m.%Y") + timedelta(days=1)
data['settle'] = float(split[1])
data.Value = float(split[1])
return data