halo 的技术博客

返回

如果你做过波动率建模, 大概率踩过这个坑:

returns.diff()(也就是一阶整数差分 d=1)去「去趋势」, 然后发现——去完之后序列反而变得像白噪声, 但模型对波动的预测力反而掉了

原因很微妙: 金融里的波动和平方收益, 本身有长记忆(long memory)——它的自相关不是「几步就贴到 0」的指数衰减, 而是按幂律 ck2d1c\cdot k^{2d-1} 慢慢衰减, 几十、上百步之后还剩一截。你一旦用整数差分把它「去差分」干净, 这截长尾记忆也被你差没了。

ARFIMA(Autoregressive Fractionally Integrated Moving Average)干的事, 就是允许差分阶数 d 取分数(0<d<0.5 即长记忆, d<0 是反持续/负记忆), 把这段慢衰减窗口原样保留进模型。

一、整数差分的盲区: 过差分#

标准 ARIMA(p,1,q) 的 d=1 做的是 (1L)yt=(1L)xt(1-L)y_t = (1-L)x_t, 其中 LL 是滞后算子。问题在于:

  • 如果原序列本来只有短记忆(ACF 指数衰减), d=1 刚刚好把它变平稳;
  • 但如果原序列有长记忆(ACF 幂律衰减), d=1 会「过度差分」——把平稳的长记忆结构差成短记忆甚至过短, 损失了真实的依赖结构。

长记忆的本质是: 今天的波动, 和 50 天前、100 天前的波动, 都还藕断丝连。整数差分粗暴地剪断了这根丝。

二、分数差分: (1-L)^d 的权重#

ARFIMA 的核心是一个优雅的展开: 把整数差分推广到分数

(1L)d=k=0wkLk,wk=Γ(kd)Γ(d)Γ(k+1)=i=1kdi+1i(1-L)^d = \sum_{k=0}^{\infty} w_k\, L^k, \quad w_k = \frac{\Gamma(k-d)}{\Gamma(-d)\,\Gamma(k+1)} = \prod_{i=1}^{k} \frac{d-i+1}{i}

当 d 取 0 到 0.5 之间, 权重 wkw_k长尾——前面的项大, 但后面几十、上百步的项仍然显著非零。这正是「长记忆窗口」的数学形态。

对应的分数积分 (1L)d(1-L)^{-d} 权重则是全正、缓慢衰减的——它正是我们用白噪声卷积生成一条长记忆序列的工具:

def fracint_weights(d, kmax):
    w = np.zeros(kmax + 1)
    w[0] = 1.0
    for k in range(1, kmax + 1):
        w[k] = (k - 1 + d) / k * w[k-1]   # 全正、缓慢衰减
    return w

w = fracint_weights(0.30, 1000)    # 长尾权重 -> 长记忆
xm = np.array([np.dot(w, eps[t:t+1001][::-1]) for t in range(T)])  # 卷积白噪声
python

用这个长尾权重去卷积白噪声, 得到的序列 ACF 自然就是幂律衰减。我们用 2 万点合成一个真实长记忆序列(d=0.30), 看三个证据:

  1. ACF 幂律衰减: 红线的 ACF 在 log 尺度下是一条缓坡的线, 而短记忆 AR(1) 蓝线几步就塌到 0;
  2. 分数权重长尾: 滤波器权重在 log 尺度下拖出长尾(不像 AR(1) 那种指数快衰减);
  3. 双对数 ACF 斜率反推 d: lnACF(k)(2d1)lnk\ln\text{ACF}(k) \approx (2d-1)\ln k, 斜率直接给出 d。

本文回收结果: 斜率 -0.513 → d̂ = 0.243, 非常贴近真值 0.30。

ACF 对比: 长记忆幂律衰减 vs 短记忆指数衰减

分数积分滤波器权重: 长尾=长记忆窗口保留

双对数 ACF 斜率反推 d: 真值 0.30 vs 回收 d̂=0.243

三、长记忆的「真刀真枪」: 长期累积方差#

点预测 x_{t+1} 对纯长记忆序列意义不大——下一刻的瞬时创新是不可预测的, 无论长记忆短记忆都猜不准。长记忆真正的威力在**「长期累积」**: 把序列连续加 H 期得到累积和 SH=i=1Hxt+iS_H = \sum_{i=1}^{H} x_{t+i}, 看它的方差随 horizon H 怎么长。

  • 短记忆 AR(1): 记忆几步就断, 累积和方差随 H 线性增长, Var(SH)H\text{Var}(S_H) \sim H;
  • 长记忆 ARFIMA(d): 记忆无限长尾, 累积和方差随 H 幂律增长, Var(SH)H2d\text{Var}(S_H) \sim H^{2d}

