该交易策略利用VIX期货和S&P迷你期货进行交易和对冲。当期现价差为正且每日滚动超过0.10点时,投资者卖出到期在十个交易日内的VIX期货;若为负且每日滚动低于-0.10点,则买入VIX期货。期货持有五个交易日,并通过迷你S&P 500期货对冲价格波动。对冲比率通过回归分析构建。

策略概述

此交易策略使用VIX期货作为交易工具,并用S&P迷你期货进行对冲。当期现价差在正向(期货曲线处于顺差)且每日滚动大于0.10点时,投资者卖出最近到期的VIX期货(距离到期至少有十个交易日);当期现价差为反向(期货曲线处于逆差)且每日滚动小于-0.10点时,买入VIX期货。期货持有五个交易日,并通过S&P 500迷你期货的多头或空头头寸对冲VIX现货价格的变动。每日滚动被定义为前月VIX期货价格与VIX现货价格的差异,除以到期前的工作日数,反映了假设基础价差线性缩小至到期时的潜在利润。对冲比率是通过回归VIX期货价格变动与迷你S&P 500期货合约的当前百分比变动(以及这些变动与VIX期货到期天数相乘的组合)来构建的。

策略合理性

学术研究指出,波动率遵循均值回复过程,这意味着基础反映了风险中性预期的波动路径。当VIX期货曲线呈现正向斜率(即顺差)时,预示着VIX会因为相对于长期水平较低而上升;同样,当VIX期货曲线倒挂(即逆差)时,预示着VIX会因为高于长期水平而下降。

论文来源

The VIX Futures Basis: Evidence and Trading Strategies [点击浏览原文]

<摘要>

本研究表明,VIX期货基差在2006年至2011年期间对现货VIX的变动没有显著预测能力,但对VIX期货价格的变动具有预测能力。研究进一步展示了当基差处于顺差时卖出VIX期货合约、基差处于逆差时买入VIX期货合约的盈利能力,并用迷你S&P 500期货对这些头寸的市场敞口进行对冲。结果表明,这些交易策略具有很高的盈利能力,并且能抵御交易成本和样本外对冲比率预测的不确定性。总体而言,分析支持了这样一种观点:VIX期货基差不能准确反映VIX现货指数的均值回复特性,而反映的是可以收割的风险溢价。

回测表现

年化收益率19.67%
波动率N/A
Beta0.053
夏普比率-0.153
索提诺比率N/A
最大回撤94.8%
胜率45%

完整python代码

import numpy as np
import pandas as pd
import statsmodels.api as sm
from collections import deque

