
The strategy sells one-month at-the-money straddles for Oil, Gold, and Natural Gas futures monthly, equally weights positions, and delta-hedges daily to maintain primary exposure to market volatility changes.
ASSET CLASS: options | REGION: Global | FREQUENCY:
Monthly | MARKET: commodities | KEYWORD: Volatility
I. STRATEGY IN A NUTSHELL
The strategy involves continuously selling one-month at-the-money straddles on Oil, Gold, and Natural Gas futures. Each straddle—comprising a call and a put at the same strike and maturity—is equally weighted. Straddles are delta-hedged daily to maintain exposure primarily to changes in market volatility rather than price direction.
II. ECONOMIC RATIONALE
This approach aims to capture the volatility risk premium in commodity markets. By selling straddles, the strategy earns the premium paid by buyers seeking protection or speculative exposure. Daily delta hedging ensures the portfolio responds to volatility changes, not directional moves. Structural market imbalances—high demand from hedgers, limited natural sellers, and reduced leverage during crises—make the volatility risk premium persistent, allowing the strategy to exploit these inefficiencies over time.
III. SOURCE PAPER
The Volatility Risk Premium [Click to Open PDF]
G. Renninson, N. Pedersen
<Abstract>
Elevated global macroeconomic uncertainty and bouts of extreme market turbulence have recently plagued financial markets. This environment has prompted a search for diversifying investment opportunities that lie outside the space of traditional asset classes. This article examines the performance of options strategies that aim to capture a return premium over time as compensation for the risk of losses during sudden increases in market volatility. We show that these “volatility risk premium” strategies deliver attractive risk-adjusted returns across 14 options markets from June 1994 to June 2012. Performance furthermore improves significantly after the crisis in 2008 (see Figure 1). We conclude that the risk-return tradeoff for volatility strategies compares favorably to those of traditional investments such as equities and bonds and that the strategies exhibit relatively low correlations to equity risk. Investors who want to diversify their portfolio’s equity risk exposures should therefore consider making allocations to volatility risk premium strategies. However, successful implementation would require diversification across major options markets (equities, interest rates, currencies and commodities), active risk management and prudent scaling.

IV. BACKTEST PERFORMANCE
| Annualised Return | 6.1% |
| Volatility | 5.2% |
| Beta | 0.109 |
| Sharpe Ratio | 1.17 |
| Sortino Ratio | -0.299 |
| Maximum Drawdown | -8.5% |
| Win Rate | 45% |
V. FULL PYTHON CODE
from AlgorithmImports import *
class VolatilityRiskPremiuminCommodities(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.min_expiry = 30
self.max_expiry = 45
self.symbols = []
self.contracts = {}
tickers = ['USO', 'GLD', 'UNG']
for ticker in tickers:
# equity data
data = self.AddEquity(ticker, Resolution.Daily)
data.SetLeverage(10)
data.SetFeeModel(CustomFeeModel())
# change normalization to raw to allow adding etf contracts
data.SetDataNormalizationMode(DataNormalizationMode.Raw)
self.symbols.append(data.Symbol)
self.settings.daily_precise_end_time = False
self.settings.minimum_order_margin_portfolio_percentage = 0.
self.last_day:int = -1
self.trade_flag = False
def OnData(self, data):
# check once a day
if self.Time.day == self.last_day:
return
self.last_day = self.Time.day
# trade option contracts after selection and make sure, they are ready
if self.trade_flag and data.OptionChains.Count != 0:
# next rebalance will be after next selection
self.trade_flag = False
# sell atm straddle of subscribed contracts
for symbol, contract_obj in self.contracts.items():
# get call and put contract
call, put = contract_obj.contracts
# get underlying price
underlying_price = contract_obj.underlying_price
options_q = int((self.Portfolio.TotalPortfolioValue / len(self.symbols)) / (underlying_price * 100))
self.Sell(call, options_q)
self.Sell(put, options_q)
# delta hedge
self.MarketOrder(symbol, options_q*50)
# check contracs expiration
for symbol in self.symbols:
# close trade once option is
if symbol in self.contracts and self.contracts[symbol].expiry_date-timedelta(days=1) <= self.Time.date():
# liquidate expired contracts
for contract in self.contracts[symbol].contracts:
self.Liquidate(contract)
self.Liquidate(symbol)
# remove Contracts object for current symbol
del self.contracts[symbol]
# rebalance, when all option contracts expiried
if len(self.contracts) == 0:
self.Liquidate()
# subscribe to new option contracts
for symbol in self.symbols:
# get all contracts for current etf
contracts = self.OptionChainProvider.GetOptionContractList(symbol, self.Time)
# get current price for etf
underlying_price = self.Securities[symbol].Price
# get strikes from etf contracts
strikes = [i.ID.StrikePrice for i in contracts]
# can't filter contracts, if there isn't any strike price
if len(strikes) <= 0:
continue
# filter calls and puts contracts with one month expiry
calls, puts = self.FilterContracts(strikes, contracts, underlying_price)
# make sure, there is at least one call and put contract
if len(calls) and len(puts):
# sort by expiry
call = sorted(calls, key = lambda x: x.ID.Date)[0]
put = sorted(puts, key = lambda x: x.ID.Date)[0]
subscription = self.SubscriptionManager.SubscriptionDataConfigService.GetSubscriptionDataConfigs(call.Underlying)
# make sure put and call contract subscription is valid
if subscription:
# add call contract
self.AddContract(call)
# add put contract
self.AddContract(put)
# retrieve expiry date for contracts
expiry_date = call.ID.Date.date() if call.ID.Date.date() <= put.ID.Date.date() else put.ID.Date.date()
# store contracts with expiry date under etf symbol
self.contracts[symbol] = Contracts(expiry_date, underlying_price, [call, put])
# strategy sells subscribed contracts on next day
self.trade_flag = True
def FilterContracts(self, strikes, contracts, underlying_price):
''' filter call and put contracts from contracts parameter '''
''' return call and put contracts '''
# Straddle
call_strike:float = min(strikes, key=lambda x: abs(x-underlying_price))
put_strike = call_strike
calls = [] # storing call contracts
puts = [] # storing put contracts
for contract in contracts:
# check if contract has one month expiry
if self.min_expiry < (contract.ID.Date - self.Time).days < self.max_expiry:
# check if contract is call
if contract.ID.OptionRight == OptionRight.Call and contract.ID.StrikePrice == call_strike:
calls.append(contract)
# check if contract is put
elif contract.ID.OptionRight == OptionRight.Put and contract.ID.StrikePrice == put_strike:
puts.append(contract)
# return filtered calls and puts with one month expiry
return calls, puts
def AddContract(self, contract):
''' subscribe option contract, set price mondel and normalization mode '''
option = self.AddOptionContract(contract, Resolution.Daily)
option.PriceModel = OptionPriceModels.CrankNicolsonFD()
class Contracts():
def __init__(self, expiry_date, underlying_price, contracts):
self.expiry_date = expiry_date
self.underlying_price = underlying_price
self.contracts = contracts
# custom fee model
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))
VI. Backtest Performance