Quant BuffetRelax, Not Over Thinking

Commodity Option Implied Volatility Strategy

Log in to collect

Academic paper

Commodity Option Implied Volatilities and the Expected Futures Returns

AuthorsLin Gao

Institute
  • LULuxembourg School of Business
  • LUUniversity of Luxembourg
  • ?Luxembourg School of Finance
  • ?Universite du Luxembourg

Strategy in a nutshell

The strategy trades 25 commodities, sorted into four groups according to their 30-day implied volatility, de-trended by the prior 12 months’ average. Commodities in the "Low" group (lowest 25% volatility) are bought, while those in the "High" group (highest 25% volatility) are sold. The portfolio is equally weighted and rebalanced monthly, implementing a systematic long-short approach based on volatility differences.

Economic rationale

The VOL strategy profits from predictable spot returns driven by the cost of volatility insurance. Commodities with expensive hedging tend to decline, while those with cheap hedging tend to rise. Limited arbitrage and capital constraints affect hedgers’ ability to manage inventories: high hedging costs reduce inventory, creating selling pressure, whereas low costs encourage hedging and support prices. This dynamic underpins the observed performance of the VOL strategy.

Backtest performance

Annualised return12.66%
Volatility18.48%
Beta-0.079
Sharpe ratio0.69
Sortino ratio0.342
Win rate50%

Full Python code

from AlgorithmImports import *
#endregion
# https://quantpedia.com/strategies/commodity-option-implied-volatility-strategy/
#
# The investment universe consists of 25 commodities.
# Commodities are sorted into four groups based on the 30-days implied volatility de-trended by the previous 12 months mean of implied volatility (see page 8 for exact formula).
# The “Low” (“High”) group contains the top 25% of all commodities with the lowest (highest) volatilities.
# The portfolio is long-short and buys commodities from the group “Low” and sells commodities from the group “High”.
# The portfolio is equally-weighted and is rebalanced on a monthly basis.
#
# QC Implementation:
import numpy as np
class CommodityOptionImpliedVolatilityStrategy(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2010, 1, 1)
self.SetCash(100000)

self.min_expiry = 25
self.max_expiry = 35

self.period = 12 # need n of implied volatilities

self.iv = {} # storing implied volatilies in RollingWindow
self.contracts = {} # storing option contracts
self.tickers_symbols = {} # storing commodities symbols under their tickers

self.tickers = ['GLD', 'USO', 'UNG', 'SLV', 'DBA', 'DBB', 'PPLT', 'PALL']
self.next_expiry = None
for ticker in self.tickers:
    # subscribe to commodity
    security = self.AddEquity(ticker, Resolution.Minute)
    
    # change normalization to raw to allow adding contracts
    security.SetDataNormalizationMode(DataNormalizationMode.Raw)
    # set fee model and leverage
    security.SetFeeModel(CustomFeeModel())
    security.SetLeverage(5)
    
    # get commodity symbol
    symbol = security.Symbol
    # store symbol under ticker
    self.tickers_symbols[ticker] = symbol
    # create RollingWindow for implied volatilities
    self.iv[symbol] = RollingWindow[float](self.period)

self.day = -1

def OnData(self, data):
# rebalance daily
if self.day == self.Time.day:
    return
self.day = self.Time.day

if self.next_expiry and self.Time.date() >= self.next_expiry.date():
    self.Liquidate()

    for symbol in self.tickers_symbols:
        if symbol in self.contracts:
            # remove expired contracts
            for contract in self.contracts[symbol]:
                self.RemoveSecurity(contract)
            # remove contracts from dictionary
            del self.contracts[symbol]
            
if not self.Portfolio.Invested:
    for symbol in self.tickers_symbols:
        if symbol not in self.contracts:
            # get all contracts for current commodity
            contracts = self.OptionChainProvider.GetOptionContractList(symbol, self.Time)
            # get current price for commodity
            underlying_price = self.Securities[symbol].Price
            
            # get strikes from commodity contracts
            strikes = [i.ID.StrikePrice for i in contracts]
            if len(strikes) > 0:
                # get at the money strike
                atm_strike:float = min(strikes, key=lambda x: abs(x-underlying_price))

                atm_calls:list = [i for i in contracts if i.ID.OptionRight == OptionRight.Call and 
                                                         i.ID.StrikePrice == atm_strike and 
                                                         self.min_expiry < (i.ID.Date - self.Time).days < self.max_expiry]
                
                atm_puts:list = [i for i in contracts if i.ID.OptionRight == OptionRight.Put and 
                                                         i.ID.StrikePrice == atm_strike and 
                                                         self.min_expiry < (i.ID.Date - self.Time).days < self.max_expiry]
                
                if len(atm_calls) and len(atm_puts):
                    # sort by expiry
                    atm_call = sorted(atm_calls, key = lambda x: x.ID.Date)[0]
                    atm_put = sorted(atm_puts, key = lambda x: x.ID.Date)[0]
                    
                    self.next_expiry = min(atm_call.ID.Date, atm_put.ID.Date)
                    
                    # add contracts
                    option = self.AddOptionContract(atm_call, Resolution.Minute)
                    option.PriceModel = OptionPriceModels.CrankNicolsonFD()
                    option.SetDataNormalizationMode(DataNormalizationMode.Raw)
                    
                    option = self.AddOptionContract(atm_put, Resolution.Minute)
                    option.PriceModel = OptionPriceModels.CrankNicolsonFD()
                    option.SetDataNormalizationMode(DataNormalizationMode.Raw)
                    
                    # store atm contracts by symbol
                    self.contracts[symbol] = [atm_call, atm_put]
    
    iv_detrend = {} # storing detrend implied volatility for options
    
    if data.OptionChains.Count != 0:
        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) < 2:
                continue
            
            atm_call_iv = None
            atm_put_iv = None
            # get ticker
            ticker = chain.Underlying.Symbol.Value
            
            # go through option contracts
            for c in contracts:
                if c.Right == OptionRight.Call:
                    # found atm call
                    atm_call_iv = c.ImpliedVolatility
                else:
                    # found put option
                    atm_put_iv = c.ImpliedVolatility
            
            if atm_call_iv and atm_put_iv:
                # make mean from atm call implied volatility and atm put implied volatility
                iv = (atm_call_iv + atm_put_iv) / 2 
                # get symbol based on ticker from option contract
                commodity_symbol = self.tickers_symbols[ticker]
                
                # check if there are enough data of mean implied volatilities
                if self.iv[commodity_symbol].IsReady:
                    # calculate mean of previous mean implied volatilities
                    vol_mean = np.mean([x for x in self.iv[commodity_symbol]])
                    # calculate detrend implied volatility and store it by symbol
                    iv_detrend[commodity_symbol] = iv - vol_mean
                    
                # add current mean of implied volatility
                self.iv[commodity_symbol].Add(iv)
    
    # can't perform quintile selection
    if len(iv_detrend) < 4:
        self.Liquidate()
        return
    
    quintile = int(len(iv_detrend) / 4)
    sorted_by_iv_detrend = [x[0] for x in sorted(iv_detrend.items(), key=lambda item: item[1])]
    
    # go long smallest quintile
    long = sorted_by_iv_detrend[:quintile]
    # go short largest quintile
    short = sorted_by_iv_detrend[-quintile:]
    
    # trade execution
    long_length = len(long)
    short_length = len(short)
    
    for symbol in long:
        self.SetHoldings(symbol, 1 / long_length)
    for symbol in short:
        self.SetHoldings(symbol, -1 / short_length)

# custom fee model
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))