halo 的技术博客

返回

因子的 IC 上周还好好的 0.06,这周突然塌到 0.02;波动率悄无声息地从 15% 翻到 30%。这些「数据在哪一刻变了 regime」的问题,是量化里最容易被忽视、却最致命的一类——你以为是策略失效,其实可能是市场在某个时点切了状态。

最朴素的诊断,是把样本拦腰切成两段,做个 Welch t 检验看「前后均值一不一样」。它的问题很硬:它只能回答「变没变」,答不出「在哪一刻变的」;而且一旦断点不在正中,它把断点前后的数据混在一起算方差,功效(power)被严重稀释。

结论先放这:贝叶斯结构断点检测把「断点位置 τ」当成随机变量,直接输出它的后验分布。在我们合成的序列(均值在 t=120 跳变、方差在 t=260 跳变)上,对包含 t=120 的窗口做检测,精确把均值断点定位在 t=121(真值 120,偏差仅 1 步);在信噪比扫描里,贝叶斯检测率上限达 0.87,而把全样本拦腰切开做 t 检验的方法上限只有 0.47——贝叶斯既更准(localization)又更强(power)。诚实地说:当 SNR≈0(压根没均值跳变)时,两者都几乎判不出(贝叶斯 0.02 vs 朴素 0.03),模型不会凭空造出断点,它只是把真实存在的断点精确定位出来。


1. 均值断点 vs 方差断点#

「数据变了」至少有两类,诊断方法完全不同:

  • 均值断点(mean break):序列的「中心」在某一步跳了。比如因子从年化 15% 的预期收益掉到 5%。
  • 方差断点(variance break):中心没动,但「散度」变了。比如波动率从 15% 翻到 30%,均值还是零。

合成序列与两个真实断点

本文的主模型针对均值断点——它假设变点前后各是平稳高斯,用贝叶斯因子(Bayes factor)比较「有一个断点」相对「没有断点」的证据。方差断点(t=260 那次)我们用它来划清方法边界:它不在本文模型的探测范围内,这恰恰说明——好模型知道自己能测什么、不能测什么。


2. 贝叶斯变点模型:把 τ 变成随机变量#

考虑一个滑动窗口 [t0, t]。我们假设窗口里至多存在一个变点 τ ∈ (t0, t]

  • 变点之前,观测来自 N(μ_pre, σ²)
  • 变点之后,观测来自 N(μ_post, σ²)
  • σ² 用窗口样本方差近似(已知方差的高斯共轭,闭式可得)

对每一个候选 τ,我们比较两种假设的边际似然比。已知方差下,均值从 μ_pre 跳到 μ_post 的贝叶斯因子近似为:

BF(τ)k(Nk)N(xˉprexˉpost)22σ2\text{BF}(\tau) \approx \frac{k(N-k)}{N} \cdot \frac{(\bar x_{pre} - \bar x_{post})^2}{2\sigma^2}

其中 k 是变点前样本数、N 是窗口总长。BF 越大,说明「这里有个断点」的证据越强。把所有候选 τ 的 BF 取 log、减最大、指数化、归一化,就得到 τ 的后验分布 p(τ)。


3. 合成数据 + 定位结果#

我们生成 T=400 个观测:均值在 t=120 从 0.0 跳到 1.2,方差在 t=260 从 1.0 跳到 2.2。

rng = np.random.default_rng(20260721)
T, tau1, tau2 = 400, 120, 260
mu1, mu2, sig1, sig2 = 0.0, 1.2, 1.0, 2.2
y = np.zeros(T)
for t in range(T):
    mu = mu1 if t < tau1 else mu2
    sig = sig1 if t < tau2 else sig2
    y[t] = rng.normal(mu, sig)

# 取一个包含 tau1 的窗口,做检测
t_end = 160
p, tau_hat = posterior_changepoint(y, t_end - 80, t_end)
print(tau_hat)          # -> 121  (真值 120, 偏差 1 步)
python

窗口内 τ 的后验分布

上图是窗口结束于 t=160 时的后验分布:概率质量几乎全压在 t=121 附近(红色虚线是真实断点 120,绿色点线是检测到的 121),偏差仅 1 步。这就是贝叶斯方法相对朴素 t 检验的本质优势——它不仅说「变了」,还说「在 t≈121 变的」。


