Skip to content
All library documents

Estimating VIX-Style Implied Volatility from Options

Code quant-trading

Summary

The script outlines a VIX-style calculation from options prices. It estimates a forward level from the call-put price difference at the strike with the smallest disparity, selects the adjacent strike not above that forward, and aggregates out-of-the-money call and put prices into a variance estimate. It then interpolates variance between two maturities to a target horizon and annualizes the result as a volatility index. Supporting steps fill missing yield observations, calculate time to settlement, and exclude zero-settlement options beyond consecutive zero quotes.

The example applies the procedure to European natural-gas options, using prior settlement prices because bid and ask quotes are unavailable. The code points to the CBOE methodology and variance-swap relationship but supplies no validation results. Its calendar assumptions, use of prior settlements, maturity selection, and data-specific handling can affect estimates; the shown implementation should not be assumed to reproduce the official VIX or transfer unchanged to other markets.

Key ideas

  • The method estimates a forward level using call-put prices at the strike with the smallest price difference.
  • It aggregates out-of-the-money option prices across strikes to estimate variance for each maturity.
  • Variance estimates for two maturities are interpolated to a target horizon and annualized.
  • The example substitutes prior settlement prices for bid-ask midpoints because quote data are unavailable.
  • Calendar rules and data choices limit how closely the script may match an official volatility index.

Tags

Full text
# VIX Calculator.py


