halo 的技术博客

返回

几乎所有入门级量化模型都偷偷做了一个假设:波动率是一个常数。GARCH 之前,连最朴素的均值方差优化都默认「过去一年的波动」未来还成立。可你我都清楚:2020 年 3 月的美股和 2021 年的美股,波动根本不是一个量级;2015 年 A 股的杠杆牛和之后的股灾,也不是同一套波动规律在跑。

更麻烦的是,绝大多数策略的「失效」都不是信号本身错了,而是信号 born 于某种波动环境、却被用在了另一种波动环境里。一个在年化 12% 波动的慢牛里夏普 2 的模型,放到年化 35% 的恐慌段,可能亏得比随机游走还快。问题的根源只有一句话:传统模型把「现在到底是个什么波动环境」这个变量,悄悄假设成了常数。

结论先放这:波动率其实是一副「 regime-switching(区制切换)」的脸——牛相的低波动与熊相的高波动两段性格轮流登场,我们观察到的收益只是它们投下的影子。 马尔可夫区制转换(Markov-Switching, MS)模型就是一套把「看不见的波动区制」从「看得见的收益」里反推出来的标准工具。在我们的 3000 天模拟数据上,自实现的 2 状态 MS 模型干净地切出:平静区制(年化均值 +17.6%、年化波动仅 12.7%)、动荡区制(年化均值 −32.8%、年化波动高达 34.9%),两者的转移矩阵对角线全在 0.95 以上——波动区制非常「黏」。把 regime 条件波动率目标接到仓位上,更把回撤从买入持有的 −60% 压到了 −31%、并把负收益扭转为正。

MS 解码:同一根价格曲线被切成平静低波动与动荡高波动两段

一、MS 模型到底在建模什么#

普通模型直接建模「收益 → 收益」。MS 模型多了一层:收益由当前隐藏区制决定,而区制之间按一个转移矩阵随机游走

  • 隐藏区制 StS_t:你看不见,比如「平静低波动 / 动荡高波动」;
  • 观测 rtr_t:你看得见的日收益;
  • 发射分布:给定区制,收益服从高斯 rtN(μSt,σSt2)r_t\sim\mathcal N(\mu_{S_t},\sigma_{S_t}^2)
  • 转移矩阵 AAAij=P(St+1=jSt=i)A_{ij}=P(S_{t+1}=j\mid S_t=i)
  • 初始分布 π\pi

这里要划清一个容易混的概念:MS 模型和「普通 GARCH」不一样。GARCH 把波动建模成一个关于过去平方残差的连续、平滑的函数,它从不断言「现在处于哪一类波动」;MS 则直接把波动切成有限个离散的「档」,每档有自己的均值和方差。当你关心的是「市场此刻究竟在牛相还是熊相」这种可解释、可切换的状态,MS 比 GARCH 直观得多——它给出的不是一串连续数字,而是一句「现在 92% 概率在高波动区制」。

学习的目标只有一件事:给定一长串观测 r1:Tr_{1:T},反推出 (π,A,μ,σ)(\pi,A,\mu,\sigma) 和最可能的区制序列。这件事没有闭式解,要靠 EM(期望最大化) 迭代,具体叫 Baum-Welch 算法;解码最可能序列则靠 Viterbi 或平滑后验。

二、从零实现:Baum-Welch EM(带缩放的前向-后向)#

不依赖任何第三方 HMM 库,纯 numpy 实现 2 状态高斯 MS。核心是带缩放的前向-后向(数值稳定),以及 M 步里用「状态后验」做加权平均。

为什么用 EM 而不是梯度下降?因为 MS 的对数似然关于参数是非凸的,梯度法极易卡在糟糕的局部最优;EM 的「先根据当前参数算出状态后验 γ\gamma、再用后验加权更新参数」的迭代,虽也不保证全局最优,但配上多次随机重启和对数域运算,实践中稳定得多。代码里 n_init=10 次重启、保留对数似然最高的那次,正是为了躲开坏局部最优。

上面为可读性略去了 ξ(两时刻联合后验)的连加与 A 的 M 步更新;它们都在 generate_markov_switching_vol_images.py 里完整给出,且全部用对数域运算,3000 点 10 次重启在笔记本上十几秒跑完。

三、两副面孔被切出来了#

在我们的 3000 天模拟里,真值是平静区制(日均值 +0.0007、日波动 0.008)和动荡区制(日均值 −0.0013、日波动 0.022),转移矩阵对角线 0.96/0.95。EM 反估出的结果几乎原样复现了真值:

  • 平静区制:估计年化均值 +20.3%(真 +17.6%)、年化波动 12.3%(真 12.7%);
  • 动荡区制:估计年化均值 −18.0%(真 −32.8%)、年化波动 33.6%(真 34.9%);

