
“The portfolio consists of S&P 500 and VIX futures, with risk-based weightings using a GARCH model. Long or short VIX positions are determined by the VIX premium, rebalanced daily.”
ASSET CLASS: CFDs, ETFs, futures | REGION: United States | FREQUENCY:
Daily | MARKET: equities | KEYWORD: VIX, Term Structure
I. STRATEGY IN A NUTSHELL
The portfolio consists of positions in the S&P 500 Index and constant maturity VIX futures, with their weightings based on equal risk contribution, estimated using a GARCH(1,1) model. The VIX premium, calculated with a one-day lag, determines whether to hold a long or short position in VIX futures. A positive VIX premium suggests a short position, while a negative VIX premium signals a long position. The portfolio is rebalanced daily to adjust for the VIX term structure and market movements, executing trades based on the shape of the VIX term structure.
II. ECONOMIC RATIONALE
The strategy combines equity exposure with higher weights during market uptrends and volatility trading exposure during market downturns. The weightings are determined using a GARCH model, ensuring stability during stress while maintaining performance during uptrends. In 2008, the average VIX futures allocation was 32.23%, while in 2017, it was 14.49%. This resulted in an average monthly return of 2.08% and a Sharpe ratio of 1.19, outperforming the S&P with its returns of 0.76% and Sharpe ratio of 0.58. The strategy uses the VIX premium, the difference between VIX futures and the VIX Index, which is positive during low volatility periods and negative during high volatility periods. The VIX premium was positive over 80% of the sample period.
III. SOURCE PAPER
Portfolio Strategies for Volatility Investing [Click to Open PDF]
- Jim Campasano, University of Massachusetts Amherst – Isenberg School of Management; Kansas State University – Department of Finance
<Abstract>
The VIX premium has been shown to hold predictive power over volatility returns and investment risk. Applied within a portfolio construct, this study proposes a conditional strategy which allocates to market and volatility risk. While the strategy is predominantly short volatility, the strategy owns volatility during much of the financial crises. Both long and short volatility allocations prove profitable over the sample period, producing a portfolio more consistently profitable than the S&P 500 Index and related strategies.

IV. BACKTEST PERFORMANCE
| Annualised Return | 23.58% |
| Volatility | 19.92% |
| Beta | 0.894 |
| Sharpe Ratio | 1.18 |
| Sortino Ratio | 0.718 |
| Maximum Drawdown | N/A |
| Win Rate | 87% |
V. FULL PYTHON CODE
from AlgorithmImports import *
import numpy as np
#endregion
class CombiningVIXFuturesTermStructureStrategySP500Index(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2010, 1, 1)
self.SetCash(10_000_000)
self.market: Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.vix_futures: Symbol = self.AddEquity('VIXY', Resolution.Daily).Symbol
# VIX and VIX3M filter
self.vix: Symbol = self.AddData(CBOE, 'VIX', Resolution.Daily).Symbol
self.vix_3M: Symbol = self.AddData(CBOE, 'VIX3M', Resolution.Daily).Symbol
self.traded_symbol: List[Symbol] = [self.market, self.vix_futures]
# daily price data
period:int = 20
self.price_data: Dict[Symbol, RollingWindow] = {symbol : RollingWindow[float](period) for symbol in self.traded_symbol}
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.settings.daily_precise_end_time = False
def OnData(self, slice: Slice) -> None:
# store daily prices
for symbol in self.traded_symbol:
if symbol in slice and slice[symbol]:
self.price_data[symbol].Add(slice[symbol].Value)
# rebalance daily
if all(x in slice and slice[x] for x in self.traded_symbol + [self.vix, self.vix_3M]):
if all(self.price_data[x].IsReady for x in self.traded_symbol):
vix: float = slice[self.vix].Value
vix_3m: float = slice[self.vix_3M].Value
vix_volatility: float = self.volatility(list(self.price_data[self.vix_futures])[1:]) # one-day lag
market_volatility: float = self.volatility(list(self.price_data[self.market])[1:]) # one-day lag
total_volatility: float = 1. / vix_volatility + 1. / market_volatility
market_w: float = (1. / market_volatility) / total_volatility
vix_w: float = (1. / vix_volatility) / total_volatility
self.SetHoldings(self.market, market_w)
if vix_3m >= vix:
# contango -> short vixy
self.SetHoldings(self.vix_futures, -vix_w)
else:
# backwardation -> long vixy
self.SetHoldings(self.vix_futures, vix_w)
def volatility(self, daily_prices: List[float]) -> float:
daily_prices: np.ndarray = np.array(daily_prices)
returns: np.ndarray = daily_prices[:-1] / daily_prices[1:] - 1
return np.std(returns)
VI. Backtest Performance