Vix Backwardation and the SPX

For a while I've been curious to see if there is any effect on a backwardated term structure in Vix Futures on future returns in the SPX.

Contago and Backwardation

A brief background on why this might be interesting...

The VIX measures the price that traders are willing to buy options to protect their portfolio. The spot VIX measures this price. You can buy futures on the VIX. Essentially you are making a bet where the VIX will settle on the date of expiration of the VIX future contract. At settlement you get paid the amount your future is worth. Thus, the futures trade off the price that traders think the index will be at settlement. In times of stress trader run to buy options, pushing the VIX up. Since the VIX is mean reverting this will pull the front month up more than the back month since traders figure that over time the VIX will return to is average levels of about 20. This term structure where the front month is greater than the back month is referred to as backwardation.

However, the "natural" term structure for VIX Futures is contango since they are somewhat tied to the price of SPX options which are naturally more expensive further out in time since there is more uncertainty in the future (even adjusted for time) -- this is why back month options trade at a higher vol (usually) than front month options.

During big down drafts we see the VIX future curve go into steep backwardation.

Here's the question:

Does this backwardation happen quickly enough into the drawdown to get you out?


In [1]:
import pandas as pd
import pandas.io.data as web
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import seaborn
from datetime import datetime, timedelta

%matplotlib inline

In [2]:
df = web.DataReader("^GSPC", 'yahoo', datetime(2000, 1, 1), datetime.today())

In [3]:
raw_vix_df = web.DataReader("^VIX", 'yahoo', datetime(2000, 1, 1), datetime.today())
raw_vix_df['vix_close'] = raw_vix_df['Close']
raw_vix_df = raw_vix_df['vix_close'] # drop all the HL shit

In [4]:
vix_df = pd.read_html("http://vixcentral.com/historical/?days=10000", header=0)[0]
vix_df = vix_df.drop(vix_df.index[-1:]) # get rid of last row
vix_df = vix_df.set_index('Date')
vix_df.index = pd.to_datetime(vix_df.index)

In [5]:
df['Close'].plot(figsize=(16, 6))
plt.xlabel('Date', fontsize=14)
plt.ylabel('SPX Close', fontsize=14)
plt.title("Our frienemy, the SPX", fontsize=16)


Out[5]:
<matplotlib.text.Text at 0x10e73c400>

In [6]:
# calculate degree of backwardation between month 1 and 2
vix_df['f2-f1'] = vix_df['F2'] - vix_df['F1']
vix_df['backwardated'] = vix_df['f2-f1'] < 0

In [7]:
# join our tables and sort with date ascending
master = df.join(raw_vix_df, how='inner')
master = master.join(vix_df, how='inner')
master = master.sort_index()

In [8]:
# compute rolling returns for 1 day, 5 days, 2 weeks, 1 month, 3 months, 6 months and 12 months
# note this is tricky than it looks -- our dataframe is in acending order, 
# so pct_change(periods=1) calculates the chage from day1 to day2, but this change is aligned with day2 so 
# we *must* shift is back the same number of periods that the change is calculated over
master['1d'] = master['Close'].pct_change(periods=1).shift(-1) * 100
master['5d'] = master['Close'].pct_change(periods=5).shift(-5) * 100
master['10d'] = master['Close'].pct_change(periods=10).shift(-10) * 100
master['1m'] = master['Close'].pct_change(periods=20).shift(-20) * 100
master['3m'] = master['Close'].pct_change(periods=60).shift(-60) * 100
master['6m'] = master['Close'].pct_change(periods=120).shift(-120) * 100
master['12m'] = master['Close'].pct_change(periods=250).shift(-250) * 100

In [9]:
# verify the pecentage changes look good
master[["Close", "1d", "5d"]]


