Vix Term Structure Lows and Vix returns N Days Later

As of yesterday (25’th january 2017) Vix Term Structure (1mo/3mo implied vol) closed at 0.794. Taking a quick look at what low Vix Ts values have meant for Vix returns going forward and run a bare bones backtest to gauge the initial validity of low Ts as a signal

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from import cboe_vix, cboe_vxv
from odo import odo
import datetime
import pyfolio as pf

xiv = get_pricing(symbols("XIV"), fields="close_price",,1,1),,1,25))

dfvix = odo(cboe_vix, pd.DataFrame)
dfvix = dfvix.set_index(dfvix["asof_date"])
dfvxv = odo(cboe_vxv, pd.DataFrame)
dfvxv = dfvxv.set_index(dfvxv["asof_date"])

df = pd.concat([dfvix, dfvxv], axis=1, join_axes=[dfvix.index])
df = df.rename(columns={"close":"vxv_close"})
df = df[["vix_close", "vxv_close"]]
df["vix_ts"] = df["vix_close"] / df["vxv_close"]

First looking at just the overall distribution of absolute Vix Ts values, readings below .8 are rare. Data starts from 2008

ecd = np.arange(1, len(df)+1, dtype=float) / len(df)

plt.figure(figsize=(10, 10))
plt.plot(np.sort(df["vix_ts"]), ecd, linewidth=1, color="#555555")
plt.axvline(0.794, linestyle="--", color="crimson", linewidth=1)
plt.xlabel("Vix Ts values")
plt.ylabel("Percent of Vix Ts values that are smaller than corresponding x")


Rather than using an absolute Vix Ts threshold as a signal or gauge to determine how low is low, i tried to use a rolling min value, meaning making the Vix Ts value relative. The threshold is simply defined as Ts – Rolling21 Min of Ts. Took two months as the trail amount, since it seems a significant enough time for Ts to make new lows

df["min_ratio"] = df["vix_ts"] - df["vix_ts"].rolling(42).min()

def formatPlot(x, where):

fig, (ax1, ax2) = plt.subplots(2, sharex=True, figsize=(16, 10))
ax1.plot(df["vix_close"], label="Vix", linewidth=1, color="#555555")
mins = df.index[df["min_ratio"] == 0]
ax1.vlines(mins, ymin=df["vix_close"].min(), ymax=90, color="crimson",
 linewidth=0.55, alpha=0.55, label="VixTs hits rolling 2 month min")

formatPlot(ax1, "center left")

ax2.plot(df["min_ratio"], color="#555555", linewidth=1, label="VixTs - rolling 2 month min")
formatPlot(ax2, "center left")


Now that the threshold is defined, one can look at the Vix returns going forward from the 2 month low being hit. Added two projected periods of 21 and 10 days to see if theres any significant difference. Average Vix returns going forward from Vix Ts hitting new 2 month lows are positive only enough so on occasions where Vix Ts itself is below 0.8 when hitting a new 2 month low

def retsShift(values, amount):
ret = (values.shift(-amount) / values) -1
return ret

df["ret21"] = retsShift(df["vix_close"], 21)
df["ret10"] = retsShift(df["vix_close"], 10)
df2 = df.copy() # Making a copy, otherwise masking will go crazy, probably doing masking wrong?
df2 = df2[df2["min_ratio"] == 0]

slope, intercept, r_val, p_val, std_err = scipy.stats.linregress(
np.array(df2["vix_ts"]), np.array(df2["ret21"]))
predict = intercept + slope * df2["vix_ts"]

plt.figure(figsize=(10, 10))
plt.plot(df2["vix_ts"], predict, "-", label="r2 = {}".format(format(r_val**2, ".2f")))
plt.scatter(df2["vix_ts"].where(df2["vix_ts"]<=0.8), df2["ret21"].where(df2["vix_ts"]<=0.8),
c="crimson", linewidth=0, s=28, alpha=0.89, label="Vix return 21 days later")
plt.scatter(df2["vix_ts"].where(df2["vix_ts"]<=0.8), df2["ret10"].where(df2["vix_ts"]<=0.8),
c="royalblue", linewidth=0, s=28, alpha=0.89, label="Vix return 10 days later")
plt.scatter(df2["vix_ts"].where(df2["vix_ts"]>0.8), df2["ret21"].where(df2["vix_ts"]>0.8),
c="#666666", linewidth=0, alpha=0.89, label=None)

