Quant Buffet放轻松,别过度思虑

交易 VIX 期货展期和波动率溢价与 VIX 期权

登录后收藏

学术论文

Trading the VIX Futures Roll and Volatility Premiums with VIX Options

作者通过 VIX 期权交易 VIX 期货展期和波动率溢价 [点击查看论文]

机构
  • Bentley University
  • ?Bentley University - Department of Finance

策略概要

该策略针对VIX指数期权,由每日展期触发,展期衡量的是近月VIX期货与VIX之间的价差,除以期货结算前的交易日。小于-0.10点的展期信号表明期货贴水,大于+0.10点的展期信号表明期货溢价。在期货贴水时,交易者买入平值VIX看涨期权,在期货溢价时,买入平值VIX看跌期权。交易以买卖报价的中间价执行,持有五个交易日,每笔交易使用交易账户的5%。回测结果按5天回报的1/20进行缩放。

II. 策略合理性

学术研究表明,当VIX期货高于VIX时,VIX往往不会上涨,VIX期货往往会沿着VIX期货曲线向下滚动,在结算时达到较低的VIX,并失去其价值。研究还显示,在样本期内,VIX期权根本没有被高估,或高估程度不高,这表明直接购买VIX期权以利用VIX期货的系统性趋势可能是有吸引力的策略,并且风险有限。

回测表现

胜率24%

完整 Python 代码

from AlgorithmImports import *
class TradingTheVIXFuturesRollAndVolatilityPremiumsWithVIXOptions(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2010, 1, 1)
self.SetCash(1000000)

self.holding_period = 5         # holding each option contract for n days
self.trade_percentage = 0.05    # each option contract has 5% of portfolio weight

self.managed_queue = []

index_symbol = self.AddIndex('VIX').Symbol
option = self.AddIndexOption(index_symbol)
option.SetFilter(-5, 5, 25, 35)
self.vix_option_symbol = option.Symbol

self.AddFuture(Futures.Indices.VIX).SetFilter(timedelta(0), timedelta(90))

self.vix_symbol = self.AddData(CBOE, 'VIX').Symbol
self.vix_price = None

self.symbol = self.AddEquity('SPY', Resolution.Daily).Symbol

self.Schedule.On(self.DateRules.EveryDay(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Rebalance)
def OnData(self, data):
# vix data comes each day at 00:00
if self.vix_symbol in data:
    # get VIX CBOE price
    vix_data = data.Get(CBOE, self.vix_symbol)
    self.vix_price = vix_data.Value
    
option_chain = data.OptionChains.get(self.vix_option_symbol)
# check if all needed data are ready
if option_chain and self.vix_price and data.FuturesChains.Count != 0:
    last_spot_vix_price = self.vix_price
    self.vix_price = None # make sure daily selection
    
    # get contract with nearest expiration
    futures_chain = [x for x in data.FuturesChains.Values][0]
    futures_chain = [c for c in futures_chain.Contracts.Values]
    
    # make sure, there is future contract
    if len(futures_chain) == 0:
        return
    
    nearest_expiration_contract = sorted(futures_chain, key=lambda c: c.Expiry)[0]
    
    last_vix_future_price = nearest_expiration_contract.LastPrice
    
    # get contract expiration and today date
    expiry_date = nearest_expiration_contract.Expiry.date()
    today = self.Time.date()
    
    # calculate days to contract expiration
    days_to_expiration = (expiry_date - today).days
    
    # can't perform division by zero
    if days_to_expiration == 0:
        return
    
    # calculate spread according to strategy description
    spread = (last_vix_future_price - last_spot_vix_price) / days_to_expiration
    
    contract_type = None
    
    # determinate if contract should by put or call type
    if spread < -0.10:
        contract_type = 1
    elif spread > 0.10:
        contract_type = 0
    
    # make sure contract type is valid
    if contract_type != None:
        contracts = [contract for contract in option_chain if contract.Right == contract_type]
        # filter ATM contract with latest expiration
        contracts = sorted(contracts, key=lambda contract: abs(option_chain.Underlying.Price - contract.Strike))
        # sort the ATM contracts by their expiration dates
        contracts = sorted(contracts, key=lambda x:x.Expiry, reverse=True)
    
        # make sure there is at least one ATM contract with needed type
        if len(contracts) > 0:
            nearest_expiration_contract = contracts[0]
            # add option contract with it's weight to managed queue
            underlying_symbol = nearest_expiration_contract.UnderlyingSymbol
            self.managed_queue.append(RebalanceItem(nearest_expiration_contract, underlying_symbol))
                
def Rebalance(self):
remove_item = None

for rebalance_item in self.managed_queue:
    # trade new contract
    if rebalance_item.holding_period == 0:
        option_contract_symbol = rebalance_item.option_contract.Symbol
        underlying_symbol = rebalance_item.underlying_symbol
        if self.Securities.ContainsKey(option_contract_symbol) and self.Securities.ContainsKey(underlying_symbol):
            if self.Securities[option_contract_symbol].Price != 0 and self.Securities[option_contract_symbol].IsTradable and self.Securities[underlying_symbol].Price != 0:
                # calculate contract quantity
                underlying_price = self.Securities[underlying_symbol].Price
                quantity = self.Portfolio.TotalPortfolioValue / self.holding_period
                quantity = np.floor((quantity / (underlying_price*100)) * self.trade_percentage)
                
                # buy contract
                self.MarketOrder(option_contract_symbol, quantity)
                rebalance_item.quantity = quantity
    
    # liquidate option contract
    elif rebalance_item.holding_period == self.holding_period:
        option_contract_symbol = rebalance_item.option_contract.Symbol
        quantity = rebalance_item.quantity
        
        # liquidate only opened positions
        if quantity != 0 and self.Portfolio[option_contract_symbol].Invested:
            self.MarketOrder(option_contract_symbol, -quantity)
            
        remove_item = rebalance_item
        
    rebalance_item.holding_period += 1

# remove liquidated option contract from managed queue    
if remove_item:
    self.managed_queue.remove(remove_item)
            
class RebalanceItem():
def __init__(self, option_contract, underlying_symbol):
self.quantity = 0
self.holding_period = 0
self.option_contract = option_contract
self.underlying_symbol = underlying_symbol
# Custom fee model
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))