Out[9]:
Close 1d 5d
2009-10-23 1079.599976 -1.171733 -4.020937
2009-10-26 1066.949951 -0.331779 -2.255958
2009-10-27 1063.410034 -1.954094 -1.692668
2009-10-28 1042.630005 2.251995 0.371176
2009-10-29 1066.109985 -2.806469 0.048777
2009-10-30 1036.189941 0.645641 3.195371
2009-11-02 1042.880005 0.242600 4.813588
2009-11-03 1045.410034 0.104262 4.553235
2009-11-04 1046.500000 1.923555 4.969901
2009-11-05 1066.630005 0.250325 1.932253
2009-11-06 1069.300049 2.223876 2.261286
2009-11-09 1093.079956 -0.006399 1.483889
2009-11-10 1093.010010 0.503198 1.583694
2009-11-11 1098.510010 -1.025937 1.027759
2009-11-12 1087.239990 0.573929 0.704539
2009-11-13 1093.479980 1.446763 -0.192045
2009-11-16 1109.300049 0.091941 -0.275855
2009-11-17 1110.319946 -0.046824 -0.420592
2009-11-18 1109.800049 -1.342586 0.074784
2009-11-19 1094.900024 -0.321492 -0.311447
2009-11-20 1091.380005 1.361578 0.389415
2009-11-23 1106.239990 -0.053331 0.236838
2009-11-24 1105.650024 0.450412 0.324693
2009-11-25 1110.630005 -1.723348 -0.964314
2009-11-27 1091.489990 0.379299 1.327542
2009-11-30 1095.630005 1.207523 0.695490
2009-12-01 1108.859985 0.034270 -1.525895
2009-12-02 1109.239990 -0.840210 -1.198121
2009-12-03 1099.920044 0.550943 0.220919
2009-12-04 1105.979980 -0.246838 0.038884
... ... ... ...
2015-08-07 2077.570068 1.280817 0.672419
2015-08-10 2104.179932 -0.955710 -0.082692
2015-08-11 2084.070068 0.095005 0.616575
2015-08-12 2086.050049 -0.127521 -0.308715
2015-08-13 2083.389893 0.391196 -2.287614
2015-08-14 2091.540039 0.521142 -5.768478
2015-08-17 2102.439941 -0.262553 -9.951770
2015-08-18 2096.919922 -0.825488 -10.935560
2015-08-19 2079.610107 -2.110017 -6.688758
2015-08-20 2035.729980 -3.185097 -2.361312
2015-08-21 1970.890015 -3.941369 0.912277
2015-08-24 1893.209961 -1.352200 4.171227
2015-08-25 1867.609985 3.903386 2.475891
2015-08-26 1940.510010 2.429775 0.430298
2015-08-27 1987.660034 0.060874 -1.837841
2015-08-28 1988.869995 -0.839167 -3.401430
2015-08-31 1972.180054 -2.957645 -0.140455
2015-09-01 1913.849976 1.829297 1.472951
2015-09-02 1948.859985 0.116479 0.176003
2015-09-03 1951.130005 -1.532960 0.508426
2015-09-04 1921.219971 2.508305 1.655722
2015-09-08 1969.410034 -1.389756 0.440738
2015-09-09 1942.040039 0.527796 2.742993
2015-09-10 1952.290039 0.448704 1.941818
2015-09-11 1961.050049 -0.408966 -0.151454
2015-09-14 1953.030029 1.283131 NaN
2015-09-15 1978.089966 0.870541 NaN
2015-09-16 1995.310059 -0.256106 NaN
2015-09-17 1990.199951 -1.613908 NaN
2015-09-18 1958.079956 NaN NaN

1486 rows × 3 columns


In [10]:
# next we'll write a custom formatter
N = len(master.index)
ind = np.arange(N)  # the evenly spaced plot indices

def format_date(x, pos=None):
    thisind = np.clip(int(x+0.5), 0, N-2)
    print(thisind)
    return master.iloc[:thisind][0].format().pop()

In [11]:
print(master.iloc[:1].index.format().pop())


2009-10-23

In [12]:
fig, ax = plt.subplots()
master['Close'].plot(ax=ax, figsize=(16, 6))
for idx, row in master.iterrows():
    if row['backwardated']:
        ax.axvspan(idx, idx+timedelta(days=1), facecolor='red', edgecolor='none', alpha=.2)
        
