“该策略包括持有标普500指数(例如,SPY ETF),并在VIX指数下降至其均值加上一个标准差时,使用多头深度实值-虚值看跌期权和空头平值-虚值看涨期权作为对冲。”

I. 策略概要

投资范围包括标普500指数复制(例如,SPY ETF)以及深度实值-虚值看跌期权和实值-虚值看涨期权等期权。该策略包括持有标普500指数,并在VIX指数下降至其均值加上一个标准差时,增加多头深度实值-虚值看跌期权和空头平值-虚值看涨期权作为对冲。该策略中使用的期权包括P95-P105和C105-C95看跌/看涨期权价差作为示例。这种对冲方法旨在在特定的VIX水平下保护投资组合,优化风险管理。

II. 策略合理性

VIX指数与标普500指数呈负相关关系,在下跌月份(16.25%)的表现优于上涨月份(-6.83%)。这种不对称性使VIX成为标普500指数的理想对冲工具,因为它在下跌趋势中涨幅更大,在上涨趋势中跌幅更小。然而,VIX不能直接投资,因此需要使用与波动率挂钩的工具。本文概述的策略使用具有单一到期日和每月展期的简单标普500指数期权结构。最具成本效益的策略是购买深度实值-虚值看跌期权价差和卖出平值-虚值看涨期权价差,这提供了多头波动率敞口。购买期权的最佳时机是VIX低于其均值加上一个标准差时。虽然这种方法可以捕捉大部分下行风险,但当波动率在较长时间内保持高位时,其表现可能不佳,尽管在这些条件下的回报仍然令人满意。

III. 来源论文

Volatility as an Asset Class: Holding VIX in a Portfolio [点击查看论文]

<摘要>

长期以来,投资者一直寻求在不牺牲上涨空间的情况下对冲市场下跌。如果VIX可以直接投资,将其作为标普500指数的对冲工具,将显著提高纯股票投资组合的业绩。然而,可交易的VIX产品并不能为投资者提供长期所需的对冲或回报。或者,解构VIX以找到驱动VIX变动的关键标普500指数期权,可以构建一个合成VIX投资组合,提供更有效的对冲。使用这些期权可以捕捉与VIX相似的相关性和回报,与标普500指数结合,其表现优于买入并持有指数投资组合。

IV. 回测表现

年化回报10.1%
波动率15.2%
β值0.928
夏普比率0.66
索提诺比率0.656
最大回撤N/A
胜率38%

V. 完整的 Python 代码