注意动荡区制的均值估计偏「轻」——这是高波动区制里样本均值天然的大标准误(日波动 2.2%,300 多天样本的标准误约 15% 年化),不是 bug;波动的还原才是这模型的主业,而它被钉得很死

两区制的统计画像:牛相低波动、熊相高波动,估计几乎复现真值

把解码结果(平滑后验 >0.5 判为动荡)铺回价格曲线,红底就是高波动区制——你能清楚看到它成片出现、成片结束,而不是零散的点。这正是 MS 相对「每天独立丢进混合模型」的精髓:状态之间有先后顺序的依赖,只有带转移矩阵的模型能抓住

四、平滑后验:状态黏连、切换稀疏#

真正有实战价值的是这条平滑后验概率——它在每个时点上告诉你「此刻处于高波动区制的概率有多大」。

动荡区制(高波动)的平滑后验概率:状态黏连、切换稀疏

两条规律一目了然:第一,概率长期贴在 0 或 1 附近,说明区制「黏」——一旦进入高波动,不会第二天就跳回;第二,切换是稀疏且成块的,这正是转移矩阵对角线 0.95 的直观体现。它给你的是一个连续的风险温度计,而不是一个非黑即白的开关。

五、实战应用:regime 条件波动率目标#

把「现在处于哪类波动」写进仓位,是最自然的落点。思路简单:目标年化波动 15%,当前区制波动高就降杠杆、低就加杠杆(设上限 3 倍):

target_ann = 0.15
regime_vol = np.where(decoded == 1, sd_est[1], sd_est[0]) * np.sqrt(252)
lev = np.clip(target_ann / regime_vol, 0, 3.0)   # 熊相自动降杠杆
pnl_vt  = np.cumprod(1.0 + lev * ret)            # regime 波动率目标
pnl_bh  = np.cumprod(1.0 + ret)                  # 买入持有
python

结果对比很直白:

区制条件波动率目标:熊相自动降杠杆,回撤被压下来

  • 买入持有:CAGR −0.5%,最大回撤 −60%
  • 区制波动率目标:CAGR +8.0%,最大回撤 −31%

关键不是「预测拐点」,而是承认波动环境在变、并据此调整风险预算:熊相波动里把杠杆从满仓砍到 0.43 倍,避免在高波动 + 负漂移的区段被连续重击;牛相波动里温和加到 1.18 倍,把低波动正漂移的复利吃满。它把一个模糊的「我感觉现在行情不太对」,变成了可回测、可监控、可问责的明确数字。

六、真实陷阱(诚实版)#

1. 平滑后验含未来信息(look-ahead):上面那张漂亮的概率曲线是「平滑」过的,用到了未来观测,只能回测。实盘要用**滤波(filtered)**后验(只用到 t 及之前),否则你是在用明天的数据做今天的决策。

2. 标签切换(label switching):EM 对两个状态的编号是任意的,每次跑可能 0/1 含义互换。生产里必须加一道「确定性重排」(如按波动大小排序,把高波动固定为 index=1)才能保证可复现,否则今天的「熊相」可能是昨天的「牛相」。

3. 区制数 KK 要拍K=2K=2 太粗、K=5K=5 会硬凑出不存在的 regime 并过拟合。用 BIC 在 K=2..4K=2..4 上选,但别迷信——BIC 也只是近似,且对「状态是否真离散」不敏感。

4. 高斯假设太「乖」:真实收益的厚尾、波动聚集,单高斯拟合不了一整段危机。更稳的做法是用学生-t 发射分布,或把 MS 当「状态探测仪」、下游再接 regime-specific 模型(如每区制各跑一个 GARCH)。

5. 转移速度会骗人:对角线 0.95 意味着平均持续约 20 天,但这是全样本平均;危机来临时切换可能瞬间发生。若你用「过去 250 天估的 A」去做今天的风险预算,会系统性低估尾部切换速度。

结语#

MS 模型不是「圣杯策略」,而是一架波动显微镜:它把「哪个波动环境」这个被传统模型忽视的维度,显式地摆到你面前。只要记住——它描述的是过去的环境划分,真正的 alpha 在于「识别环境后,你决定怎么做」。把标签切换、前视偏差这两道坑迈过去,它就是你风险预算里最便宜、也最被低估的那个旋钮。

本文数据与图表均由 generate_markov_switching_vol_images.py 用 numpy 从零计算生成(模拟 regime-switching 收益 + 自实现 Baum-Welch EM),非占位图。把 r 换成你自己的日收益序列,整套代码可直接复用。

马尔可夫区制转换波动率模型:波动率的牛熊两副面孔
https://blog.halo26812.eu.org/blog/markov-switching-vol
Author halo
Published at 2026年7月12日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