
The strategy switches monthly between selling S&P 500 put options or investing in the index, based on whether the prior month’s median VIX exceeds the historical median level.
ASSET CLASS: ETFs, options | REGION: United States | FREQUENCY:
Monthly | MARKET: equities | KEYWORD: VIX
I. STRATEGY IN A NUTSHELL
Invests in S&P 500 options or the index based on VIX levels, selling puts when VIX is high and holding the index when VIX is low. Positions are adjusted monthly using the historical median VIX to optimize risk and return.
II. ECONOMIC RATIONALE
Exploits the volatility risk premium, capitalizing on overpriced options during high VIX periods and benefiting from low-volatility, positive-return environments. Conditional positioning reduces losses and is robust across time periods and market conditions.
III. SOURCE PAPER
Option Writing: Using VIX to Improve Returns [Click to Open PDF]
Malkiel, Burton G. and Rinaudo, Alex and Saha, Atanu
<Abstract>
Buy-Write and Put-Write strategies have been shown to match market returns with lower volatility resulting in higher risk-adjusted performance. The strategies benefit from the fact that implied volatility of options is generally higher than actual realized volatility. In this paper we show that this premium is higher at elevated levels of implied volatility (as represented by the VIX index level). Based on this finding we propose a simple conditional strategy in which one sells options at elevated levels of the VIX. Using data from 1990 through 2018, we find that this conditional strategy outperforms both the market and continuous option selling strategies on an absolute and risk-adjusted basis.
IV. BACKTEST PERFORMANCE
| Annualised Return | 10.88% |
| Volatility | 11.36% |
| Beta | 0.652 |
| Sharpe Ratio | 0.61 |
| Sortino Ratio | 0.262 |
| Maximum Drawdown | -31.15% |
| Win Rate | 60% |
V. FULL PYTHON CODE
from collections import deque
from AlgorithmImports import *
import numpy as np
from QuantConnect.Python import PythonQuandl
class UsingVIXtoTimeOptionsWriting(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2015, 1, 1)
self.SetCash(100000)
self.symbol = self.AddEquity("SPY", Resolution.Minute).Symbol
data = self.AddEquity("BIL", Resolution.Minute)
data.SetLeverage(2)
self.bills = data.Symbol
# SPY options.
option = self.AddOption("SPY", Resolution.Minute)
option.SetFilter(-20, 20, 25, 35)
# Vix spot.
self.vix_spot = self.AddData(CBOE, 'VIX', Resolution.Daily).Symbol
# VIX historical monthly data.
self.data = None
# Get vix history.
history = self.History(self.vix_spot, 10*12*30, Resolution.Daily)
if 'close' in history.columns:
closes = history['close']
self.data = deque(closes)
# Next expiration date.
self.expiration_date = None
def OnData(self, slice):
# store VIX price
if self.vix_spot in slice and slice[self.vix_spot]:
price = slice[self.vix_spot].Value
self.data.append(price)
# Open new trades only on market close.
if not (self.Time.hour == 15 and self.Time.minute == 59):
return
# At least year of data is ready.
if len(self.data) < 12 * 30: return
if self.expiration_date:
if self.Time.date() < self.expiration_date.date():
return
if self.Portfolio.Invested:
self.Liquidate()
vix_median = np.median(self.data)
# Last month VIX median.
vix_median_t1 = np.median([x for x in self.data][-21:])
for i in slice.OptionChains:
chains = i.Value
if not self.Portfolio.Invested:
puts = list(filter(lambda x: x.Right == OptionRight.Put, chains))
if not puts: return
underlying_price = self.Securities[self.symbol].Price
expiries = [i.Expiry for i in puts]
# 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 puts]
# determine at-the-money strike
strike = min(strikes, key=lambda x: abs(x-underlying_price))
atm_put = [i for i in puts if i.Expiry == expiry and i.Strike == strike]
if atm_put:
if not self.expiration_date:
self.expiration_date = atm_put[0].Expiry
return
self.expiration_date = atm_put[0].Expiry
if vix_median_t1 < vix_median:
self.SetHoldings(self.symbol, 1)
return
options_q = int(self.Portfolio.MarginRemaining / (underlying_price * 100))
self.Securities[atm_put[0].Symbol].MarginModel = BuyingPowerModel(5)
self.SetHoldings(self.bills, 1)
self.Sell(atm_put[0].Symbol, options_q)
if self.Portfolio.Invested:
self.Liquidate(self.symbol)
VI. Backtest Performance
=