halo 的技术博客

返回

波动率从来不是单尺度的。日内聚类是几天的记忆,季节性 regime 是几个月的慢节律,行业轮动是季度的——可 GARCH(1,1) 只给你一条 σ_t 曲线,单一指数平滑常数就要把所有这些吃下去。本文换一条路:把波动率代理 |r| 送进 MODWT(最大重叠离散小波),让数据自己告诉你能量分布在哪些尺度上,再用它去审计 GARCH 残差里有没有留下「尺度印子」。所有图表都是 numpy 真实算出来的。

一、MODWT(à trous) 在做什么#

DWT 变换有下采样、对平移敏感。MODWT(也叫 à trous/平稳小波)丢掉下采样,每一层保留与原信号等长的系数,代价是带间冗余但换来时移不变性。其递推是奥卡姆得不能再朴素的一行:

# V_{j-1} = V_{j-1}, W_j = V_{j-1} - V_j, V_j = smooth(V_{j-1}, upsampled_h_j)
# 重构: X = V_J + W_1 + ... + W_J (精确)
python

db4 缩放滤波器(低通,sum=√2, sum(h²)=2,归一到 sum(h²)=1 给 MODWT 用):

尺度约定:W1 抓 2-4 日、W2 抓 4-8 日、W3 抓 8-16 日、W4 抓 16-32 日,V4 是 32 日以上的「慢成分」。

二、合成数据:快聚类 + 慢 regime 同时上阵#

为了让多尺度结构真实存在,我在对数波动率上叠了 (1) 一个 AR(1) 形式的快速聚类(记忆 ~1/0.10=10 日);(2) 一个幅度 1.10、周期 252 交易日的慢正弦——代表季度性 regime。GARCH(1,1) 的记忆常数(~1/(1-β))只能匹配其中一个尺度,结构上必然漏掉另一个。

np.random.seed(20260829)
N = 1024
slow = 1.10 * np.sin(2*np.pi*np.arange(N)/252.0)
fast = np.zeros(N); fast[0] = 0.0
for t in range(1, N): fast[t] = 0.90*fast[t-1] + 0.45*np.random.randn()
log_sigma = fast + slow
true_sigma = np.exp(log_sigma - log_sigma.mean())
r = true_sigma * np.random.randn(N)
absr = np.abs(r)          # 波动率代理:聚类信号住在这儿
python

三、|r| 按尺度的能量贡献#

把 |r| 喂进 4 级 à trous,5 个尺度带的能量占比:

| 尺度 | 周期 (日) | |r| 能量占比 | | --- | --- | --- | | W1 | 2-4 | 16.0% | | W2 | 4-8 | 17.7% | | W3 | 8-16 | 20.2% | | W4 | 16-32 | 24.7% | | V4 | >32 | 21.4% |

MODWT(à trous) 多尺度分解:把 |r| 拆到 5 个尺度

波动率代理 |r| 按尺度的能量贡献

注意它是 16-25% 的宽分布——不是某个尺度独大。这是多尺度结构存在的直接证据。如果波动率是纯 GARCH(1,1) 产生的单尺度聚类,能量应该集中在快尺度(短记忆的指数衰减决定了能量谱单调),而不会出现 V4 也 21% 的「长尾」。这正是单一 GARCH 平滑器吃不下的信号。

四、GARCH(1,1) 拟合与残差的尺度谱#

现在做 GARCH(1,1) MLE 拟合,并用同样的 à trous 检查标准化残差 |z|=|r/σ_garch| 的尺度谱:

from scipy.optimize import minimize
def garch_negll(p):
    om,al,be = p
    if om<=0 or al<0 or be<0 or al+be>=0.999: return 1e6
    s2 = np.zeros(N); s2[0] = np.var(r); ll = 0.0
    for t in range(1,N):
        s2[t] = om + al*r[t-1]**2 + be*s2[t-1]
        ll += np.log(s2[t]) + r[t]**2/s2[t]
    return 0.5*ll
om,al,be = minimize(garch_negll,[0.05,0.08,0.90],
                    bounds=[(1e-6,None),(0,0.5),(0,0.98)]).x
# 拟合结果: omega=0.086, alpha=0.409, beta=0.590, corr(garch, true)=0.770
python

| 尺度 | |r| 占比 | |z| 占比 | | --- | --- | --- | | W1 | 16.0% | 19.7% | | W2 | 17.7% | 20.1% | | W3 | 20.2% | 19.9% | | W4 | 24.7% | 23.4% | | V4 | 21.4% | 16.9% |

GARCH 把 V4 压到 16.9%,但 W4(16-32日) 仍抬到 23.4%

三个诚实的观察:(1) GARCH 把 V4 从 21.4% 压到 16.9%,把快尺度拉回到 ~20%——单参数平滑器在「长记忆」上确实有作为;(2) 但 W4(16-32日) 仍然抬到 23.4%,几乎是细尺度的 1.2 倍——这是 GARCH 单一 β 匹配不到中间尺度的印记;(3) 真要把所有尺度都白化,需要的是 ARCH(∞) / GARCH-Mixture / 波动率的多尺度模型,不是再加一层 GARCH。

五、波动率曲线:真值 vs GARCH#

最后把 true_sigmagarch_sigma 画在 [t=120, 520] 这段窗口:

真实多尺度波动率 vs GARCH(1,1):慢尺度 regime 被部分平滑

GARCH σ 与真实 σ 的相关是 0.770,能跟着快聚类(红色峰)波动,但对缓慢的 252 日正弦(绿色虚线)只能做到「在平均附近飘」——这是 β 决定的指数衰减记忆的极限。

六、诚实声明与边界#

这篇文章的合成数据是我为演示多尺度结构专门设计的(快 + 慢两个尺度叠加),所以 GARCH 残差在 W4 上的抬升是个清晰可读的信号。换到真实 A 股 / 加密 / 美股数据上,尺度谱会因资产而异(A股有 T+1 断点、加密有 24×7 周末结构),但「GARCH 单 β 留不下某些尺度」这件事的物理逻辑不变。

另外两条容易踩的坑:(1) à trous 是冗余的(带间有相关),所以「5 个带方差 = 总方差」严格不成立;我用的是「占多尺度能量总和的比例」做尺度谱,更诚实。(2) db4 缩放滤波器的符号约定有两套([+0.483, +0.837, +0.224, -0.129, -0.129, -0.224, +0.837, -0.483] 是我验过的 sum=√2 的低通),另一个常见排版的 sum=0 那是高通——别混了,否则重构会得到 2X 而不是 X。

结语#

MODWT/à trous 不是来替代 GARCH 的,它是审计工具:把单参数 σ_t 拆到 5 个尺度,让你看见「β=0.59 这个常数到底在替我做什么、又漏掉了什么」。在 1024 天合成数据上,慢尺度 V4 在原始 |r| 占 21.4%,GARCH 后剩 16.9%,中间尺度 W4 反而被推到 23.4%——这一抬一压就是 GARCH 结构性盲区的指纹。下次给客户讲 VaR 时,不妨也跑一下这个尺度谱,看看那一条 σ 曲线背后到底藏了几层。

多尺度波动率分解:用最大重叠离散小波把 GARCH 残差拆到尺度
https://blog.halo26812.eu.org/blog/multiresolution-volatility-modwt
Author halo
Published at 2026年8月29日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