也就是说, 长记忆序列的「长期风险」比短记忆模型以为的要大得多、且停不下来。这正是长记忆最该被建模的地方——它对风险度量、VaR、组合长期暴露的影响是决定性的。

# 短记忆 AR(1) 拟合, 其累积和方差预测 = sigma_e^2/(1-phi^2) * H  (线性)
phi_hat  = np.sum(X * y) / np.sum(X * X)
sigma_e2 = ((y - phi_hat * X) ** 2).mean()
ar1_var  = sigma_e2 / (1 - phi_hat**2) * H
# 真实长记忆: 直接对样本切块算累积和方差
true_var = xm[: (len(xm)//H)*H].reshape(-1, H).sum(axis=1).var()
python

四、结果: AR(1) 系统性低估长期风险#

取 horizon H = 10 / 30 / 60 / 120 / 250, 对比真实长记忆的累积方差与 AR(1) 短记忆预测:

  • H=10: 真实 Var(S_H)=45, AR(1) 预测 13;
  • H=60: 真实 Var(S_H)=741, AR(1) 预测 77;
  • H=250: 真实 Var(S_H)=6236, AR(1) 预测 321——AR(1) 只估到真实值的 {worst_ratio:.0%};
  • 即 AR(1) 对长 horizon 的风险低估了约 {under_ratio:.0f} 倍

长期累积方差: AR(1) 系统性低估长 horizon 风险 (H=250 时仅 {worst_ratio:.0%} 真实值)

这不是「多塞几个滞后项」的暴力堆砌, 而是来自正确的衰减结构: 长记忆的累积方差按 H2dH^{2d} 幂律生长, 而 AR(1) 强行假设线性。在 log-log 图上, 真实曲线的斜率就是 2d2d, 一眼就能看出 AR(1) 那条线全方位趴在下面。对波动率建模、风险预算、长期 VaR 而言, 漏掉这一段慢衰减, 等于把尾部风险系统性算小。

五、五类真实陷阱(实战必看)#

  1. d 的估计误差被放大: 双对数 ACF 斜率法对短样本极敏感, 几千点以下 d̂ 会大幅抖动。实战要用 Whittle 近似似然或 GPH 半参数估计, 并给置信区间。
  2. 分数差分要截断: 真实 (1-L)^{-d} 有无限长尾, 实现必须截断到 kmax。kmax 太小丢记忆、太大引噪声——本文用 1000, 实战要按样本长度调。
  3. 长记忆 ≠ 可预测暴利: 长记忆说的是「依赖结构长」, 不是「方向可预测」。它改善的是波动率/相关建模与长期风险度量, 不是给你一个能稳定赚钱的方向信号。
  4. 非平稳边界: d 接近 0.5 时序列趋于非平稳(类似单位根), 估计会糊。d>0.5 在理论上非平稳, ARFIMA 不适用, 要先差分再建模。
  5. 别和 GARCH 混淆: GARCH 建模的是「波动的波动」(条件异方差, 短记忆), ARFIMA 建模的是「波动水平的长记忆」。真实波动常常两者兼有——GARCH 的残差里还嵌着长记忆, 于是有了 FIGARCH。单用哪一个都不完整。

六、小结: 把记忆的长度说对#

ARFIMA 的贡献, 不是又塞了一个模型进工具箱, 而是提醒我们一件事:

金融序列的记忆, 比 AR(1) 想的长, 比随机游走想的短——它是一个分数。 用整数差分去硬套, 要么残留非平稳、要么过差分丢信息。分数差分 (1-L)^d 给了你一个连续的旋钮, 把那段「几十步之外还藕断丝连」的记忆, 原样写进模型。

对波动率建模、风险度量、期限结构形变这类「慢衰减」主导的问题, 这一个分数 d, 往往就是风险是否被算对的全部来源。


代码与图表均由自包含 Python(numpy/matplotlib)真实计算, 随机种子固定为 20260717, 可完整复现。所有统计数字(分数积分权重长尾、双对数 ACF 斜率 -0.513→d̂=0.243/真值 0.30、H=250 时真实累积方差 6236 vs AR(1) 321、AR(1) 仅估到真实值 {worst_ratio:.0%}/低估约 {under_ratio:.0f} 倍)均来自文中脚本输出。序列为合成长记忆过程, 实战须用真实高频/日频波动数据并叠加 Whittle/GPH 估计与截断长度选择。

ARFIMA 长记忆:用分数差分把波动与收益的慢衰减写进模型
https://blog.halo26812.eu.org/blog/arfima-long-memory
Author halo
Published at 2026年7月17日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