plt.xlabel('Date (Red denotes periods of backwardation)', fontsize=14)
plt.ylabel('SPX Close', fontsize=14)
plt.title("SPX Value with Backwardation Highlighted", fontsize=16)


Out[12]:
<matplotlib.text.Text at 0x10cb14a58>

In [13]:
plot = master['vix_close'].plot(figsize=(16, 6))
for idx, row in master.iterrows():
    if row['backwardated']:
        plot.axvspan(idx, idx+timedelta(days=1), facecolor='red', edgecolor='none', alpha=.2)
        
plt.xlabel('Date (Red denotes periods of backwardation)', fontsize=14)
plt.ylabel('VIX Close', fontsize=14)
plt.title("VIX Close with Backwardation Highlighted", fontsize=16)


Out[13]:
<matplotlib.text.Text at 0x108f0d978>

Now, lets dig into the data, segmenting periods of backwardation and contango on the VIX futures curve.

For each rolling return we'll:

  1. Plot the SPX with the backwardation highlighted in red.
  2. Plot the rolling return with the backwardation highlighted in red.
  3. Look at some stats about the return distribution

In [14]:
plot = master['1d'].plot(figsize=(16, 6))
for idx, row in master.iterrows():
    if row['backwardated']:
        plot.axvspan(idx, idx+timedelta(days=1), facecolor='red', edgecolor='none', alpha=.2)
        
plt.xlabel('Date (Red denotes periods of backwardation)', fontsize=14)
plt.ylabel('Rolling 1 day percentage returns', fontsize=14)


Out[14]:
<matplotlib.text.Text at 0x108f785c0>

In [15]:
plt1, plt2 = master['1d'].hist(by=master['backwardated'], bins=20, figsize=(16, 6))

plt1.set_title("Contango", fontsize=16)
plt1.set_xlabel('Bucketed 1d Returns', fontsize=14)
plt1.set_ylabel('Ocurrances', fontsize=14)
plt2.set_title("Backwardation", fontsize=16)
plt2.set_xlabel('Bucketed 1d Returns', fontsize=14)
plt2.set_ylabel('Ocurrances', fontsize=14)


Out[15]:
<matplotlib.text.Text at 0x1120822b0>

In [16]:
plot = master['5d'].plot(figsize=(16, 6))
for idx, row in master.iterrows():
    if row['backwardated']:
        plot.axvspan(idx, idx+timedelta(days=1), facecolor='red', edgecolor='none', alpha=.2)
        
plt.xlabel('Date (Red denotes periods of backwardation)', fontsize=14)
plt.ylabel('Rolling 5 day percentage returns', fontsize=14)


Out[16]:
<matplotlib.text.Text at 0x112230f28>

In [17]:
plt1, plt2 = master['5d'].hist(by=master['backwardated'], bins=20, figsize=(16, 6))

plt1.set_title("Contango", fontsize=16)
plt1.set_xlabel('Bucketed 5d Returns', fontsize=14)
plt1.set_ylabel('Ocurrances', fontsize=14)
plt2.set_title("Backwardation", fontsize=16)
plt2.set_xlabel('Bucketed 5d Returns', fontsize=14)
plt2.set_ylabel('Ocurrances', fontsize=14)


Out[17]:
<matplotlib.text.Text at 0x1125dbe80>

In [18]:
plot = master['10d'].plot(figsize=(16, 6))
for idx, row in master.iterrows():
    if row['backwardated']:
        plot.axvspan(idx, idx+timedelta(days=1), facecolor='red', edgecolor='none', alpha=.2)
        
plt.xlabel('Date (Red denotes periods of backwardation)', fontsize=14)
plt.ylabel('Rolling 10 day percentage returns', fontsize=14)


Out[18]:
<matplotlib.text.Text at 0x112844470>

In [19]:
plt1, plt2 = master['10d'].hist(by=master['backwardated'], bins=20, figsize=(16, 6))

