Quant BuffetRelax, Not Over Thinking

Using VIX to Time Options Writing

Log in to collect

Academic paper

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.

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.

Backtest performance

Annualised return10.88%
Volatility11.36%
Beta0.652
Sharpe ratio0.61
Sortino ratio0.262
Maximum drawdown-31.15%
Win rate60%

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)