Front-Running S&P GSCI Index
Log in to collectAcademic paper
Is the Supply Curve for Commodity Futures Contracts Upward Sloping?
Lei Yan; Scott H. Irwin; Dwight R. Sanders
- 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
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