plt.axhline(0, linestyle="--", color="#666666", linewidth=1)
plt.axvline(0.794, linestyle="--", color="crimson", linewidth=1, label="Vix Ts as of 25 Jan 2017")
plt.legend(loc="upper right")
plt.xlabel("Vix Ts level at which it hits a new 2 month low")
plt.ylabel("Vix return N days later")
plt.yticks(np.arange(-0.5, 1.5, 0.25))
formatPlot(plt, "upper right")


Next, looking at Vix behaviour, while filtering out only instances where Ts was below 0.8 when hitting its 2 month low.  Which is what we have in Vix Ts as of this writing

But also added mean of all instances where Ts was above 0.8

for index, row in df3.iterrows():
if row["min_ratio"] == 0 and row["vix_ts"] <= 0.8:
ret = df3["vix_close"].iloc[index:index+22]
ret = np.log(ret).diff().fillna(0)
ret = pd.Series(ret).reset_index(drop=True)
df_instances[index] = ret.cumsum()

for index, row in df3.iterrows():
if row["min_ratio"] == 0 and row["vix_ts"] > 0.8:
ret = df3["vix_close"].iloc[index:index+22]
ret = np.log(ret).diff().fillna(0)
ret = pd.Series(ret).reset_index(drop=True)
df_instances2[index] = ret.cumsum()

plt.plot(df_instances, color="#333333", alpha=0.05, linewidth=1, label=None)
plt.plot(df_instances2, color="#333333", alpha=0.05, linewidth=1, label=None)
plt.plot(df_instances.mean(axis=1), color="crimson", label="Mean Vix returns where Ts <= 0.8")
plt.plot(df_instances2.mean(axis=1), color="royalblue", label="Mean Vix returns where Ts > 0.8")
plt.axhline(0, linewidth=1, linestyle="--", color="#333333")
plt.xlabel("# Of days from Vix Ts rolling 2 month low")
plt.ylabel("Vix % return")
plt.xticks(np.arange(0, 22, 1))
plt.ylim(-0.2, 0.4)
plt.xlim(0, 21)
formatPlot(plt, "upper right")


However, since one can not directly trade Vix, the results are spurious, meaning when applying the signal to buy Vxx when Vix Ts hits a new 2 month low and holding it for N days (21 in the case below), the results are horrible. The reason being that Vxx (or any long volatility etf) experiences siginificant decay when Vix futures are in contango (which is most of the time in a bull market), so that holding it (just it alone) is not viable in any way. It can be useful however as a hedge in a more reasonable position sizing and allocation context

Just to make the contango effect on Vxx plain, also added backtest version which does the opposite, meaning it goes into Xiv when Vix Ts is hitting new 2 month lows and holds for 21 days

However adding the Xiv version illustrates the Vix futures contango benefits on Xiv (at least while the underlying trend in Spx is up). The backtest’s are no way anything viable, i just added them in order to gauge validity of the raw signal itself.

One could run an optimizer in order to determine the best possible Ts min trailing threshold, but i wont do that. In my limited experience, if a signal is not immediatley evident, it wont help massaging it to fit the curve

bt_vxx = get_backtest("588a07ff3d6aed61f57ca491").daily_performance.returns
bt_vxx_rets = pf.timeseries.cum_returns(bt_vxx, starting_value=1.0)

bt_xiv = get_backtest("588a069f66ba2e6145ca1aca").daily_performance.returns
bt_xiv_rets = pf.timeseries.cum_returns(bt_xiv, starting_value=1.0)

plt.figure(figsize=(16, 8))
plt.plot(bt_vxx_rets, color="royalblue", label="Strategy trading Vxx")
plt.plot(bt_xiv_rets, color="crimson", label="Strategy trading Xiv")
plt.plot((xiv.pct_change().cumsum()+1), label="Xiv b&amp;h", color="#555555")
formatPlot(plt, "center left")


I will look further into the underlying signal though, with a more reasonable position sizing and trade logic and will post a follow up once im somewhere with it

If anyone can pitch in regarding any mistakes, misconceptions or any other comments, please do – Thanks for your time


Discretionary trades update

Scaling out of Vix short (via long Xiv) position that i have held since election day
Also starting to scale out of Tesla long that i acquired below 200, however will just reduce position and looking to add again once it takes a breather

Have smaller open positions in
Short miners
Short biotech

Recent closed positions
Short natgas