```py

# coding: utf-8

# In[1]:


#check cboe white paper on the details of computation
# http://www.cboe.com/micro/vix/vixwhite.pdf
#check this awesome article on the connection between vix and variance swap
# https://berentlunde.netlify.app/post/the-fear-index-vix-and-variance-swaps
#check this paper on the variance swap
# https://www.researchgate.net/publication/246869706_More_Than_You_Ever_Wanted_to_Know_About_Volatility_Swaps
import pandas as pd
import datetime as dt
import dateutil
import decimal
import os
import numpy as np
os.chdir('K:/ecole/github/televerser/données')


# In[2]:


#fill weekend and holiday missing cmt rate
def cmt_rate_fill_date(cmt_rate):

    #get missing date as well
    complete_date=pd.date_range(cmt_rate['Date'].min(),cmt_rate['Date'].max())

    #reindex to fill missing date
    cmt_rate=cmt_rate.pivot(index='Date',columns='maturity',values='value')
    cmt_rate=cmt_rate.reindex(complete_date)

    #cleanse
    cmt_rate.index.name='Date'
    cmt_rate.reset_index(inplace=True)

    #ffill
    cmt_rate.fillna(method='ffill',inplace=True)

    #revert to the original form
    cmt_rate=cmt_rate.melt(id_vars='Date',value_vars=['1 Mo', '1 Yr', '10 Yr', '2 Mo', '2 Yr', '20 Yr', '3 Mo',
           '3 Yr', '30 Yr', '5 Yr', '6 Mo', '7 Yr'])
    
    return cmt_rate


# In[3]:


#get settlement day in datetime format
#you can scrape the following website of cme instead
# f'https://www.cmegroup.com/CmeWS/mvc/ProductCalendar/Options/{futures_id}?optionTypeFilter=&optionTypeFilter='
#however it has some conflicts with cme globex s own holiday calendar
# https://www.cmegroup.com/tools-information/holiday-calendar.html
def get_settlement_day(current_day,time_horizon,
                       expiration_day,expiration_hour,
                       public_holidays):

    #get month end at expiration hour
    month_end=current_day+dateutil.relativedelta.relativedelta(day=31,hour=expiration_hour,
                                                                    minute=0,second=0,
                                                                   microsecond=0,
                                                                    months=+time_horizon-1)

    
    #adjust to the nth last day of the month
    settlement_day=month_end
    
    #use loop to skip non trading day
    correct=False
    
    #count the month end if its a weekday
    counter=1 if dt.datetime.weekday(settlement_day) in range(5) else 0
    while not correct:
        
        #cannot be a weekend day or a federal holiday
        if (dt.datetime.weekday(settlement_day) in range(5)) and \
        (str(settlement_day)[:10] not in public_holidays) and \
        counter>expiration_day-1:
            correct=True
        else:
            settlement_day-=dt.timedelta(days=1)
            
            #weekday is counted even if its federal holiday
            if (dt.datetime.weekday(settlement_day) in range(5)):
                counter+=1
        
    return settlement_day


# In[4]:


#get time to expiration
#instead of current day+settlement day+other days
#we can directly use timedelta to obtain the result in minutes
#no need to skip any holidays in this calculation
def get_time_to_expiration(current_day,time_horizon,
                           expiration_day,expiration_hour,
                           public_holidays):

    #get settlement day
    settlement_day=get_settlement_day(current_day,time_horizon,
                                       expiration_day,expiration_hour,
                                       public_holidays)

    #convert seconds to minutes
    #divided by minutes in a year
    return (settlement_day-current_day).total_seconds()/60/525600


# In[5]:


#get forward level and strike price no larger than forward
def get_forward_strike(options,
                       interest_rate,
                       time_to_expiration):

    #cleanse
    options=options.sort_values('options-strikePrice')
    find_forward=options.pivot(index='options-strikePrice',
                         columns='options-optiontype',
                        values='options-priorSettle')

    #find the forward level with the least put call disparity
    min_diff_ind=(find_forward['call']-find_forward['put']).apply(abs).idxmin()
    min_diff=float(decimal.Decimal(find_forward['call'][min_diff_ind].astype(str))-decimal.Decimal(find_forward['put'][min_diff_ind].astype(str)))
    forward=min_diff_ind+np.e**(interest_rate*time_to_expiration)*(min_diff)

    #find the strike price no larger than forward level
    strike=find_forward.index[find_forward.index<=forward][-1]
    
    return forward,strike


# In[6]:


#select out of money call options
#with zero prior settle exclusion
def get_options_call_inclusion(options,strike):

    options_call=options[options['options-optiontype']=='call']

    #select outta money options
    #cuz they are usually more liquid
    options_call_otm=options_call[options_call['options-strikePrice']>strike]

    #sort by strike price
    options_call_otm=options_call_otm.sort_values('options-strikePrice')
    options_call_otm.reset_index(inplace=True,drop=True)

    #find all zero prior settle options
    options_call_otm_zeros=options_call_otm[options_call_otm['options-priorSettle']==0]
    options_call_otm_zeros.reset_index(inplace=True)

    #as we dont have bid and ask
    #we use prior settle instead
    #once options with consecutive strike prices have zero prior settle
    #any further out of money options would be excluded
    if options_call_otm_zeros[options_call_otm_zeros['index'].diff()==1].empty:
        ind=len(options_call_otm)
    else:
        ind=options_call_otm_zeros['index'][options_call_otm_zeros['index'].diff()==1].iloc[0]

    #exclude all zero prior settle options
    options_call_inclusion=options_call_otm[options_call_otm['options-priorSettle']!=0][:ind]

    #cleanse
    options_call_inclusion.reset_index(inplace=True,drop=True)
    
    return options_call_inclusion


# In[7]:


#select out of money put options
#with zero prior settle exclusion
def get_options_put_inclusion(options,strike):

    options_put=options[options['options-optiontype']=='put']

    #select outta money options
    #cuz they are usually more liquid
    options_put_otm=options_put[options_put['options-strikePrice']<strike]

    #sort by strike price
    options_put_otm=options_put_otm.sort_values('options-strikePrice',
                                                ascending=False)
    options_put_otm.reset_index(inplace=True,drop=True)

    #find all zero prior settle options
    options_put_otm_zeros=options_put_otm[options_put_otm['options-priorSettle']==0]
    options_put_otm_zeros.reset_index(inplace=True)

    #as we dont have bid and ask
    #we use prior settle instead
    #once options with consecutive strike prices have zero prior settle
    #any further out of money options would be excluded
    if options_put_otm_zeros[options_put_otm_zeros['index'].diff()==1].empty:
        ind=len(options_put_otm)
    else:
        ind=options_put_otm_zeros['index'][options_put_otm_zeros['index'].diff()==1].iloc[0]

    #exclude all zero prior settle options
    options_put_inclusion=options_put_otm[options_put_otm['options-priorSettle']!=0][:ind]

    #cleanse
    options_put_inclusion.reset_index(inplace=True,drop=True)
    
    return options_put_inclusion


# In[8]:


#compute sigma based upon variance swap formula
def compute_sigma(forward,strike,
                  options_call_inclusion,
                  options_put_inclusion,
                  interest_rate,time_to_expiration):

    contributions=0.0
    for i in [options_call_inclusion,
              options_put_inclusion]:
        for j in i.index:
            
            #interval between strike prices
            if j-1<0:
                delta=abs(i['options-strikePrice'][j]-i['options-strikePrice'][j+1])
            elif j+1==len(i):
                delta=abs(i['options-strikePrice'][j]-i['options-strikePrice'][j-1])
            else:
                delta=abs(i['options-strikePrice'][j-1]-i['options-strikePrice'][j+1])/2

            contributions+=i['options-priorSettle'][j]*np.exp(interest_rate*time_to_expiration)*delta/(i['options-strikePrice'][j])**2

    #replace bid ask spread midpoint with prior settle
    sigma=contributions*2/time_to_expiration-((forward/strike-1)**2)/time_to_expiration
    
    return sigma


# In[9]:


#weighted avg of vix
def compute_vix(time_to_expiration_front,
                time_to_expiration_rear,
                sigma_front,sigma_rear,
                num_of_mins_timeframe,
                num_of_mins_year):

    sum1=time_to_expiration_front*sigma_front*(time_to_expiration_rear*num_of_mins_year-num_of_mins_timeframe)/(time_to_expiration_rear*num_of_mins_year-time_to_expiration_front*num_of_mins_year)
    sum2=time_to_expiration_rear*sigma_rear*(num_of_mins_timeframe-time_to_expiration_front*num_of_mins_year)/(time_to_expiration_rear*num_of_mins_year-time_to_expiration_front*num_of_mins_year)
    vix=((sum1+sum2)*num_of_mins_year/num_of_mins_timeframe)**0.5*100
    
    return vix


# In[10]:


#aggregate all functions into one
def vix_calculator(df,cmt_rate,calendar,
                   options_id,tradedate,
                   timeframe_front,timeframe_rear,
                   expiration_hour,expiration_day,
                   num_of_mins_timeframe,num_of_mins_year):
    
    #us federal holidays
    federal_holidays=calendar['DATE'].tolist()
    
    #daily treasury yield curve rate
    interest_rate_front=cmt_rate['value'][cmt_rate['maturity']==f'{timeframe_front} Mo'][cmt_rate['Date']==tradedate].item()/100
    interest_rate_rear=cmt_rate['value'][cmt_rate['maturity']==f'{timeframe_rear} Mo'][cmt_rate['Date']==tradedate].item()/100

    #find current options
    currentoptions=df[df['options-id']==options_id][df['tradeDate']==tradedate].copy()

    #determine next term and near term contracts
    nextterm=dt.datetime.strptime(tradedate,'%Y-%m-%d')+dt.timedelta(days=30*timeframe_rear)
    nearterm=dt.datetime.strptime(tradedate,'%Y-%m-%d')+dt.timedelta(days=30*timeframe_front)

    #determine rear month and front month
    rearmonth=pd.to_datetime(f'{nextterm.year}-{nextterm.month}-1')
    frontmonth=pd.to_datetime(f'{nearterm.year}-{nearterm.month}-1')

    #create dataframe copies
    options_rear=currentoptions[currentoptions['futures-expirationDate']==rearmonth].copy()
    options_front=currentoptions[currentoptions['futures-expirationDate']==frontmonth].copy()

    #take futures updated datetime as the current one
    current_day_front=options_front['futures-updated'].iloc[0]
    current_day_rear=options_rear['futures-updated'].iloc[0]

    #get time to expiration
    time_to_expiration_front=get_time_to_expiration(current_day_front,
                                                    timeframe_front,
                                                    expiration_day,
                                                    expiration_hour,
                                                    federal_holidays)

    time_to_expiration_rear=get_time_to_expiration(current_day_rear,
                                                    timeframe_rear,
                                                    expiration_day,
                                                    expiration_hour,
                                                    federal_holidays)

    #get forward level and strike price no larger than forward
    forward_front,strike_front=get_forward_strike(options_front,
                                                  interest_rate_front,
                                                  time_to_expiration_front)

    forward_rear,strike_rear=get_forward_strike(options_rear,
                                                  interest_rate_rear,
                                                  time_to_expiration_rear)

    #prepare options for calculation
    options_call_front_inclusion=get_options_call_inclusion(
        options_front,strike_front)

    options_call_rear_inclusion=get_options_call_inclusion(
        options_rear,strike_rear)

    options_put_front_inclusion=get_options_put_inclusion(
        options_front,strike_front)

    options_put_rear_inclusion=get_options_put_inclusion(
        options_rear,strike_rear)

    #use put call avg 
    #if strike price exists in the out of money dataset
    for i in [options_call_front_inclusion,
              options_put_front_inclusion]:
        if strike_front in i['options-strikePrice'].tolist():
            i['opt'][i['options-strikePrice']==strike_front]=options_front['options-priorSettle'][options_front['options-strikePrice']==strike_front].mean()

    for i in [options_call_rear_inclusion,
              options_put_rear_inclusion]:
        if strike_rear in i['options-strikePrice'].tolist():
            i['opt'][i['options-strikePrice']==strike_rear]=options_rear['options-priorSettle'][options_rear['options-strikePrice']==strike_rear].mean()


    #compute sigmas
    sigma_front=compute_sigma(forward_front,strike_front,
                      options_call_front_inclusion,
                      options_put_front_inclusion,
                      interest_rate_front,time_to_expiration_front)

    sigma_rear=compute_sigma(forward_rear,strike_rear,
                      options_call_rear_inclusion,
                      options_put_rear_inclusion,
                      interest_rate_rear,time_to_expiration_rear)

    #enfin,vix!!!
    vix=compute_vix(time_to_expiration_front,
                    time_to_expiration_rear,
                    sigma_front,sigma_rear,
                    num_of_mins_timeframe,
                    num_of_mins_year)
    
    return vix


# In[11]:


#compute 3 month ahead vix for henry hub european options
def main():

    #read data
    df=pd.read_csv('henry hub european options.csv')
    calendar=pd.read_csv('cme holidays.csv')
    cmt_rate=pd.read_csv('treasury yield curve rates.csv')

    #datetime format
    df['futures-expirationDate']=pd.to_datetime(df['futures-expirationDate'])
    df['tradeDate']=pd.to_datetime(df['tradeDate'])
    df['futures-updated']=pd.to_datetime(df['futures-updated'])
    df['options-updated']=pd.to_datetime(df['options-updated'])
    cmt_rate['Date']=pd.to_datetime(cmt_rate['Date'])

    #fill weekend and holiday missing cmt rate
    cmt_rate=cmt_rate_fill_date(cmt_rate)

    #preset parameters
    #check contractSpecs of the underlying asset
    #in our case
    # https://www.cmegroup.com/trading/energy/natural-gas/natural-gas_contractSpecs_options.html#optionProductId=1352
    options_id=1352;tradedate='2020-11-12'
    timeframe_front=2;timeframe_rear=3
    expiration_hour=16;expiration_day=4
    num_of_mins_timeframe=timeframe_rear*30*24*60
    num_of_mins_year=365*24*60

    #vix!!!
    vix=vix_calculator(df,cmt_rate,calendar,
                       options_id,tradedate,
                       timeframe_front,timeframe_rear,
                       expiration_hour,expiration_day,
                       num_of_mins_timeframe,num_of_mins_year)

    print(vix)


# In[12]:


if __name__ == '__main__':
    main()


```

Shown in full with attribution under the source's licence. Licence: Apache-2.0

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.