“该策略使用基于隐含波动率价差和宏观经济变量的预测模型交易标准普尔500指数期货,每日在股指和无风险利率之间动态分配。”

I. 策略概要

该策略基于一个使用隐含波动率价差和宏观经济变量的预测模型交易标准普尔500指数期货。隐含波动率价差每天计算,其值为虚值看跌期权(行权价0.80-0.95)和平值看涨期权(行权价0.95-1.05)的成交量加权平均波动率价差之差。该价差以及VIX平方、违约利差变化、期限利差变化、去趋势无风险利率、股息收益率和滞后指数回报等解释变量,用于回归分析以预测超额市场回报。使用从数据集中间点到t日的数据,系数预测超额回报。如果预测为正,策略将100%投资于股指;如果为负,则投资于无风险利率。每日回报实现,回归通过扩展数据窗口进行更新,以实现持续预测和调整。该模型整合了波动率和宏观经济动态,用于系统性股票市场投资。

II. 策略合理性

投资者对市场持积极预期时,会增加对看涨期权的需求并减少对看跌期权的需求,从而提高看涨期权波动率并降低看跌期权波动率。较低的看跌-看涨隐含波动率价差表明标的资产的预期回报更高。这种效应在信息密集时期(例如收益公告或有关现金流和折现率的重大新闻)以及消费者信心指数值处于极端时更为明显,这突显了价差对市场情绪和信息驱动条件的敏感性。

III. 来源论文

Implied Volatility Spreads and Expected Market Returns [点击查看论文]

<摘要>

本文研究了波动率价差与股票市场总预期回报之间的跨期关系。我们提供了证据表明,在每日和每周频率上,波动率价差与预期回报之间存在显著的负相关关系。我们认为这种联系是由期权市场到股票市场的信息流驱动的。记录的关系在以下时期显著更强:(i)标准普尔500成分股公司宣布其收益;(ii)现金流和折现率新闻幅度较大;以及(iii)消费者信心指数取极端值。在控制了条件波动率、方差风险溢价和宏观经济变量后,跨期关系仍然保持强烈的负相关。此外,考虑到交易成本后,基于与波动率价差的跨期关系的交易策略比投资标准普尔500指数的被动策略具有更高的投资组合回报。

IV. 回测表现

年化回报11.09%
波动率14.43%
β值0.193
夏普比率0.77
索提诺比率-0.019
最大回撤N/A
胜率46%

V. 完整的 Python 代码