4. 滚动后验:断点处的质量会「跳」一下#

把上面的检测对每个时点 t 用长度 W=80 的滑动窗口重做,得到随时间的滚动后验。当窗口扫过真实断点 t=120 时,后验质量会瞬间聚集到窗口内的对应偏移;窗口驶离后,质量随距离衰减。

滚动后验热力图

这张热力图里,白色虚线标出两个真实断点。可以看到:均值断点 t=120 处,后验质量明显「亮」起来(因为均值跳变正是模型在测的量);而方差断点 t=260 处,模型没有给出同等强度的定位信号——它本来就不探测纯方差跳变,这是方法的诚实边界,不是 bug。


5. 与朴素两段 t 检验对比#

朴素做法:把全样本(或长窗口)从正中切开,对前后两段做 Welch t 检验,用 |t| > 2 当「有断点」。问题:断点不在正中时,它把断点两侧的异质数据混在一起,方差被高估、功效被稀释;而且它只给一个「变没变」的 yes/no,不给位置。

我们用信噪比(mean jump 大小 / σ)扫描,对比两种方法在 60 次重复下的检测率(|τ_hat − 120| ≤ 3 算命中):

贝叶斯 vs 朴素 t 检验

  • 贝叶斯检测率随 SNR 上升,上限达 0.87
  • 朴素 t 检验上限仅 0.47,且全程落后
  • 关键诚实结论:当 SNR≈0(压根没有均值跳变)时,贝叶斯 0.02、朴素 0.03,两者都几乎判不出。这说明模型不会「无中生有」地制造断点——它只是把真实存在的断点找出来并精确定位。它能赢,是因为它给了断点一个位置参数 τ 并随之积分,而不是把窗口生硬劈成两半。

注:朴素 t 检验在「变点恰在中点」的特殊情形下会很强,但实战中断点位置未知且任意,这正是它系统性失效的地方。贝叶斯方法把「τ 在哪儿」作为待估变量,天然规避了这个陷阱。


6. 五类真实陷阱(高阶)#

  1. 方差断点盲区:本文模型只测均值跳变。波动率翻倍(纯方差断点)它定位不到——实战中必须配一个独立测方差/二阶矩变化的模型(如 BCP、或对边际似然用方差也未知的完整共轭),否则会漏掉最重要的「波动率 regime 切换」。
  2. 窗口长度 W 主观:W 太小噪声大、W 太大对近期断点迟钝。W 是超参数,没有银弹——通常按「你关心的最小 regime 长度」来设,并做敏感性扫描。
  3. 多断点假设不成立:模型假设窗口内「至多一个」断点。遇到 2020 年 3 月那种连续 regime 切换(流动性断点 + 相关性断点紧挨着),单断点假设会把两个断点平滑成一个模糊的伪断点。生产环境应改用 PELT / 贝叶斯在线变点(BOCPD)这类多断点框架。
  4. 方差未知时的校准:我们用窗口样本方差近似 σ²。若窗口内恰好横跨断点,σ² 被高估,BF 被压低、漏检。更严谨的做法是让前后段方差也各自未知(完整正态共轭),代价是 BF 不再是闭式、需要数值积分。
  5. 前视偏差:滚动检测必须在「只看 t 及之前」的窗口里做。任何把 t 之后的数据放进窗口的写法,都会把未来信息偷偷算进断点判定,回测里给出虚假的「完美定位」。

7. 完整 Python 代码#

下面是本文全部数字与配图的端到端复现脚本(与图一一对应):


一句话总结:结构断点检测的价值不在「有没有变」这个 yes/no,而在「在哪一刻变的」这个精确位置——它决定了你该在哪个时点切换模型、缩减敞口、重启训练。贝叶斯框架把 τ 变成随机变量,给出的后验分布既是告警信号,也是定位坐标;但记住它的边界:它测均值跳变、默许多断点要升级框架、且永远别让未来信息漏进窗口。

贝叶斯结构断点检测:把「数据什么时候变了」变成一个概率分布
https://blog.halo26812.eu.org/blog/bayesian-structural-break
Author halo
Published at 2026年7月21日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