该策略投资SVIX、SVOL、VIXY用于波动性风险溢价(VRP),UGL、USO等用于商品交易顾问(CTA)。VRP和CTA为独立策略,组合策略将未用资金分配给CTA。每日再平衡,按斜率和z值调整仓位,同时将未分配资本用于15%的趋势跟随策略,以降低与波动性策略的相关性。

策略概述

该策略的投资范围包括以下ETF(可通过期货合约复制):

VRP和CTA是单独的波动性和趋势跟随策略。VRP + CTA是组合策略,其中未使用的资金分配给CTA。

主要模型基于现金VIX和期货VIXF之间的平均斜率。斜率通过在每个时间点上的简单横截面回归来估算(方程(1,2))。还为斜率计算z值,用于以下情况:

    然而,在乐观情景下,做空/做多波动性时的亏损可能会恶化策略的风险调整后回报。因此,未分配的资本不会保持为现金,而是分配给15%波动性的趋势跟随策略(内部程序每日再平衡),基于选定的大宗商品和债券ETF。选择仅使用大宗商品和债券作为资产类别的原因是为了最大限度地减少与波动性策略的相关性,该策略也具有股票偏向。

    1. 计算a. 风险调整动量信号(方程(7)),b. EMA交叉信号(方程(8)),以及c. EMA突破信号(方程(10)),并通过252天的指数加权波动性进行归一化(方程(11)),最后将它们转化为基于sigmoid函数的仓位大小,限制为2个标准差(方程(12))。
    2. 每种大宗商品的总权重为10%,固定收益资产的总权重为20%。这确保了大宗商品的名义分配为60%,固定收益为40%。

    现在,CTA和VRP之间的分配非常简单。例如,如果名义权重的40%分配给SVOL,那么在这种情况下,将剩余资本的60%分配给CTA程序。

    每种策略每日再平衡,结果计算不包括交易成本。仓位大小(波动性大小)的具体计算如上所述。

    策略合理性

    主要模型基于选定的做空波动性产品。在常规期间,预期波动性的期限结构往往呈上升趋势或保持正价差(contango)。这是由于未来波动性预期较高,增加了对冲成本的不确定性(在交易开始时,未来实现波动性和隐含波动性之间的预期差异较小)。这表明存在正的波动性风险溢价。在市场波动性加剧之前,由于上述原因,隐含波动性及其预期的期限结构相对于常规期间表现出趋平的迹象。因此,事后我们进入了一个短期预期波动性高于长期波动性的状态(倒价差backwardation)。相应的抛售导致做多波动性的投资者实现波动性保险的索赔。波动性持有策略的分布是趋同的,也称为负偏斜。另一方面,(大宗商品和债券)趋势跟随策略本质上具有扩展的伽马分布,因此,在战术波动性信号产生的虚假信号期间,它们预计会提供缓冲。

    论文来源

    A Tactical Strategy using ETFs: Harvesting Volatility Risk Premia & Crisis Alpha [点击浏览原文]

    <摘要>

    本文讨论了通过ETF投资波动性风险溢价的系统方法。选择ETF主要是由于个人对波动性投资的兴趣,并且能够在没有资本限制的情况下自行管理策略的风险。然而,我将主要使用期货合约来回填我的回测数据,但主要思想集中在通过ETF获得的期货合约。我构建了一个用于收割波动性风险溢价的主要模型,随后通过岭回归叠加了一个元模型用于风险管理。我还使用选定的大宗商品ETF整合了CTA程序来收割危机阿尔法。

    回测表现

    年化收益率22.2%
    波动率24.13%
    Beta-0.251
    夏普比率0.92
    索提诺比率0.17
    最大回撤-37.7%
    胜率54%

    完整python代码

    from AlgorithmImports import *
    from data_tools import SymbolData, EWMParamType, halflife, annual_port_vol, round_float
    import itertools
    import numpy as np
    # endregion
    
    class HarvestingVolatilityRiskPremiaandCrisisAlphaviaETFs(QCAlgorithm):
    
        def Initialize(self):
            self.SetStartDate(2010, 1, 1)
            self.SetCash(100000)
    
            # VRP symbols
            self.long_vol_symbol: Symbol = self.AddEquity('VIXY', Resolution.Daily).Symbol
            self.short_vol_symbol: Symbol = self.AddEquity('SVXY', Resolution.Daily).Symbol
    
            self.slope_symbols: List[Symbol] = [
                self.AddData(CBOE, 'VIX', Resolution.Daily).Symbol, 
                self.AddData(CBOE, 'VIX3M', Resolution.Daily).Symbol
            ]
    
            self.min_model_period: int = 2 * 12 * 21
    
            self.slope_threshold: float = 0.
            self.VRP_vol_target: float = 0.2
            self.CTA_vol_target: float = 0.15
            self.CTA_final_vol_period: int = 252
    
            # VRP params
            self.center_of_mass: int = 63 
    
            # CTA params (Risk Adjusted Momentum, EMA Crossover, EMA Breakout)
            self.n: List[int] = [21, 63, 252]
            self.crossover_s: List[int] = [5, 10, 20]
            self.crossover_n: List[int] = [20, 40, 80]
    
            self.symbol_data: Dict[Union[Symbol, str], SymbolData] = {symbol : SymbolData(self.min_model_period) for symbol in [self.long_vol_symbol, self.short_vol_symbol]}
            self.symbol_data['slope'] = SymbolData(self.min_model_period)
    
            # CTA symbols
            self.CTA_commodities: List[str] = ['UGL', 'USO', 'USL', 'UNG', 'SOYB']   # 'JJC' (history since 2018)
            self.gross_weight_commodities: float = 0.1
    
            self.CTA_bonds: List[str] = ['TMF', 'TYD']
            self.gross_weight_bonds: float = 0.2
    
            self.CTA_symbols: List[Symbol] = [self.AddEquity(ticker, Resolution.Daily).Symbol for ticker in self.CTA_commodities + self.CTA_bonds]
            
            for symbol in self.CTA_symbols:
                self.symbol_data[symbol] = SymbolData(self.min_model_period)
            
            [self.Securities[s].SetLeverage(3) for s in self.symbol_data if s != 'slope']
            
            self.SetWarmup(self.min_model_period, Resolution.Daily)
            self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
    
        def OnData(self, slice: Slice) -> None:
            # store price data
            for symbol in self.symbol_data:
                if symbol == 'slope': continue
    
                if slice.ContainsKey(symbol) and slice[symbol] is not None:
                    self.symbol_data[symbol].update(slice[symbol].Value)
    
            # store VIX data
            if all(slice.ContainsKey(symbol) and slice[symbol] is not None for symbol in self.slope_symbols):
                self.symbol_data['slope'].update(slice[self.slope_symbols[1]].Value - slice[self.slope_symbols[0]].Value)
            
            if self.IsWarmingUp: return
            
            if all(self.symbol_data[symbol].is_ready() for symbol in self.symbol_data):
    
                # VRP strategy
                slope_data: SymbolData = self.symbol_data['slope']
                # calculate two latest values of slope z-score; 
                # index: 0-latest, 1-one day lag
                z_score_values: List[float] = [(slope_data.get_latest_value(lag) - slope_data.value_EWMA(EWMParamType.SPAN, self.center_of_mass, lag + 1)) \
                    / slope_data.value_EWSD(EWMParamType.SPAN, self.center_of_mass, lag + 1) \
                    for lag in range(2)]
                
                # long-short trading signal
                VRP_traded_symbol: Union[Symbol, None] = None
    
                if slope_data.get_latest_value() > self.slope_threshold and z_score_values[0] > 0:
                    VRP_traded_symbol = self.short_vol_symbol
                
                elif slope_data.get_latest_value() > self.slope_threshold and z_score_values[0] < 0:
                    VRP_traded_symbol = self.long_vol_symbol
                
                elif (slope_data.get_latest_value(1) > self.slope_threshold and z_score_values[1] < 0) and \
                    (slope_data.get_latest_value() < self.slope_threshold and z_score_values[0] < -1):
                    VRP_traded_symbol = self.long_vol_symbol
                
                elif (slope_data.get_latest_value(1) < self.slope_threshold and z_score_values[1] < -1) and \
                    (slope_data.get_latest_value() < self.slope_threshold and z_score_values[0] > -1):
                    VRP_traded_symbol = self.short_vol_symbol
                
                if VRP_traded_symbol is not None:
                    VRP_symbol_volatility: float = self.symbol_data[VRP_traded_symbol].return_EWSD(self.center_of_mass)
                    
                    # scaled volatility
                    VRP_weight: float = round_float(self.VRP_vol_target / VRP_symbol_volatility)
                
                    # CTA strategy
                    # 1. Risk Adjusted Momentum
                    risk_adjusted_momentum: Dict[Symbol, List[float]] = {
                        symbol : [self.symbol_data[symbol].compounded_return(lookback) / self.symbol_data[symbol].scaled_volatility(lookback) for lookback in self.n]
                        for symbol in self.CTA_symbols
                    }
    
                    # 2. EMA Crossover
                    EMA_crossover: Dict[Symbol, List[float]] = {
                        symbol : [(self.symbol_data[symbol].value_EWMA(EWMParamType.HALFLIFE, halflife(s)) - self.symbol_data[symbol].value_EWMA(EWMParamType.HALFLIFE, halflife(n))) / self.symbol_data[symbol].value_EWSD(EWMParamType.HALFLIFE, halflife(n)) \
                        for s, n in list(zip(self.crossover_s, self.crossover_n))]
                        for symbol in self.CTA_symbols
                    }
    
                    # 3. EMA Breakout
                    EMA_breakout: Dict[Symbol, List[float]] = {
                        symbol : [(self.symbol_data[symbol].get_latest_value() - self.symbol_data[symbol].value_EWMA(EWMParamType.SPAN, lookback)) / self.symbol_data[symbol].value_EWSD(EWMParamType.SPAN, lookback) for lookback in self.n]
                        for symbol in self.CTA_symbols
                    }
    
                    # update x_n
                    for symbol in self.CTA_symbols:
                        x_n: np.ndarray = np.array([risk_adjusted_momentum[symbol], EMA_crossover[symbol], EMA_breakout[symbol]])
                        self.symbol_data[symbol].update_x_n(x_n)
    
                    if all(self.symbol_data[symbol].x_n_is_ready() for symbol in self.CTA_symbols):
                        # calculate CTA weights
                        n: np.ndarray = [
                            self.n,
                            self.crossover_n,
                            self.n
                        ]
                        
                        CTA_weight: float = 1. - VRP_weight
                        
                        CTA_gross_weight: Dict[Symbol, float] = {
                            symbol : self.gross_weight_commodities if symbol.Value in self.CTA_commodities else self.gross_weight_bonds for symbol in self.CTA_symbols
                        }
    
                        # signal strength
                        CTA_x_n_weight: Dict[Symbol, float] = {
                            symbol : self.symbol_data[symbol].get_weight(n, 252) for symbol in self.CTA_symbols
                        }
                        
                        CTA_volatility_weight: Dict[Symbol, float] = {
                            symbol : (self.CTA_vol_target / self.symbol_data[symbol].return_EWSD(self.center_of_mass)) ** 2 for symbol in self.CTA_symbols
                        }
                        
                        CTA_asset_weight: Dict[Symbol, float] = {
                            symbol : CTA_gross_weight[symbol] * CTA_x_n_weight[symbol] * CTA_volatility_weight[symbol] for symbol in self.CTA_symbols
                        }
    
                        # instantaneous portfolio volatility
                        returns: np.ndarray = np.array([self.symbol_data[symbol].get_daily_returns()[-self.CTA_final_vol_period:] for symbol in self.CTA_symbols])
                        weights: np.ndarray = abs(np.array(list(CTA_x_n_weight.values())))
                        CTA_portfolio_volatility: float = annual_port_vol(returns, weights)
    
                        final_CTA_weight: float = round_float(self.CTA_vol_target / CTA_portfolio_volatility)
    
                        # concat final VRP and CTA portfolio assets
                        targets: List[PortfolioTarget] = [
                            PortfolioTarget(symbol, round_float(CTA_weight * final_CTA_weight * CTA_asset_weight[symbol])) for symbol in self.CTA_symbols
                        ] + [PortfolioTarget(VRP_traded_symbol, VRP_weight)]
    
                        self.SetHoldings(targets, True)
    

    Leave a Reply

    Discover more from Quant Buffet

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

    Continue reading