SADF 结构突变检验:实时监测价格序列的爆炸性泡沫
泡沫的技术定义不是'涨太多',而是价格序列从随机游走切换到爆炸性自回归(ρ>1)——SADF/BSADF 把 ADF 单位根检验改造成实时监测器:右尾检验 + 起点滑动取上确界,让统计量只对'最近一段是否爆炸'敏感。1000 日受控实验(500 日随机游走 + 230 日 ρ=1.012 温和爆炸 + 崩溃)实测:BSADF 在爆炸开始后 65 个交易日越过 95% 临界值报警,峰值 11.3 远超临界值 0.57,崩溃后迅速回落——它同时检测泡沫的生成与破灭。双泡沫实验暴露起点锚定的致命伤:第一轮泡沫崩溃后,锚定 t=0 的扩张窗 ADF 对第二轮泡沫完全失灵(峰值 0.14 不及临界值零头),BSADF 靠滑动起点甩掉旧样本稀释、峰值 2.72 正常报警——这正是 PSY 2015 从 SADF 升级到 GSADF/BSADF 的全部动机。三个诚实边界:65 日检测滞后意味着它是确认工具而非预测工具;临界值必须蒙特卡洛模拟(非标准分布)且随窗口设定变化;对'漂移强但不爆炸'的慢牛会保持沉默——它检验的是自回归系数不是涨幅。AFML 用法:BSADF 序列不做择时开关,做元标注特征喂给二级模型(高阶)
一句话版本#
泡沫不是「涨太多」,是价格的生成机制换挡:从随机游走(涨跌无记忆)切换到爆炸性自回归(涨会引来更多涨)。SADF 把教科书里的单位根检验改造成一个滑动监测器,专门回答一个问题:最近这段行情,是不是已经进入自我强化模式?
「泡沫」需要一个可检验的定义#
「现在是不是泡沫」是金融市场上被问得最多、答得最烂的问题。烂的原因是大多数回答用的定义不可检验:「估值太贵」(多贵算太贵?)、「涨太快」(多快算太快?)、「脱离基本面」(基本面是哪个数?)。
计量经济学给过一个干净得多的定义。写一个最简单的自回归:
- :随机游走。今天的涨跌对明天没有约束力,价格是鞅,这是有效市场的底色。
- :均值回复。涨多了会被拉回来。
- :爆炸过程。偏离会自我放大——涨引来追涨,追涨引来更高的价格。这是泡沫的数学签名。
这个定义的好处是它完全不依赖「合理估值」这种主观锚点。它只问一件事:价格偏离的动力学是收敛的、中性的、还是发散的。而 是可以从数据里估计并检验的。
标准工具是 ADF(Augmented Dickey-Fuller)检验。但教科书版 ADF 有两个先天缺陷,让它没法直接拿来测泡沫——修掉这两个缺陷的过程,就是 SADF 的诞生史。
从 ADF 到 SADF:两处手术#
手术一:检验方向反过来。 教科书 ADF 是左尾检验——原假设「有单位根(ρ=1)」,备择假设「平稳(ρ<1)」,统计量越负越显著。测泡沫要的是右尾:备择假设换成「爆炸(ρ>1)」,统计量越正越显著。检验统计量本身不用改,改的是你看哪一侧的分位数。
手术二:窗口滑动起来。 泡沫是局部现象——一段 1000 日的样本里,可能只有中间 200 日是爆炸的。全样本 ADF 把这 200 日和前后 800 日的随机游走搅在一起,爆炸信号被大量「正常样本」稀释。
Phillips 及合作者的方案(SADF,2011;GSADF/BSADF,2015):不固定窗口起点,把所有可能的起点都试一遍,取统计量的上确界。在每个观察时点 ,对所有满足最小窗口长度的起点 计算 ADF 统计量,然后取最大值:
直觉:如果最近 150 天在爆炸,那么起点恰好落在爆炸段开头的那个窗口会给出巨大的 ADF 统计量——取上确界保证这个窗口不会被平庸的窗口埋没。监测器只对「以当前时点结尾的某段子样本是否爆炸」敏感,历史上的死水完全不构成稀释。
代码骨架(滞后阶数取 0 的简化版,够说明机制):
import numpy as np
def adf_stat(y):
"""带漂移无趋势 ADF,滞后0:Δy_t = a + b·y_{t-1} + e,返回 b 的 t 值"""
dy, ylag = np.diff(y), np.asarray(y)[:-1]
X = np.column_stack([np.ones_like(ylag), ylag])
beta, *_ = np.linalg.lstsq(X, dy, rcond=None)
resid = dy - X @ beta
s2 = resid @ resid / (len(dy) - 2)
se_b = np.sqrt(s2 * np.linalg.inv(X.T @ X)[1, 1])
return beta[1] / se_b
def bsadf(logp, min_window=100, step=5):
"""每个时点:所有合法起点的 ADF 上确界"""
T = len(logp)
out = np.full(T, np.nan)
for t in range(min_window, T):
stats = [adf_stat(logp[t0:t+1])
for t0 in range(0, t - min_window + 1, step)]
out[t] = max(stats)
return outpython起点扫描的 step=5 是纯粹的计算量妥协;生产环境用增量更新的回归可以做到逐点扫描。
受控实验:植入一段温和爆炸#
空谈机制不如做一个知道真相的实验。我构造了 1000 日的对数价格:
- 第 1-500 日:随机游走 + 微弱漂移(正常市场)
- 第 500-730 日:爆炸段,偏离项按 复利放大——注意这个 ρ 只比 1 大 1.2%,日线上肉眼几乎不可辨,这是对真实泡沫的诚实模拟(泡沫在早期从不敲锣打鼓)
- 第 730-780 日:崩溃(负漂移 + 波动放大 1.8 倍)
- 第 780-1000 日:回到随机游走

对这条路径跑 BSADF(最小窗口 100 日),临界值用 200 条纯随机游走路径蒙特卡洛模拟的 95% 分位(≈0.57——注意这个分布是非标准的,不能查正态或 t 分布表,临界值必须模拟,且依赖最小窗口和样本长度设定):

三个读数:
- 报警时点 t=565:爆炸从 t=500 开始,BSADF 在 65 个交易日后越过临界值。三个月的滞后——记住这个数字,后面要用它泼冷水。
- 爆炸段内峰值 11.3,临界值 0.57 的近 20 倍。一旦爆炸积累到可检测的程度,信号不是暧昧的,是压倒性的。
- 崩溃后迅速回落。BSADF 同时监测泡沫的生成与破灭——统计量从高位跳水本身就是「爆炸机制已终结」的信号,PSY 用这个性质给泡沫做起止日期的断代。
为什么起点必须滑动:双泡沫实验#
上面的实验里,锚定起点 t=0 的扩张窗 ADF(即原始 SADF 的单个窗口)其实也能测到第一轮泡沫。真正把「滑动起点」从优化变成刚需的,是样本里有不止一轮泡沫的场景——这正是 Phillips-Shi-Yu 2015 年那篇论文的核心动机。
第二个实验:1000 日里植入两轮泡沫(200-320 日、600-720 日),各自跟着崩溃段:

结果是教科书级的对比:
| 监测器 | 泡沫 2 段内峰值 | 临界值 | 结论 |
|---|---|---|---|
| 扩张窗 ADF(起点锚定 t=0) | 0.14 | 0.57 | 完全失灵 |
| BSADF(起点滑动) | 2.72 | 0.57 | 正常报警 |
失灵的机制值得说透:第一轮泡沫崩溃后,窗口里躺着一段「暴涨然后暴跌」的完整回合——从 t=0 看过去,这段历史整体上更像均值回复而不是爆炸。当第二轮泡沫启动时,锚定起点的检验被迫拖着这段旧历史一起算,第一轮泡沫的尸体成了第二轮泡沫的掩体。BSADF 的滑动起点允许它把窗口起点挪到第一轮崩溃之后,轻装检验最近一段——峰值 2.72,干净利落。
真实市场里泡沫从来不是一次性事件(A 股 2007、2015;纳斯达克 2000、2021),所以实战中没有理由使用锚定起点的版本。
三盆冷水:这个工具不能做什么#
冷水一:65 日滞后意味着它是确认工具,不是预测工具。 BSADF 报警时,爆炸已经运行了三个月。它不能帮你「买在泡沫起点」;它能做的是在泡沫中段给你一个统计上严格的「当前机制是爆炸性的」确认——这对风控(该减仓的仓别拖)和策略切换(趋势跟踪在爆炸段该让位于什么)有价值,对抄底逃顶没价值。检测滞后可以通过缩短最小窗口来压缩,代价是误报率上升——这是硬权衡,没有免费午餐。
冷水二:临界值是设定依赖的。 换最小窗口、换样本长度、换滞后阶数,95% 分位都会动。任何声称「BSADF 大于某个通用数字就是泡沫」的用法都是错的——临界值必须针对你的设定重新模拟。这也意味着跨市场比较 BSADF 的绝对值没有意义,可比的是「相对自己临界值的超越程度」。
冷水三:它对慢牛沉默。 BSADF 检验的是自回归系数不是涨幅。一段漂移强劲但 ρ=1 的行情——每天期望涨 0.1%,但今天的涨不影响明天——可以翻倍而不触发任何报警。这是特性不是缺陷:这种行情在定义上不是自我强化的泡沫,它没有「必然内爆」的动力学结构。但如果你期望用 BSADF 捕捉一切大涨行情,你会失望。
AFML 的用法:特征,不是开关#
López de Prado 在 AFML 第十七章收录结构突变检验,给出的定位和直觉用法相反:不要把 BSADF 当择时开关(报警就清仓),要把它当特征喂给元标注的二级模型。
理由回到这个系列反复出现的架构:一级模型产生信号,二级模型判断「当前环境下这个信号值不值得信」。BSADF 序列恰好是对「当前环境」的一个高质量刻画——
- 趋势跟踪信号在 BSADF 高企时胜率如何?(爆炸段趋势自我强化,可能更高)
- 均值回复信号在 BSADF 从峰值回落时是什么表现?(机制切换期,历史规律最不可靠)
这些问题不需要你手工回答,把 BSADF 值放进二级模型的特征矩阵,让它自己学。相比「报警就清仓」的开关用法,特征用法保留了统计量的连续信息,也避免了对单一临界值的过度依赖。
# 元标注特征矩阵中加入结构突变信息
features['bsadf'] = bsadf_series
features['bsadf_chg_20d'] = bsadf_series.diff(20) # 机制在增强还是衰减
features['bsadf_pct'] = bsadf_series.rolling(500).rank(pct=True) # 相对历史分位pythonA 股落地注记#
- 数据条件无障碍:日线收盘价 + 对数变换即可,BSADF 对数据的要求是这个系列里最低的。
- 涨跌停的双向污染:连续涨停段的价格序列是被截断的爆炸过程(真实需求溢出到第二天),会让 ρ 的估计偏低、检测更滞后;连续跌停同理污染崩溃段的断代。对小盘股建议在检验前标记涨跌停日。
- 2015 年是天然验证集:上证指数 2014-06 至 2015-06 的行情是文献里被 PSY 方法反复检验过的样本,自己实现后拿这段做冒烟测试——BSADF 应在 2014 年底至 2015 年 Q1 之间越过临界值,并在 6 月崩盘后跳水。
- T+1 与择时的错配:即便你执意用报警做减仓开关,A 股 T+1 意味着信号日收盘确认、次日才能执行——把这一天滞后写进回测,爆炸段末端的单日波动足以显著改变结论。
定位与边界#
在 AFML 管线里,结构突变检验属于特征工程层:bar 采样 → 事件过滤 → 标注 → 特征(含 BSADF) → 元标注 → 仓位。它和 CUSUM 过滤器是近亲——都在监测「偏离是否积累到统计显著」——但 CUSUM 管的是采样时机,BSADF 管的是机制识别,一个是开关,一个是温度计。
它能做的:给「当前是否处于自我强化机制」一个可检验、可复现、带显著性水平的答案;给泡沫断代(起止日期);给二级模型提供环境特征。
它不能做的:预测泡沫何时开始(滞后 65 日)、预测泡沫何时破灭(报警持续期内随时可能崩也可能再翻倍)、识别不爆炸的慢牛。
把工具的边界说清楚,恰好是这个工具自己教的方法论:先定义可检验的假设,再谈市场观点。
本文实验代码基于 numpy/matplotlib,BSADF 实现为滞后 0 阶简化版,临界值经 200 路径蒙特卡洛模拟,参数见文中说明。系列相关篇目:CUSUM 事件过滤器、元标注 Meta-Labeling。