Quant BuffetRelax, Not Over Thinking

Portfolio Hedging Using VIX Options

Log in to collect

Academic 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)