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.

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 Return10.88%
Volatility11.36%
Beta0.652
Sharpe Ratio0.61
Sortino Ratio0.262
Maximum Drawdown-31.15%
Win Rate60%

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

=

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading