Implied Skewness Strategy in Commodities
Log in to collectAcademic paper
Commodity Return Predictability: Evidence from Implied Variance, Skewness and their Risk Premia
Marinela Adriana Finta; José Renato Haas Ornelas
- SGSingapore Management University
- BRCentral Bank of Brazil
- ?Banco Central do Brasil
Strategy in a nutshell
Trades 8 major commodities using implied skewness from one-month options. Goes long top 25% and short bottom 25% based on skewness. Portfolios (2 long, 2 short) are equally weighted, rebalanced daily, and held for 21 business days.
Economic rationale
Positive implied skewness predicts higher future returns, revealing informed trading and hedging effects in commodity markets. The strategy delivers a strong risk-return tradeoff, outperforming benchmarks and remaining robust across controls for momentum and roll yield.
Backtest performance
Annualised return17.21%
Volatility28%
Beta-0.121
Sharpe ratio0.62
Sortino ratio0.043
Win rate48%
Full Python code
from AlgorithmImports import *
class ImpliedSkewnessStrategyInCommodities(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2015, 1, 1)
self.SetCash(100000)
self.symbols = [] # storing commodities symbols
self.managed_queue = [] # storing parts of portfolio with their current holding period
self.contracts_expiry = {} # storing contracts expiry date under symbols
self.tickers_symbols = {} # storing commodities symbols under their tickers
self.futures = ['GLD', 'USO', 'UNG', 'SLV', 'CORN', 'WEAT', 'SOYB', 'CPER']
self.quantile = 4
for ticker in self.futures:
# subscribe to commodity future
security = self.AddEquity(ticker, Resolution.Minute)
# change normalization to raw to allow adding futures contracts
security.SetDataNormalizationMode(DataNormalizationMode.Raw)
# set fee model and leverage
security.SetFeeModel(CustomFeeModel())
security.SetLeverage(5)
# get commodity symbol
symbol = security.Symbol
# store future symbol under future ticker
self.tickers_symbols[ticker] = symbol
# add commodity symbol to symbols, which can be traded in this strategy
self.symbols.append(symbol)
self.min_expiry = 25
self.max_expiry = 35
self.holding_period = 21 # holding each part of portfolio n days
self.current_day = -1
def OnData(self, data):
# rebalance daily
if self.current_day == self.Time.day:
return
self.current_day = self.Time.day
for symbol in self.symbols:
# subscribe to new contracts, because current ones has expiried
if symbol not in self.contracts_expiry or self.contracts_expiry[symbol] <= self.Time.date():
# subsribe to new itm and otm contracts
self.SubscribeOptionContracts(symbol)
# storing implied skewness for each commodity
implied_skewness = {}
for kvp in data.OptionChains:
chain = kvp.Value
contracts = [x for x in chain]
# check if there are enough contracts for option
if len(contracts) < 4:
continue
# get future symbol
future_symbol = self.tickers_symbols[chain.Underlying.Symbol.Value]
# get implied volatility for each contract
itm_put_iv, itm_call_iv, otm_put_iv, otm_call_iv = self.GetImpliedVolatilities(contracts)
# make sure, there is implied volatility for each contract
if itm_put_iv and itm_call_iv and otm_put_iv and otm_call_iv:
# calculate otm mean
otm_mean = (otm_put_iv + otm_call_iv) / 2
# calculate itm mean
itm_mean = (itm_put_iv + itm_call_iv) / 2
# calculate and store skewness
implied_skewness[future_symbol] = otm_mean - itm_mean
# make sure, there are enough symbols for quartile selection
if len(implied_skewness) < self.quantile:
# try to liquidate old part of portfolio
self.TradeAndLiquidate()
return
# quartile selection
quantile = int(len(implied_skewness) / self.quantile)
sorted_by_skewness = [x[0] for x in sorted(implied_skewness.items(), key=lambda item: item[1])]
# long top quartile
long = [x for x in sorted_by_skewness[-quantile:] if self.GetLastPrice(x, data)]
# short bottom quartile
short = [x for x in sorted_by_skewness[:quantile] if self.GetLastPrice(x, data)]
if len(long) != 0 and len(short) != 0:
# calculate long and short weight for new portfolio part
long_w = self.Portfolio.TotalPortfolioValue / self.holding_period / len(long)
short_w = self.Portfolio.TotalPortfolioValue / self.holding_period / len(short)
# create long and shory symbols_q
long_symbols_q = [(x, np.floor(long_w / self.GetLastPrice(x, data))) for x in long]
short_symbols_q = [(x, -np.floor(short_w / self.GetLastPrice(x, data))) for x in short]
# add new part of portfolio to managed_queue list
self.managed_queue.append(RebalanceQueueItem(long_symbols_q + short_symbols_q))
# trade new part of portfolio and try to liquidate old one
self.TradeAndLiquidate()
def SubscribeOptionContracts(self, symbol):
''' get itm and otm strike for specific symbol '''
''' then it filters itm and otm puts and calls '''
''' if there are enough itm and otm puts and calls this function subscribes one of their contracts based on expiry and store expiry date '''
# get all contracts for current commodity future
contracts = self.OptionChainProvider.GetOptionContractList(symbol, self.Time)
# get current price for commodity future
underlying_price = self.Securities[symbol].Price
# get strikes from commodity future contracts
strikes = [i.ID.StrikePrice for i in contracts]
# check if there is at least one strike
if len(strikes) <= 0:
return
# in the money with 95%
itm_strike:float = min(strikes, key=lambda x: abs(x-(underlying_price*0.95)))
# out the money with 105%
otm_strike:float = min(strikes, key=lambda x: abs(x-(underlying_price*1.05)))
# filtred contracts based on option rights and strikes
itm_puts:list = self.FilterContracts(contracts, OptionRight.Put, itm_strike)
itm_calls:list = self.FilterContracts(contracts, OptionRight.Call, itm_strike)
otm_puts:list = self.FilterContracts(contracts, OptionRight.Put, otm_strike)
otm_calls:list = self.FilterContracts(contracts, OptionRight.Call, otm_strike)
# make sure there are enough contracts
if len(itm_puts) > 0 and len(itm_calls) > 0 and len(otm_puts) > 0 and len(otm_calls) > 0:
# sort by expiry
itm_put, itm_call, otm_put, otm_call = self.SortByExpiry(itm_puts, itm_calls, otm_puts, otm_calls)
# add contracts
for contract in [itm_put, itm_call, otm_put, otm_call]:
self.AddContract(contract)
# store expiry date
self.contracts_expiry[symbol] = itm_put.ID.Date.date()
def FilterContracts(self, contracts, option_right, strike):
''' filter contracts based on option_right and strike parameter from contracts parameter'''
# filter contracts based on option right and strike parameters
filtered_contracts:list = [i for i in contracts if i.ID.OptionRight == option_right and
i.ID.StrikePrice == strike and
self.min_expiry < (i.ID.Date - self.Time).days < self.max_expiry]
# return filtered contracts
return filtered_contracts
def SortByExpiry(self, itm_puts, itm_calls, otm_puts, otm_calls):
''' sort contracts based on expiry and returns contracts with nearest expiry date '''
# sort by expiry
itm_put = sorted(itm_puts, key = lambda x: x.ID.Date)[0]
itm_call = sorted(itm_calls, key = lambda x: x.ID.Date)[0]
otm_put = sorted(otm_puts, key = lambda x: x.ID.Date)[0]
otm_call = sorted(otm_calls, key = lambda x: x.ID.Date)[0]
return [itm_put, itm_call, otm_put, otm_call]
def AddContract(self, contract):
''' subcribe to contract, set price model and normalization mode '''
# add contract
option = self.AddOptionContract(contract, Resolution.Minute)
option.PriceModel = OptionPriceModels.CrankNicolsonFD()
option.SetDataNormalizationMode(DataNormalizationMode.Raw)
def GetImpliedVolatilities(self, contracts):
''' return implied volatility for itm_put, itm_call, otm_put and otm_call contract '''
itm_put_iv = None
itm_call_iv = None
otm_put_iv = None
otm_call_iv = None
# go through option contracts
for c in contracts:
# get underlying price of contract
underlying_price = self.Securities[c.UnderlyingSymbol].Price
if c.Right == OptionRight.Call and c.Strike < underlying_price:
# found itm call
itm_call_iv = c.ImpliedVolatility
elif c.Right == OptionRight.Call and c.Strike >= underlying_price:
# found otm call
otm_call_iv = c.ImpliedVolatility
elif c.Right == OptionRight.Put and c.Strike < underlying_price:
# found itm put
itm_put_iv = c.ImpliedVolatility
else:
# found otm put
otm_put_iv = c.ImpliedVolatility
# return implied volatility for each contract
return itm_put_iv, itm_call_iv, otm_put_iv, otm_call_iv
def GetLastPrice(self, symbol, data):
''' return symbol price from data object, or returns None if there isn't the price '''
# check if symbol has price in data object
if symbol in data and data[symbol]:
# return price
return data[symbol].Value
else:
# return None, if symbol doesn't have price in data object
return None
def TradeAndLiquidate(self):
''' handles daily rebalancing of portfolio parts '''
''' it trades new parts and liquidate parts, which has holding_period equal to self.holding_period '''
''' after liquidation these parts are removed from self.managed_queue '''
remove_portfolio_part = None
for portfolio_part in self.managed_queue:
# liquidate this part of portfolio
if portfolio_part.holding_period == self.holding_period:
for symbol, quantity in portfolio_part.symbol_q:
# liquidate symbol from this portfolio part
self.MarketOrder(symbol, -quantity)
# reinitialize remove_portfolio_part
remove_portfolio_part = portfolio_part
# trade new portfolio part
elif portfolio_part.holding_period == 0:
# store symbols, which were opened
opened_symbol_q = []
for symbol, quantity in portfolio_part.symbol_q:
if self.Securities[symbol].Price != 0 and self.Securities[symbol].IsTradable:
# trade symbol
self.MarketOrder(symbol, quantity)
# store symbol in opened symbol_q
opened_symbol_q.append((symbol, quantity))
# change symbol_q of current portfolio part
portfolio_part.symbol_q = opened_symbol_q
# increase holding period of current portfolio part
portfolio_part.holding_period += 1
# in the end try to remove portfolio part from self.managed_queue
if remove_portfolio_part:
self.managed_queue.remove(remove_portfolio_part)
class RebalanceQueueItem():
def __init__(self, symbol_q):
# symbol/quantity collections
self.symbol_q = symbol_q
self.holding_period = 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"))