投资范围包括跟踪道琼斯工业指数(DJI)的工具(如DIA ETF)和现金(如BIL)。回归方程用于调整长期波动率,变量为过去24个月DJI月度回报的标准差和短期波动率。投资者在股票指数和无风险利率之间分配财富,使用公式确定最优持仓权重。公式中,风险厌恶参数为3,回报预测基于调整后的长期波动率。投资组合每月重新平衡,初始估算期为60个月。

策略概述

投资范围包括单一跟踪道琼斯工业指数(DJI)的工具(例如,DIA ETF)和现金(例如,BIL)。回归方程为𝐴𝐷𝐽_𝐿𝑉𝑡 = 𝐿𝑉𝑡 ‒ 𝛽𝑜𝑙𝑠,𝑡 ∙ 𝑆𝑉𝑡,其中自变量为长期波动率𝐿𝑉𝑡,即过去24个月内DJI月度回报的标准差,年化方法是乘以12的平方,短期波动率𝑆𝑉𝑡基于Welch和Goyal(2008)的方法进行修正,使用OLS贝塔来估计因变量调整后的长期波动率𝐴𝐷𝐽_𝐿𝑉𝑡。

投资者在股票指数和无风险利率之间分配财富,按照公式𝜔^∗𝑡 = 𝑟𝑡+1/(𝛾 * 𝜎𝑡+1^2)确定股票指数的最优持仓权重,其中𝑟𝑡+1是t+1月份的回报预测,𝛾=3为风险厌恶参数,𝜎𝑡+1^2是使用过去60个月的股票回报方差来估计的股票方差。回归公式用于计算下个月的回报预测,独立变量为调整后的长期波动率。初始估算期为60个月,投资组合基于该因子加权,并假设每月重新平衡。

策略合理性

调整后的长期波动率在预测股票回报方面表现出色,尤其是与其他流行预测指标结合时表现更佳。更重要的是,调整后的长期波动率指标所揭示的正向风险-回报关系符合资产定价理论的预测。从上世纪60年代开始,波动率与回报之间的关系便备受关注,但至今仍困扰着量化分析师、科学家和分析师们。Qiu、Rui、Liu、Jing和Li、Yan(2022)的研究为使用调整后的长期波动率预测股票回报作出了重要贡献,帮助理解波动率与预期回报之间复杂的实证关系。

论文来源

Adjusted Long-Term Volatility and Stock Return Predictability [点击浏览原文]

<摘要>

我们设计了一个调整后的长期波动率(ADJ_LV)指标,通过消除短期波动率的干扰信息来研究ADJ_LV对股票回报的预测能力。在2000年至2019年的样本中,考虑了19个流行的预测模型,ADJ_LV能够正向预测S&P 500指数下个月的回报,相应的单变量模型表现出最佳的预测性能,样本内调整后的R平方为3.825%,样本外R平方为3.356%,回报增益为5.976%,确定性等价回报增益为4.708,夏普比率增益为0.394。将ADJ_LV作为额外预测因子加入其他19个单变量模型中,显著提高了样本内和样本外的预测性能。此外,我们发现ADJ_LV对长期(1-12个月)股票回报也具有预测能力,并且能够预测行业组合及按规模、市净率、经营风险和投资风险形成的组合的回报。ADJ_LV对股票回报的预测能力在道琼斯工业指数的预测中也表现出稳健性,并且使用替代估算的ADJ_LV同样有效。

回测表现

年化收益率3.23%
波动率N/A
Beta0.037
夏普比率N/A
索提诺比率-0.466
最大回撤0%
胜率60%

完整python代码

from AlgorithmImports import *
from pandas.core.frame import DataFrame
import pandas as pd
import statsmodels.api as sm
# endregion

class ModifiedVolatilityPredictsDJIReturns(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2010, 1, 1)
        self.SetCash(100000)
        
        self.leverage:int = 3
        self.equity:Symbol = self.AddEquity("DIA", Resolution.Minute, leverage=self.leverage).Symbol
        self.cash:Symbol = self.AddEquity("BIL", Resolution.Minute, leverage=self.leverage).Symbol

        self.gamma:float = 3.
        self.estimation_period:int = 60
        self.std_period:int = 24
        self.months_in_year:float = 12.
        self.allocation_cap:List[float] = [-0.5, 1.5]

        self.recent_month:int = -1

    def OnData(self, data: Slice) -> None:
        if not(data.ContainsKey(self.equity) and data.ContainsKey(self.cash) and data[self.equity] and data[self.cash]):
            return

        # monthly rebalance
        if self.Time.month == self.recent_month:
            return
        self.recent_month = self.Time.month

        # get monthly closes
        period:int = (self.estimation_period + self.std_period) * 2 + 1
        history:DataFrame = self.History(self.equity, period * 31, Resolution.Daily)['close'].unstack(level=0)
        history = history.groupby(pd.Grouper(freq='M')).last()[self.equity]

        if len(history) >= period:
            history = history.iloc[-period:]
            returns:DataFrame = history.pct_change().iloc[1:]

            # long-term volatility 
            lv:DataFrame = returns.rolling(self.std_period).std() * np.sqrt(self.months_in_year)
            lv = lv.dropna()
            
            # short-term volatility 
            # SVAR is stock variance as in Welch and Goyal (2008)
            svar:DataFrame = (returns ** 2).rolling(self.std_period).sum()
            svar = svar.dropna()
            sv:DataFrame = np.sqrt(svar / self.months_in_year)

            lv_mean:DataFrame = lv.rolling(self.estimation_period).mean()
            lv_mean = lv_mean.dropna()
            sv_mean:DataFrame = sv.rolling(self.estimation_period).mean()
            sv_mean = sv_mean.dropna()

            # beta estimation
            beta_factor:np.ndarray = np.array([((lv.iloc[i-self.estimation_period:i] - lv_mean.iloc[i]) * (sv.iloc[i-self.estimation_period:i] - sv_mean.iloc[i])).sum() for i in range(-1, -(len(lv_mean)+1), -1)])
            beta_devisor:np.ndarray = np.array([((sv.iloc[i-self.estimation_period:i] - sv_mean.iloc[i]) ** 2).sum() for i in range(-1, -(len(lv_mean)+1), -1)])
            beta:np.ndarray = beta_factor / beta_devisor
            adj_lv:np.ndarray = (lv.iloc[-len(beta):].values - (beta * sv.iloc[-len(beta):].values))[-self.estimation_period:]

            # regression to predict return
            model = self.multiple_linear_regression(adj_lv[:-1], returns.iloc[-len(adj_lv):].values[1:])
            ret_pred:float = model.predict(adj_lv[-1])
            variance_pred:float = svar.iloc[-1] #lv.iloc[-1] ** 2

            # allocation
            equity_allocation:float = (ret_pred / (self.gamma * variance_pred))[-1]
            equity_allocation = min(max(equity_allocation, min(self.allocation_cap)), max(self.allocation_cap))
            cash_allocation:float = 1. - equity_allocation

            # trade execution
            self.SetHoldings(self.equity, equity_allocation)
            self.SetHoldings(self.cash, cash_allocation)
        else:
            if self.Portfolio.Invested:
                self.Liquidate()

    def multiple_linear_regression(self, x:np.ndarray, y:np.ndarray):
        x:np.ndarray = np.array(x).T
        # x = sm.add_constant(x)
        result = sm.OLS(endog=y, exog=x).fit()
        return result

Leave a Reply

Discover more from Quant Buffet

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

Continue reading