from numpy import mean, std
from AlgorithmImports import *
class HoldingArtificialVIXinaPortfolio(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2012, 1, 1)
        self.init_cash = 100000
        self.SetCash(self.init_cash)
        
        self.vix: Symbol = self.AddData(CBOE, 'VIX', Resolution.Daily).Symbol
        self.period: float = 12 * 21
        self.SetWarmUp(self.period, Resolution.Daily)
        self.vix_prices = []
        
        data: Equity = self.AddEquity('SPY', Resolution.Daily)
        data.SetLeverage(10)
        data.SetFeeModel(CustomFeeModel())
        self.market: Symbol = data.Symbol
        option = self.AddOption(self.market, Resolution.Minute)
        # filter the contracts with strikes between(ATM Strike - 10 * strike space value, market price + 10 * strike space value) and with expiration days less than 35 days
        option.SetFilter(-5, +5, timedelta(days=0), timedelta(days=35))
        # storing an entire option chain for a single underlying security
        self.chains = None
        
        self.portfolio_multiplier = 1 # invest only n percent of portfolio
        
        # benchmark chart settings
        benchmark_chart = Chart("Equity Curve With Benchmark")
        benchmark_chart.AddSeries(Series("Equity Curve", SeriesType.Line, 0))
        benchmark_chart.AddSeries(Series("Benchmark", SeriesType.Line, 0))
        self.benchmark_values: List[float] = []
        self.AddChart(benchmark_chart)
        
        self.last_day: int = -1
    def OnData(self, slice: Slice) -> None:
        if self.vix in slice and slice[self.vix]:
            vix_price: float = slice[self.vix].Value
            self.vix_prices.append(vix_price)
        
        for i in slice.OptionChains:
            self.chains = i.Value
        
        # check once a day
        if self.Time.day == self.last_day:
            return
        self.last_day = self.Time.day
        
        if len(self.vix_prices) < self.period: return
        market_price: float = self.Securities[self.market].Price
        current_vix_price: float = self.vix_prices[-1]
        # update benchmark chart
        self.benchmark_values.append(market_price)
        benchmark_perf = self.init_cash * self.benchmark_values[-1] / self.benchmark_values[0]
        self.Plot("Equity Curve With Benchmark", "Equity Curve", self.Portfolio.TotalPortfolioValue)
        self.Plot("Equity Curve With Benchmark", "Benchmark", benchmark_perf)
        
        # buy market if it is not invested in
        if not self.Portfolio.Invested:
            self.SetHoldings(self.market, self.portfolio_multiplier)
        
        vix_mean: float = mean(self.vix_prices)
        vix_std: float = std(self.vix_prices)
        
        invested: List[str] = [x.Key for x in self.Portfolio if x.Value.Invested]
        
        # hedge only if vix declines to its mean plus one standard deviation
        if current_vix_price <= vix_mean + vix_std:
            if self.chains:
                # only if market or nothing is invested in
                if len(invested) <= 1:
                    calls: List = list(filter(lambda x: x.Right == OptionRight.Call, self.chains))
                    puts: List = list(filter(lambda x: x.Right == OptionRight.Put, self.chains))
                    if not calls or not puts: return
                
                    # spreads picked from source paper
                    # P95-P105
                    # C105-C95
                    spread: List[float] = [0.95, 1.05]
                    
                    call_expiries: List = [i.Expiry for i in calls]
                    call_strikes: List = [i.Strike for i in calls]
                    # 1-month to expiration calls
                    call_expiry = min(call_expiries, key=lambda x: abs((x.date()-self.Time.date()).days-30))
    
                    put_expiries = [i.Expiry for i in puts]
                    put_strikes = [i.Strike for i in puts]
                    # 1-months to expiration puts
                    put_expiry = min(put_expiries, key=lambda x: abs((x.date()-self.Time.date()).days-30))
    
                    # determine strikes
                    put_strikes = [min(put_strikes, key = lambda x:abs(x - (boundary * market_price))) for boundary in spread]
                    call_strikes = [min(call_strikes, key = lambda x:abs(x - (boundary * market_price))) for boundary in spread]
                    
                    puts: List = [[i for i in puts if i.Expiry == put_expiry and i.Strike == put_strike] for put_strike in put_strikes]
                    calls: List = [[i for i in calls if i.Expiry == call_expiry and i.Strike == call_strike] for call_strike in call_strikes]
                    
                    # found both spread options
                    if len(puts) != 0 and len(calls) != 0:
                        puts: List = [x[0] for x in puts if len(x) != 0] # [P0.95, P1.05]
                        calls: List = [x[0] for x in calls if len(x) != 0] # [C0.95, C1.05]
                        
                        if all(self.Securities[x.Symbol].IsTradable for x in puts) and all(self.Securities[x.Symbol].IsTradable for x in calls):
                            if len(puts) == 2 and len(calls) == 2:
                                options_q: int = int((self.Portfolio.TotalPortfolioValue*self.portfolio_multiplier) / (market_price * 100))
                                for opt in [puts[0], calls[1]]:  # buy first put and second call from self.spread
                                    # self.Securities[opt.Symbol].MarginModel = BuyingPowerModel(10)
                                    self.Buy(opt.Symbol, options_q)
                                    
                                for opt in [puts[1],calls[0]]: # sell second put and first call from self.spread
                                    # self.Securities[opt.Symbol].MarginModel = BuyingPowerModel(10)
                                    self.Sell(opt.Symbol, options_q)
# custom fee model
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))

发表评论

了解 Quant Buffet 的更多信息

立即订阅以继续阅读并访问完整档案。

继续阅读