找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2921

积分

0

好友

409

主题
发表于 昨天 00:01 | 查看: 0| 回复: 0

在量化投资领域,波动率与市场收益之间的关系是最常被利用的现象之一。当波动率飙升时,市场往往下跌;当市场平静时,上涨趋势更容易持续。但你知道吗?负收益对未来波动率的放大效应,远大于同等幅度的正收益。这种不对称性被称为“杠杆效应”。

今天这篇文章将带你了解如何使用 GJR-GARCH 模型来捕捉这种不对称性,并构建一个实用的“低波动率满仓、高波动率空仓”的择时策略。即使你不打算实盘交易,这套在云栈社区技术论坛中也常被讨论的框架,也能帮你深入理解Python波动率建模和回测的核心要点。

什么是 GJR-GARCH 模型?

标准的 GARCH 模型对正向和负向冲击的处理是对称的,而 GJR-GARCH(Glosten-Jagannathan-Runkle GARCH)模型增加了一个不对称项,公式如下:

σ²ₜ = ω + αε²ₜ₋₁ + γε²ₜ₋₁Iₜ₋₁ + βσ²ₜ₋₁

其中 Iₜ₋₁ 是一个指示变量:当 εₜ₋₁ < 0(即出现负收益)时,Iₜ₋₁ = 1,否则为 0。当 γ > 0 时,负向冲击对未来波动率的影响比正向冲击更大。这在股票市场中是被广泛验证的经验事实,因此 GJR-GARCH 特别适合用于风险管理。

策略核心思路

这个策略的思路非常直接:

  1. 使用滚动窗口拟合 GJR-GARCH 模型,估计条件波动率
  2. 根据波动率的历史分位数判断当前是“高波动”还是“低波动”市场状态
  3. 低波动时 100% 持仓,高波动时 0% 持仓(即空仓)

核心假设是:在市场波动剧烈时退出,可以有效降低回撤,同时在平静期捕捉大部分上涨收益。

代码实现详解

下面是策略的核心实现。我们使用 Python 的 arch 库来拟合 GJR-GARCH 模型。

1. 策略配置类

from dataclasses import dataclass

@dataclass
class StrategyConfig:
    """策略配置参数"""
    ticker: str = “SPY” # 交易标的
    benchmark_ticker: str = “^GSPC” # 基准指数
    start_date: str = “2010-01-01” # 回测起始日期
    end_date: str = “2024-12-31” # 回测结束日期
    garch_window: int = 252 # GARCH 滚动窗口(交易日)
    vol_percentile_threshold: float = 70.0 # 波动率分位数阈值
    low_vol_weight: float = 1.0 # 低波动时的仓位
    high_vol_weight: float = 0.0 # 高波动时的仓位
    commission_per_share: float = 0.005 # 每股佣金
    min_commission: float = 1.0 # 最低佣金

2. 滚动波动率估计

import numpy as np
import pandas as pd
from arch import arch_model

class GJRGARCHEngine:
    """GJR-GARCH 波动率引擎"""

    def __init__(self, config: StrategyConfig):
        self.config = config

    def fit_gjr_garch(self, returns: pd.Series) -> dict:
        """拟合 GJR-GARCH 模型"""
        # 创建 GJR-GARCH(1,1,1) 模型,使用 t 分布捕捉肥尾特性
        model = arch_model(
            returns * 100, # 转换为百分比收益率
            vol=‘Garch’,
            p=1, # GARCH 项阶数
            o=1, # 不对称项阶数(GJR 的核心)
            q=1, # ARCH 项阶数
            dist=‘t’ # Student-t 分布
        )
        result = model.fit(disp=‘off’, show_warning=False)
        return {
            ‘model’: result,
            ‘conditional_vol’: result.conditional_volatility / 100,
            ‘params’: result.params
        }

    def compute_rolling_volatility(self, returns: pd.Series) -> pd.Series:
        """计算滚动条件波动率"""
        n = len(returns)
        window = self.config.garch_window
        conditional_vol = pd.Series(index=returns.index, dtype=float)

        for i in range(window, n):
            window_returns = returns.iloc[i-window:i]
            try:
                result = self.fit_gjr_garch(window_returns)
                # 预测未来 1 天的波动率
                forecast = result[‘model’].forecast(horizon=1)
                conditional_vol.iloc[i] = np.sqrt(forecast.variance.values[-1, 0])
            except:
                # 拟合失败时使用简单标准差作为后备
                conditional_vol.iloc[i] = window_returns.std()

        return conditional_vol