plt1.set_title("Contango", fontsize=16)
plt1.set_xlabel('Bucketed 10d Returns', fontsize=14)
plt1.set_ylabel('Ocurrances', fontsize=14)
plt2.set_title("Backwardation", fontsize=16)
plt2.set_xlabel('Bucketed 10d Returns', fontsize=14)
plt2.set_ylabel('Ocurrances', fontsize=14)


Out[19]:
<matplotlib.text.Text at 0x112c93908>

In [20]:
plot = master['1m'].plot(figsize=(16, 6))
for idx, row in master.iterrows():
    if row['backwardated']:
        plot.axvspan(idx, idx+timedelta(days=1), facecolor='red', edgecolor='none', alpha=.2)
        
plt.xlabel('Date (Red denotes periods of backwardation)', fontsize=14)
plt.ylabel('Rolling 1 month percentage returns', fontsize=14)


Out[20]:
<matplotlib.text.Text at 0x112ef6f98>

In [21]:
plt1, plt2 = master['1m'].hist(by=master['backwardated'], bins=20, figsize=(16, 6))

plt1.set_title("Contango", fontsize=16)
plt1.set_xlabel('Bucketed 1m Returns', fontsize=14)
plt1.set_ylabel('Ocurrances', fontsize=14)
plt2.set_title("Backwardation", fontsize=16)
plt2.set_xlabel('Bucketed 1m Returns', fontsize=14)
plt2.set_ylabel('Ocurrances', fontsize=14)


Out[21]:
<matplotlib.text.Text at 0x111233d30>

In [22]:
plot = master['3m'].plot(figsize=(16, 6))
for idx, row in master.iterrows():
    if row['backwardated']:
        plot.axvspan(idx, idx+timedelta(days=1), facecolor='red', edgecolor='none', alpha=.2)
        
plt.xlabel('Date (Red denotes periods of backwardation)', fontsize=14)
plt.ylabel('Rolling 3 month percentage returns', fontsize=14)


Out[22]:
<matplotlib.text.Text at 0x1134384a8>

In [23]:
plt1, plt2 = master['3m'].hist(by=master['backwardated'], bins=20, figsize=(16, 6))

plt1.set_title("Contango", fontsize=16)
plt1.set_xlabel('Bucketed 3m Returns', fontsize=14)
plt1.set_ylabel('Ocurrances', fontsize=14)
plt2.set_title("Backwardation", fontsize=16)
plt2.set_xlabel('Bucketed 3m Returns', fontsize=14)
plt2.set_ylabel('Ocurrances', fontsize=14)


Out[23]:
<matplotlib.text.Text at 0x113843b38>

In [24]:
plot = master['6m'].plot(figsize=(16, 6))
for idx, row in master.iterrows():
    if row['backwardated']:
        plot.axvspan(idx, idx+timedelta(days=1), facecolor='red', edgecolor='none', alpha=.2)
        
plt.xlabel('Date (Red denotes periods of backwardation)', fontsize=14)
plt.ylabel('Rolling 6 month percentage returns', fontsize=14)


Out[24]:
<matplotlib.text.Text at 0x11126cef0>

In [25]:
plt1, plt2 = master['6m'].hist(by=master['backwardated'], bins=20, figsize=(16, 6))

plt1.set_title("Contango", fontsize=16)
plt1.set_xlabel('Bucketed 6m Returns', fontsize=14)
plt1.set_ylabel('Ocurrances', fontsize=14)
plt2.set_title("Backwardation", fontsize=16)
plt2.set_xlabel('Bucketed 6m Returns', fontsize=14)
plt2.set_ylabel('Ocurrances', fontsize=14)


Out[25]:
<matplotlib.text.Text at 0x113f522b0>

In [26]:
plot = master['12m'].plot(figsize=(16, 6))
for idx, row in master.iterrows():
    if row['backwardated']:
        plot.axvspan(idx, idx+timedelta(days=1), facecolor='red', edgecolor='none', alpha=.2)
        
plt.xlabel('Date (Red denotes periods of backwardation)', fontsize=14)
plt.ylabel('Rolling 12 month percentage returns', fontsize=14)


Out[26]:
<matplotlib.text.Text at 0x11405d668>

