
“该策略包括持有标普500指数(例如,SPY ETF),并在VIX指数下降至其均值加上一个标准差时,使用多头深度实值-虚值看跌期权和空头平值-虚值看涨期权作为对冲。”
资产类别: 差价合约、ETFs、期货、期权 | 地区: 美国 | 周期: 每月 | 市场: 股票 | 关键词: 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"))