3. 市场状态分类(避免前视偏差)

这是很多回测容易犯错的地方。如果使用整个数据集计算分位数,就会引入“前视偏差”——用未来的信息做过去的决策。正确的做法是使用扩展窗口分位数

class RegimeClassifier:
    """市场状态分类器"""

    def __init__(self, config: StrategyConfig):
        self.config = config

    def classify_regime(self, conditional_vol: pd.Series) -> pd.Series:
        """基于扩展窗口分位数进行状态分类"""
        # 计算每个时间点相对于历史的分位数排名
        expanding_percentile = conditional_vol.expanding(min_periods=20).apply(
            lambda x: pd.Series(x).rank(pct=True).iloc[-1] * 100,
            raw=False
        )

        regime = pd.Series(index=conditional_vol.index, dtype=int)
        # 0 表示低波动状态,1 表示高波动状态
        regime[expanding_percentile <= self.config.vol_percentile_threshold] = 0
        regime[expanding_percentile > self.config.vol_percentile_threshold] = 1

        return regime

4. 信号生成(注意滞后处理)

信号必须滞后一期,确保我们基于昨天的信息做今天的决策:

class StrategyEngine:
    """策略引擎"""

    def __init__(self, config: StrategyConfig):
        self.config = config

    def generate_signals(self, regime: pd.Series) -> pd.Series:
        """生成交易信号"""
        target_weight = pd.Series(index=regime.index, dtype=float)
        target_weight[regime == 0] = self.config.low_vol_weight # 低波动满仓
        target_weight[regime == 1] = self.config.high_vol_weight# 高波动空仓

        # 关键:信号滞后一期,避免使用当天信息
        signal = target_weight.shift(1)
        return signal

回测结果分析

使用 2010-2024 年的 SPY 数据进行回测,结果如下:

指标 策略 基准(买入持有)
总收益 163.83% 352.98%
年化收益 7.24% 11.49%
年化波动率 9.99% 17.21%
夏普比率 0.724 0.668
最大回撤 -18.95% -33.93%

从绝对收益来看,策略跑输了基准。这不奇怪,因为策略有约 30% 的时间处于空仓状态,错过了一些在高波动期出现的反弹。

但从风险调整后的角度看,结果就不一样了:波动率降低了 42%,最大回撤减少了 44%,夏普比率略高于基准。对于那些无法承受 30% 以上回撤的投资者来说,这种取舍是值得的。

在 15 年间共发生了约 78 次状态切换,平均每年约 5 次,并没有频繁交易的问题。

策略的局限性

  1. 机会成本真实存在:空仓期间会错过市场反弹,而一些涨幅最大的交易日往往出现在高波动时期
  2. 阈值选择具有主观性:70% 分位数是经验选择,不同阈值会产生不同结果
  3. 二元配置过于粗糙:更成熟的做法是根据波动率水平渐进式调整仓位
  4. 计算量较大:滚动拟合 GARCH 模型计算成本较高,实盘需要优化
  5. 未考虑滑点:假设能以收盘价成交,对大资金不现实

可能的改进方向

  1. 采用渐进式仓位调整,而非简单的 0/100% 切换
  2. 结合 VIX 指数进行信号确认
  3. 扩展到多资产组合,使用层次风险平价进行分散
  4. 引入马尔可夫状态切换模型,对状态转换进行概率建模
  5. 增加趋势过滤器,减少震荡市中的假信号

总结

GJR-GARCH 状态切换策略展示了一种基于原理的波动率择时方法。通过尊重波动率冲击的不对称性,并严格避免前视偏差,我们构建了一个能够显著降低风险的策略框架。

它能跑赢买入持有吗?从绝对收益来看,不能。但从回撤调整后的指标来看,它具有竞争力。对于那些在 30% 以上回撤时难以安睡的投资者来说,这种取舍可能正是他们需要的。

这套代码和分析仅供学习参考。过去的表现不代表未来的结果,在实施任何交易策略之前,请进行充分的研究并考虑咨询专业的财务顾问。

参考文章

  1. Building a GJR-GARCH Regime-Switching Strategy in Python: https://medium.com/@NFS303/building-a-gjr-garch-regime-switching-strategy-in-python-f633693b56b5



上一篇:SpringBoot整合JWT实现无状态登录:告别Session共享难题
下一篇:Quadratic:将Python、SQL与AI融入Web电子表格,为数据分析师与开发者赋能
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-3 00:40 , Processed in 0.300547 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表