Quant BuffetRelax, Not Over Thinking

Implied Skewness Strategy in Commodities

Log in to collect

Academic paper

Commodity Return Predictability: Evidence from Implied Variance, Skewness and their Risk Premia

AuthorsMarinela Adriana Finta; José Renato Haas Ornelas

Institute
  • 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"))