Portfolio Hedging Using VIX Options
Log in to collectAcademic paper
Strategy in a nutshell
The strategy combines a 60% stock and 40% bond portfolio using SPY for equities and IEF for bonds, with a small allocation to VIX call options (0–100 bps) across one- to four-month maturities. The allocation is adjusted dynamically based on VIX levels, rolled before expiration, and reinvested into the stock/bond portfolio, providing a systematic hedge against market volatility.
Economic rationale
The strategy exploits the mean-reverting behavior of volatility, buying more VIX calls when volatility is low and fewer when it is high. By carefully selecting moneyness, expiry, and timing, the investor avoids overpaying for protection, maintaining an optimal risk-return balance while hedging against market turbulence.
Backtest performance
Annualised return8.63%
Volatility8.44%
Beta0.636
Sharpe ratio1.02
Sortino ratio0.504
Maximum drawdown-10.47%
Win rate35%
Full Python code
from AlgorithmImports import *
class PortfolioHedgingUsingVIXOptions(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2010, 1, 1)
self.SetCash(1000000)
data = self.AddEquity("SPY", Resolution.Minute)
data.SetLeverage(5)
self.spy = data.Symbol
data = self.AddEquity("IEF", Resolution.Minute)
data.SetLeverage(5)
self.ief = data.Symbol
data = self.AddEquity("VIXY", Resolution.Minute)
data.SetLeverage(5)
self.vix = data.Symbol
option = self.AddOption('VIXY', Resolution.Minute)
option.SetFilter(-20, 20, 25, 35)
def OnData(self,slice):
for i in slice.OptionChains:
chains = i.Value
# Max 2 positions - spy and ief are opened. That means option expired.
invested = [x.Key for x in self.Portfolio if x.Value.Invested]
if len(invested) <= 2:
calls = list(filter(lambda x: x.Right == OptionRight.Call, chains))
if not calls: return
underlying_price = self.Securities[self.vix].Price
expiries = [i.Expiry for i in calls]
# Determine expiration date nearly one month.
expiry = min(expiries, key=lambda x: abs((x.date() - self.Time.date()).days - 30))
strikes = [i.Strike for i in calls]
# Determine out-of-the-money strike.
otm_strike = min(strikes, key = lambda x:abs(x - (float(1.35) * underlying_price)))
otm_call = [i for i in calls if i.Expiry == expiry and i.Strike == otm_strike]
if otm_call:
# Option weighting.
weight = 0.0
if underlying_price >= 15 and underlying_price <= 30:
weight = 0.01
elif underlying_price > 30 and underlying_price <= 50:
weight = 0.005
if weight != 0:
options_q = int((self.Portfolio.MarginRemaining * weight) / (underlying_price * 100))
# Set max leverage.
self.Securities[otm_call[0].Symbol].MarginModel = BuyingPowerModel(5)
# Sell out-the-money call.
self.Buy(otm_call[0].Symbol, options_q)
self.SetHoldings(self.spy, 0.6)
self.SetHoldings(self.ief, 0.4)