
“The strategy involves shorting commodities from the S&P GSCI Index with decreased weights, with portfolio weights proportional to the changes. Short positions are covered by the 10th business day after January’s adjustment.”
ASSET CLASS: CFDs, futures | REGION: United States | FREQUENCY:
Daily | MARKET: commodities | KEYWORD: GSCI Index
I. 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.
II. 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.
III. SOURCE PAPER
Is the Supply Curve for Commodity Futures Contracts Upward Sloping? [Click to Open PDF]
- Yan, Irwin, Sanders, Yale University, University of Illinois at Urbana-Champaign, Southern Illinois University – Agribusiness Economics
<Abstract>
Annual rebalancing of the S&P GSCI index provides a novel and strong identification to estimate the shape of supply curves for commodity futures contracts. Using the 24 commodities included in the S&P GSCI for 2004 2017, we show that cumulative abnormal returns (CARs) reach a peak of 59 basis points in the middle of the week following the rebalancing period, but the impact is temporary as it declines to near zero within the next week. The findings provide clear evidence that the supply curve for commodity futures contracts is upward sloping in the short-run but almost flat in the longer-run.


IV. BACKTEST PERFORMANCE
| Annualised Return | 11.1% |
| Volatility | 18.05% |
| Beta | -0.005 |
| Sharpe Ratio | 0.61 |
| Sortino Ratio | N/A |
| Maximum Drawdown | N/A |
| Win Rate | 48% |
V. 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
VI. Backtest Performance