from AlgorithmImports import *
import data_tools
import statsmodels.api as sm
import pandas as pd
from QuantConnect.DataSource import *
#endregion
class ImpliedVolatilitySpreadsandExpectedMarketReturnsinSP500(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2005, 1, 1)
        self.SetCash(100_000)
        
        self.leverage: int = 10
        self.vix: Symbol = self.AddData(CBOE, 'VIX', Resolution.Daily).Symbol
        self.put_call: Symbol = self.AddData(data_tools.PutCallRatio, "PutCallRatio", Resolution.Daily).Symbol
        
        # bond yield data
        self.us_yield_10y: Symbol = self.AddData(data_tools.BondYield, 'US10YT', Resolution.Daily).Symbol
        
        self.us_yield_3m: Symbol = self.AddData(data_tools.InterestRate3M, 'IR3TIB01USM156N', Resolution.Daily).Symbol
        self.us_yield_3m_sma: SimpleMovingAverage = SimpleMovingAverage(12)
        # warmup 3m yield history
        us_yield_3m_history: dataframe = self.History(self.us_yield_3m, 12*30, Resolution.Daily)
        yields: Series = us_yield_3m_history.loc[self.us_yield_3m].value
        for time, _yield in yields.items():
            self.us_yield_3m_sma.Update(time, _yield)
        
        self.daaa_yield: Symbol = self.AddData(data_tools.BondYield, 'DAAA', Resolution.Daily).Symbol
        self.dbaa_yield: Symbol = self.AddData(data_tools.BondYield, 'DBAA', Resolution.Daily).Symbol
        self.last_default_spread: float|None = None
        
        # risk free etf
        symbol_data: Equity = self.AddEquity('BIL', Resolution.Daily)
        self.risk_free_asset: Symbol= symbol_data.Symbol
        symbol_data.SetLeverage(self.leverage)
        
        # SPY and SPTR data
        symbol_data: Equity = self.AddEquity('SPY', Resolution.Daily)
        symbol_data.SetLeverage(self.leverage)
        self.spy: Symbol = symbol_data.Symbol
        
        self.sptr: Symbol = self.AddData(data_tools.SPTRIndex, 'SP500TR', Resolution.Daily).Symbol
        
        self.period: int = 21
        self.SetWarmUp(self.period, Resolution.Daily)
        self.spy_history: RollingWindow = RollingWindow[float](self.period)
        self.sptr_history: RollingWindow = RollingWindow[float](self.period)
        
        self.regression_data: List = []
        self.min_regression_period: int = 12 * 21
        
    def OnData(self, slice: Slice) -> None:
        # check if data is still comming in
        put_call_last_update_date: Dict[str, datetime.date] = data_tools.PutCallRatio.get_last_update_date()
        bond_yield_last_update_date: Dict[str, datetime.date] = data_tools.BondYield.get_last_update_date()
        ir_last_update_date: Dict[str, datetime.date] = data_tools.InterestRate3M.get_last_update_date()
        index_last_update_date: Dict[str, datetime.date] = data_tools.SPTRIndex.get_last_update_date()
        if not ((self.put_call.Value in put_call_last_update_date and self.Time.date() < put_call_last_update_date[self.put_call.Value]) and \
            (all(symbol.Value in bond_yield_last_update_date and self.Time.date() < bond_yield_last_update_date[symbol.Value] for symbol in [self.us_yield_10y, self.daaa_yield, self.dbaa_yield])) and \
            (self.us_yield_3m.Value in ir_last_update_date and self.Time.date() < ir_last_update_date[self.us_yield_3m.Value]) and \
            (self.sptr.Value in index_last_update_date and self.Time.date() < index_last_update_date[self.sptr.Value])):
            
            self.Liquidate()
            return
        # update 3M yield moving average
        if self.us_yield_3m in slice:
            yield_3m: float = slice[self.us_yield_3m].Value
            self.us_yield_3m_sma.Update(self.Time, yield_3m)
            
        # update SPY and SPTR history
        if self.spy in slice and self.sptr in slice:
            spy_price: float = slice[self.spy].Value
            sptr_price: float = slice[self.sptr].Value
            if spy_price != 0 and sptr_price != 0:
                self.spy_history.Add(spy_price)
                self.sptr_history.Add(sptr_price)
        
        # calculate independent variables
        reg_data: Dict[str, float] = {}
        if (self.vix in slice and self.put_call in slice) and \
            (self.daaa_yield in slice and self.dbaa_yield in slice) and   \
            (self.us_yield_3m_sma.IsReady and self.Securities.ContainsKey(self.us_yield_3m) and self.us_yield_10y in slice) and  \
            (self.spy_history.IsReady and self.sptr_history.IsReady):
            aaa_yield: float = slice[self.daaa_yield].Value
            baa_yield: float = slice[self.dbaa_yield].Value
            default_spread: float = baa_yield - aaa_yield
            
            if self.last_default_spread:
                change_in_default_spread: float = default_spread - self.last_default_spread
                reg_data['change_in_default_spread'] = change_in_default_spread    #independend variable
            
                vixsq: float = slice[self.vix].Value ** 2
                reg_data['vixsq'] = vixsq    #independend variable
                
                put_call_spread: float = slice[self.put_call].Value
                reg_data['put_call_spread'] = put_call_spread    #independend variable
                
                yield_3m: float = self.Securities[self.us_yield_3m].Price
                yield_sma_3m: float = self.us_yield_3m_sma.Current.Value
                
                detrended_riskless_rate: float = yield_3m - yield_sma_3m
                reg_data['detrended_riskless_rate'] = detrended_riskless_rate    #independend variable
                
                yield_10y: float = slice[self.us_yield_10y].Value
                term_spread: float = yield_10y - yield_3m
                reg_data['term_spread'] = term_spread    #independend variable
                
                spy_prices: List[float] = list(self.spy_history)
                sptr_prices: List[float] = list(self.sptr_history)
                spy_return: float = spy_prices[0] / spy_prices[-1] - 1
                reg_data['spy_return'] = spy_return    #dependend variable
                
                sptr_return: float = sptr_prices[0] / sptr_prices[-1] - 1
                
                if spy_return != 0:
                    dividend_price_ratio: float = sptr_return / spy_return
                    reg_data['dividend_price_ratio'] = dividend_price_ratio    #independend variable
                    
                    # one day lagged returns.
                    spy_prices = list(self.spy_history)[1:]
                    lagged_return: float = spy_prices[0] / spy_prices[-1] - 1
                    reg_data['lagged_return'] = lagged_return    #independend variable
                    
                    # update regression data
                    # NOTE We might want to ommit concat or append wor functions.
                    # "It is worth noting that concat() (and therefore append()) makes a full copy of the data, and that constantly reusing this function can create a significant performance hit. 
                    # If you need to use the operation over several datasets, use a list comprehension."
                    #   - Source: https://pandas.pydata.org/pandas-docs/stable/user_guide/merging.html
                    self.regression_data.append(reg_data)
                
                # regression data is ready
                if len(self.regression_data) >= self.min_regression_period:
                    regression_df: dataframe = pd.dataframe(self.regression_data)
                    
                    x: dataframe = regression_df.loc[:, regression_df.columns != 'spy_return'][:-1]    # offset x. Last x entry is used to get Y prediction.
                    x = sm.add_constant(x)
                    y: dataframe = regression_df['spy_return'][1:]
                    y = y.reset_index(drop=True)
                    lm = sm.OLS(y, x).fit()
                    last_x = x.tail(1).reset_index(drop=True)
                    y_predicted: float = lm.predict(last_x)[0]
                    
                    if y_predicted > 0:
                        if self.Portfolio[self.risk_free_asset].Invested:
                            self.Liquidate(self.risk_free_asset)
                        self.SetHoldings(self.spy, 1)
                    else:
                        if self.Portfolio[self.spy].Invested:
                            self.Liquidate(self.spy)
                        self.SetHoldings(self.risk_free_asset, 1)
                    
            self.last_default_spread = default_spread
        else:
            self.Liquidate()

发表评论

了解 Quant Buffet 的更多信息

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

继续阅读