In [27]:
plt1, plt2 = master['12m'].hist(by=master['backwardated'], bins=20, figsize=(16, 6))

plt1.set_title("Contango", fontsize=16)
plt1.set_xlabel('Bucketed 12m Returns', fontsize=14)
plt1.set_ylabel('Ocurrances', fontsize=14)
plt2.set_title("Backwardation", fontsize=16)
plt2.set_xlabel('Bucketed 12m Returns', fontsize=14)
plt2.set_ylabel('Ocurrances', fontsize=14)


Out[27]:
<matplotlib.text.Text at 0x1145e8748>

In [28]:
master.groupby(master['backwardated']).describe()[['vix_close', '1d','5d', '10d', '1m', '3m', '6m', '12m']]


Out[28]:
vix_close 1d 5d 10d 1m 3m 6m 12m
backwardated
False count 1327.000000 1327.000000 1326.000000 1326.000000 1326.000000 1288.000000 1228.000000 1123.000000
mean 17.152050 0.033411 0.164018 0.379796 0.765255 2.509253 5.171100 12.852408
std 4.397970 0.842642 1.884081 2.548827 3.521301 5.575858 7.116328 7.664303
min 10.320000 -3.897590 -10.935560 -16.297682 -16.467412 -18.199885 -16.388027 -6.625103
25% 13.795000 -0.373819 -0.709205 -0.954606 -1.092262 -0.257734 1.956702 7.345828
50% 16.270000 0.054917 0.354402 0.683488 1.342844 2.885028 5.915315 13.138006
75% 19.165000 0.491808 1.302859 1.966620 3.166168 6.252867 9.345582 18.298972
max 38.320000 4.331531 7.388642 8.330232 10.099940 17.674567 28.037405 31.675205
True count 159.000000 158.000000 155.000000 150.000000 140.000000 138.000000 138.000000 113.000000
mean 29.220755 0.143748 0.809551 1.154762 2.024101 5.274847 11.692221 18.640072
std 7.622389 1.877331 3.452738 3.929996 4.820032 4.010339 5.984883 5.695905
min 16.030001 -6.663446 -13.013815 -8.708650 -9.802344 -8.418610 -8.417392 3.932781
25% 22.135001 -1.003537 -1.086628 -1.332970 -1.444826 3.236077 7.804142 14.603315
50% 30.320000 0.452061 1.281145 1.912548 2.241605 5.770079 11.931472 18.907827
75% 34.519998 1.346590 3.063420 4.165109 5.755337 7.854694 14.441405 23.220777
max 48.000000 4.740685 8.702459 9.245563 14.016181 13.683218 28.863844 31.344876

TL;DR

So what did we see?

When the term structure of the VIX is backwardated we see average returns across all time frames actually increase! But, we also have a greater dispersion. This is pretty much a given, since when the VIX is backwadated the Vol level is elevated. We could show this is the case with a correlation analysis, but if you trade you already knew this.

Basically, this means it is a useless signal for the long-only equity trader. You might think that it's actually a inverse signal, but buying the wrong downdraft (i.e. 2008) and you don't get to play again.

For the options trader the question is more complicated since options are much more expensive during these periods, but the moves are much bigger too.

The main problem is that that data set for VIX futures is just too small! We only have ~150 days of backwadadion in the last 6 years. It's hardly enough to go on.

I'll be honest. I'm a premium seller and I'm nervous opening new positions right. Part of me says that this hesitancy is why these trades will pay out. So I'll probably sell some SPX strangles tomorrow, small.

That said, I'm still curious how returns would compare between the premium seller who goes flat during backwadation and the guy who holds on. My guess is that over this data set (2009-present) the guy who held on killed it. But, that same trader would have gotten killed in 2008-2009. I might try to model this using the buy-write index, so be on the lookout for that.

I'd also be curious to segment out the data with respect to the steepness of the curve (i.e., how are return when we are in 1% contago vs -5% backwardation vs 10% backwardation), but honestly you can't draw any good conclusions with this small dataset, so I'll skip it for now.


In [ ]: