该策略使用标普500数据,通过VIX和GARCH预测计算净情绪波动指数(NEVI)。当NEVI低于第2分位时做多,高于第8分位时做空,持仓周期为30天。

I. 策略概述

该策略使用标普500的历史数据估算GARCH(1,1)模型系数并预测30天的波动率。将这些预测的平均值定义为平均GARCH预测(AGF),并通过计算VIX与AGF的差值得到净情绪波动指数(NEVI)。将NEVI值划分为十分位,并将其作为动量指标。当NEVI低于第2分位时,投资者建立多头头寸;当其高于第8分位时,建立空头头寸。信号发出后,头寸持有30天,旨在利用波动性驱动的市场动量效应。

II. 策略合理性

VIX可以分解为两部分:理性市场波动性和情绪波动性。从VIX中扣除GARCH预测的情绪波动性,是比VIX更纯粹的市场情绪指标。

III. 论文来源

Volatility Based Sentiment Indicators for Timing the Markets [点击浏览原文]

<摘要>

VIX是由芝加哥期权交易所发布的著名隐含波动率估算指标。本文评估了VIX作为情绪指标的能力,以及其在短期投资策略中发出的信号。研究证明并讨论了基于VIX的策略(通常称为“逆向”策略)如何实现比市场更高的回报。我们还提出了一种从VIX派生的更纯粹的情绪指标,能够提供更准确的市场时机信号。我们称之为“净情绪波动指数”(NEVI)。研究表明,该指标具有显著的统计关系和有趣的属性。

IV. 回测表现

年化收益率12.65%
波动率5.92%
Beta-0.25
夏普比率1.46
索提诺比率N/A
最大回撤N/A
胜率57%

V. 完整python代码

from AlgorithmImports import *
#endregion
# https://quantpedia.com/strategies/net-emotional-volatility-index-effect/
#
# Historical S&P 500 index data are used to estimate the coefficients in the GARCH (1,1) model. Once the GARCH coefficients are obtained, for each day, the investor forecasts the volatility 
# for the next 30 days, reiterating the forecast, which means that every forecast becomes the last observation for the next forecast. The average of these values is called the Average GARCH 
# Forecast (AGF). The investor then calculates the Net Emotional Volatility Index (NEVI) as the difference between the VIX and AGF. The historical NEVI index is then sorted into deciles, and 
# the current NEVI is used as a “Momentum” indicator – the investor opens long on a position when the indicator goes below the 2nd decile and short on positions when it goes above the 8th decile.
# The position is held 30 days from the day of the last long or short signal.
import numpy as np
from arch import arch_model
class NetEmotionalVolatilityIndexEffect(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2008, 1, 1)
        self.SetCash(100000)
        
        self.period:int = 21
        self.holding_period:int = 30
        self.SetWarmUp(self.period + self.holding_period)
            
        self.market:Symbol = self.AddEquity("SPY", Resolution.Daily).Symbol
            
        self.vix:Symbol = self.AddData(QuandlVix, "CBOE/VIX", Resolution.Daily).Symbol # starts in 2004
        
        self.days_held:int = 0
        
        self.market_data:RollingWindow = RollingWindow[float](self.period)
        self.volatility:RollingWindow = RollingWindow[float](self.period)
        self.nevi_index:list[float] = []
    def OnData(self, data):
        # market data are present in the algorithm
        if self.market in data and data[self.market]:
            market_price:float = data[self.market].Value
            if market_price != 0:
                # store market price data
                self.market_data.Add(market_price)
            
            if self.market_data.IsReady:
                # calcualte market daily returns
                market_prices:np.ndarray = np.array([x for x in self.market_data][::-1])
                market_returns:np.ndarray = (market_prices[:-1] / market_prices[1:] - 1) * 100
                
                # forecast market volatility
                exp_market_volatility:float = self.garch_vol_predict_volatility(market_returns, 1, 1, 30)
                self.volatility.Add(exp_market_volatility)
                
                if self.volatility.IsReady:
                    agf:float = np.mean([x for x in self.volatility])
                    
                    # vix data are present in the algorithm
                    if self.vix in data and data[self.vix]:
                        vix:float = data[self.vix].Value
                        self.Plot("Expected Market Volatility", "Expected Market Volatility", exp_market_volatility)
                        self.Plot("VIX", "VIX", vix)
                        if vix != 0:
                            nevi:float = vix - agf
                            self.nevi_index.append(nevi)
                            
                            # wait until at least 30 data points is present for NEVI
                            if len(self.nevi_index) >= self.period and not self.Portfolio[self.market].Invested and not self.IsWarmingUp:
                                low_decile:float = np.percentile(self.nevi_index[:-1], 20)
                                high_decile:float = np.percentile(self.nevi_index[:-1], 80)
                                
                                recent_nevi:float = self.nevi_index[-1]
                                if recent_nevi > high_decile:
                                    self.SetHoldings(self.market, -1)
                                    self.days_held = 0
                                elif recent_nevi < low_decile:
                                    self.SetHoldings(self.market, 1)
                                    self.days_held = 0
        
        if self.Portfolio[self.market].Invested:
            self.days_held += 1
            if self.days_held == self.holding_period:
                self.Liquidate(self.market)
                self.days_held = 0
    def garch_vol_predict_volatility(self, returns, p, q, h) -> float:
        am = arch_model(returns, mean='Zero', vol='GARCH', p=p, q=q)
        res = am.fit(disp='off')
        forecasts = res.forecast(horizon=h)
        return np.sqrt(forecasts.variance.values[-1,:][0] * 252)
        
class QuandlVix(PythonQuandl):
    def __init__(self):
        self.ValueColumnName = "close"




发表评论

了解 Quant Buffet 的更多信息

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

继续阅读