class ExploitingTermStructureVIXFutures(XXX):

    def Initialize(self):
        self.SetStartDate(2011, 1, 1)
        self.SetCash(100000)

        self.vix = self.AddData(QuandlVix, "CBOE/VIX", Resolution.Daily).Symbol              # Add Quandl VIX price (daily)
        self.vx1 = self.AddData(QuandlFutures, "CHRIS/CBOE_VX1", Resolution.Daily).Symbol    # Add Quandl VIX front month futures data (daily)
        self.es1 = self.AddData(QuandlFutures, "CHRIS/CME_ES1", Resolution.Daily).Symbol     # Add Quandl E-mini S&P500 front month futures data (daily)

        vx_data = self.AddFuture(Futures.Indices.VIX)
        vx_data.SetFilter(timedelta(0), timedelta(days=180))
        vx_data.MarginModel = BuyingPowerModel(5) # leverage
        
        es_data = self.AddFuture(Futures.Indices.SP500EMini)
        es_data.SetFilter(timedelta(0), timedelta(days=180))
        es_data.MarginModel = BuyingPowerModel(5) # leverage

        self.front_VX = None
        self.front_ES = None

        # request the history to warm-up the price and time-to-maturity 
        hist = self.History([self.vx1, self.es1], timedelta(days=450), Resolution.Daily)
        settle = hist['settle'].unstack(level=0)
        
        # the rolling window to save the front month VX future price
        self.price_VX = deque(maxlen=252)   
        # the rolling window to save the front month ES future price
        self.price_ES = deque(maxlen=252)   
        # the rolling window to save the time-to-maturity of the contract
        self.days_to_maturity = deque(maxlen=252) 
        
        expiry_date = self.get_expiry_calendar()
        df = pd.concat([settle, expiry_date], axis=1, join='inner')

        for index, row in df.iterrows():
            self.price_VX.append(row[str(self.vx1) + ' 2S'])
            self.price_ES.append(row[str(self.es1) + ' 2S'])
            self.days_to_maturity.append((row['expiry']-index).days)
            
        self.Schedule.On(self.DateRules.EveryDay(self.vix), self.TimeRules.AfterMarketOpen(self.vix), self.Rebalance)
    
    def OnData(self, data):
        # select the nearest VIX and E-mini S&P500 futures with at least 10 trading days to maturity 
        # if the front contract expires, roll forward to the next nearest contract
        for chain in data.FutureChains:
            future_indices = chain.Key.Value[1:] # First letter in this variable is '/'
            
            if future_indices == Futures.Indices.VIX:
                if self.front_VX is None or ((self.front_VX.Expiry-self.Time).days <= 1):
                    contracts = list(filter(lambda x: x.Expiry >= self.Time + timedelta(days = 10), chain.Value))
                    self.front_VX = sorted(contracts, key = lambda x: x.Expiry)[0]
            if future_indices == Futures.Indices.SP500EMini:
                if self.front_ES is None or ((self.front_ES.Expiry-self.Time).days <= 1):
                    contracts = list(filter(lambda x: x.Expiry >= self.Time + timedelta(days = 10), chain.Value))
                    self.front_ES = sorted(contracts, key = lambda x: x.Expiry)[0]
    
    def Rebalance(self):
        if self.Securities.ContainsKey(self.vx1) and self.Securities.ContainsKey(self.es1):
            # update the rolling window price and time-to-maturity series every day
            if self.front_VX and self.front_ES:
                self.price_VX.append(float(self.Securities[self.vx1].Price))
                self.price_ES.append(float(self.Securities[self.es1].Price))
                self.days_to_maturity.append((self.front_VX.Expiry-self.Time).days)
            
                # calculate the daily roll
                daily_roll = (self.Securities[self.vx1].Price - self.Securities[self.vix].Price)/(self.front_VX.Expiry-self.Time).days

                if not self.Portfolio[self.front_VX.Symbol].Invested:
                    # Short if the contract is in contango with adaily roll greater than 0.10 
                    if daily_roll > 0.1:
                        hedge_ratio = self.CalculateHedgeRatio()
                        self.SetHoldings(self.front_VX.Symbol, -0.4)
                        self.SetHoldings(self.front_ES.Symbol, -0.4*hedge_ratio)
                    # Long if the contract is in backwardation with adaily roll less than -0.10
                    elif daily_roll < -0.1:
                        hedge_ratio = self.CalculateHedgeRatio()
                        self.SetHoldings(self.front_VX.Symbol, 0.4)
                        self.SetHoldings(self.front_ES.Symbol, 0.4*hedge_ratio)
                
                # exit if the daily roll being less than 0.05 if holding short positions                 
                if self.Portfolio[self.front_VX.Symbol].IsShort and daily_roll < 0.05:
                    self.Liquidate()
                    self.front_VX = None
                    self.front_ES = None
                    return
                
                # exit if the daily roll being greater than -0.05 if holding long positions                    
                if self.Portfolio[self.front_VX.Symbol].IsLong and daily_roll > -0.05:
                    self.Liquidate()
                    self.front_VX = None
                    self.front_ES = None
                    return
                
        if self.front_VX and self.front_ES:
            # if these exit conditions are not triggered, trades are exited two days before it expires
            if self.Portfolio[self.front_VX.Symbol].Invested and self.Portfolio[self.front_ES.Symbol].Invested: 
                if (self.front_VX.Expiry-self.Time).days <=2 or (self.front_ES.Expiry-self.Time).days <=2:
                    self.Liquidate()
                    self.front_VX = None
                    self.front_ES = None
                    return
                
    def CalculateHedgeRatio(self):
        price_VX = np.array(self.price_VX)
        price_ES = np.array(self.price_ES)
        delta_VX = np.diff(price_VX)

        res_ES = np.diff(price_ES) / price_ES[:-1]*100
        tts = np.array(self.days_to_maturity)[1:]
        df = pd.DataFrame({"delta_VX":delta_VX, "SPRET":res_ES, "product":res_ES*tts}).dropna()

        # remove rows with zero value
        df = df[(df != 0).all(1)]
        y = df['delta_VX'].astype(float)
        X = df[['SPRET', "product"]].astype(float)
        X = sm.add_constant(X)

        model = sm.OLS(y, X).fit()
        beta_1 = model.params[1]
        beta_2 = model.params[2]
        
        hedge_ratio = abs((1000*beta_1 + beta_2*((self.front_VX.Expiry-self.Time).days)*1000)/(0.01*50*float(self.Securities[self.es1].Price)))
        
        return hedge_ratio

    def get_expiry_calendar(self):
        # import the futures expiry calendar
        url = "data.quantpedia.com/backtesting_data/economic/vix_futures_expiration.csv"
        csv_string_file = self.Download(url)
        dates = csv_string_file.split('\r\n')
        dates = [datetime.strptime(x, "%Y-%m-%d") for x in dates]
        df_date = pd.DataFrame(dates, index = dates, columns = [ 'expiry'])

        # convert the index and expiry column to datetime format
        # df_date.index = pd.to_datetime(df_date.index)
        df_date['expiry'] = pd.to_datetime(df_date['expiry'])
        # idx = pd.date_range('19-01-2005', '16-12-2020')

        # populate the date index and backward fill the dataframe    
        # return df_date.reindex(idx, method='bfill')
        return df_date

class QuandlVix(PythonQuandl):
    def __init__(self):
        self.ValueColumnName = "close"

class QuandlFutures(PythonQuandl):
    def __init__(self):
        self.ValueColumnName = "settle"

Leave a Reply

Discover more from Quant Buffet

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

Continue reading