
The strategy combines a 60% stock and 40% bond portfolio with dynamic allocation to VIX call options. It adjusts VIX option weights based on the VIX Index, with monthly rebalancing.
ASSET CLASS: ETFs, options | REGION: United States | FREQUENCY:
Monthly | MARKET: bonds, equities | KEYWORD: Portfolio, Hedging, VIX Options
I. 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.
II. 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.
III. SOURCE PAPER
A Study in Portfolio DIversification Using VIX Options [Click to Open PDF]
Dominick Paoloni, IPS Strategic Capital
<Abstract>
The search for dependable, low-cost portfolio tail protection or hedge from exogenous events such as the 1987 crash, the 2000 dot-com bubble, the 2008 credit crisis, and the 2011 European crisis continues. This study assesses the performance of a systematic VIX call buying strategy with a defined cost to hedge an equity portfolio from systemic risk. A portfolio manager must weigh these costs against those of hedging strategies that have potentially undefinable costs, for example, protective puts or shorting equity index futures. The analysis shows that a passive allocation to VIX calls has proven effective in large drawdown periods and can be accomplished by spending a relatively small defined percentage of capital when the hedge is not needed. The study applies a set of fixed rules to empirical data with the goal of optimizing ex-post the moneyness and expiry of VIX call options over the period studied. For the period of analysis, the systematic purchase of properly placed VIX calls tends to provide sufficient protection in tail risk events for minimal cost when hedging is not needed.


IV. BACKTEST PERFORMANCE
| Annualised Return | 8.63% |
| Volatility | 8.44% |
| Beta | 0.636 |
| Sharpe Ratio | 1.02 |
| Sortino Ratio | 0.504 |
| Maximum Drawdown | -10.47% |
| Win Rate | 35% |
V. 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)
VI. Backtest Performance