回测报告里经常能见到这样一句话:「策略日均超额 0.26%,t 值 2.5,显著。」这个 t 值很可能被高估了。问题并不出在数据本身,而是普通 t 检验默认每天的观测相互独立;量化策略的日收益几乎从不满足这个前提。
本文围绕三件事展开:t 值到底衡量什么;相邻交易日相关为什么会推高 t 值;HAC(Newey–West)如何修正,并一步一步算清楚。所有例子基于模拟数据,核心代码见文末。
一、普通 t 值:平均值是不是真的大于 0
假设一个选股策略每天的超额收益是 $x_1, x_2, \dots, x_n$。我们想知道:它真实的平均超额是不是大于 0?
光看平均值并不够。日均 +0.26% 看起来不错,但如果每日波动是 ±2%,这 0.26% 也有可能纯属运气。
t 值衡量的是“平均值相对它自己的误差有多大”:
t = 平均值 ÷ 标准误
标准误 = 每日波动 ÷ √n
用 338 个交易日举例:
标准误 = 1.92% ÷ √338 ≈ 0.104%
t = 0.265% ÷ 0.104% ≈ 2.53
通常认为 t ≥ 2 时,大约有 95% 的把握认为真实均值不为 0(单边看大于 0 的置信度更高)。
关键就在这个“÷ √n”上。它背后的假设是:338 天就是 338 份独立的信息。
二、相邻日子相关,信息就“缩水”了
量化策略的日收益,相邻几天往往是正相关的:
- 市场风格会延续:小盘股强势阶段可能持续几周,模型顺风时连续几天走好,逆风时连续几天走差。
- 持仓有重叠:调仓慢、持股周期长的策略,相邻两天手里的股票很多是同一批。
- 收益区间重叠:每天计算一次“未来 5 日收益”时,相邻两天的 5 日收益有 4 天是重合的,这是最极端的情形。
相邻日子正相关时,连续几天的好结果,说穿了只是“同一件好事”重复听了好几遍。338 天的有效信息,可能只相当于一两百天。
对于最简单的情形,即每天的结果只跟前一天有关、相关系数为 ρ(统计上叫 AR(1)),有一个近似公式:
有效天数 ≈ n × (1 − ρ) ÷ (1 + ρ)
t 值被高估的倍数 ≈ √( (1 + ρ) ÷ (1 − ρ) )
| 相邻两天的相关系数 ρ |
338 天的有效天数 |
t 值被高估约 |
| 0 |
338 |
1.00 倍 |
| 0.1 |
277 |
1.11 倍 |
| 0.3 |
182 |
1.36 倍 |
| 0.5 |
113 |
1.73 倍 |
ρ = 0.3 并不罕见。此时普通 t 值如果是 2.7,真实大约只有 2.0。
模拟:一个什么都不会的策略,有多大概率“显著”?
生成 5000 个真实平均值为 0 的策略,每个 338 天,相邻两天的相关系数为 ρ。理论上,t ≥ 2 的比例应该接近 2.5%(单边):
| ρ |
普通 t ≥ 2 的比例 |
HAC t ≥ 2 的比例 |
| 0 |
2.3% |
2.6% |
| 0.1 |
3.7% |
2.8% |
| 0.3 |
7.2% |
3.4% |
| 0.5 |
12.3% |
4.5% |
ρ = 0.5 时,用普通 t 检验,每 8 个没有真实能力的策略里就有 1 个会被判为“显著”,差不多是应有比例的 5 倍。HAC 则把比例拉回到接近 2.5%。
更极端的例子:重叠收益
每天计算一次“未来 5 日收益”作为策略表现(例如持有 5 天、每天调仓 1/5),相邻两天的相关系数理论上高达 0.8。同样模拟 5000 次,真实平均值为 0:
| 方法 |
t ≥ 2 的比例 |
| 普通 t |
18.8% |
| HAC,滞后 5 天 |
4.5% |
| HAC,滞后 10 天 |
3.6% |
普通 t 检验在这里几乎失效:将近五分之一的纯噪声会被判为显著。
三、HAC 怎么修正
HAC 是 Heteroskedasticity and Autocorrelation Consistent 的缩写,意思是“对异方差和自相关都稳健”。最常用的版本由 Newey 和 West 在 1987 年提出,所以也常被称为 Newey–West 标准误。
思路
普通标准误只用到每天自己的波动(方差)。HAC 再把“相邻日子之间的协方差”加进来:相邻日子正相关,误差就加大;负相关,误差就减小。
公式
长期方差 = γ₀ + 2 × [ w₁·γ₁ + w₂·γ₂ + … + w_L·γ_L ]
HAC 标准误 = √( 长期方差 ÷ n )
HAC t = 平均值 ÷ HAC 标准误
- γ₀:日收益的方差
- γⱼ:相隔 j 天的协方差,即“今天”和“j 天前”一起偏离平均值的程度
- L:最多看到相隔几天,称为滞后阶数
- wⱼ = 1 − j ÷ (L + 1):权重,相隔越远的协方差权重越小(也叫 Bartlett 核)。这样处理能保证估计的方差不会是负数,也避免远处噪声干扰
为了理解方便,可以把长期方差写成“普通方差 × 放大倍数”:
放大倍数 = 1 + 2 × [ w₁·ρ₁ + w₂·ρ₂ + … + w_L·ρ_L ]
ρⱼ 是相隔 j 天的自相关系数。HAC 标准误 = 普通标准误 × √放大倍数。
滞后阶数 L 取多少
常用经验公式(Newey–West 1994):
L = floor( 4 × (n / 100)^(2/9) )
n = 338 时,L = 5;n = 1000 时,L = 6。
经验上:
- 收益区间有重叠时,L 至少要等于重叠的天数。持有 5 天就至少取 5,否则修正不够。
- L 取大一点通常更保守,但样本少时,估计本身会变得不稳定。
四、一步一步算一遍
模拟一个策略:真实日均超额 0.3%,每日波动 2%,相邻两天的相关系数 0.3,共 338 天。
第 1 步:基本统计
| 项目 |
数值 |
| 实际日均超额 |
0.265% |
| 每日波动 |
1.92% |
| 普通标准误 |
1.92% ÷ √338 = 0.104% |
| 普通 t |
2.53 |
第 2 步:滞后阶数:n = 338,L = floor(4 × 3.38^0.222) = 5
第 3 步:自相关系数和权重
| 相隔天数 j |
自相关 ρⱼ |
权重 wⱼ |
wⱼ × ρⱼ |
| 1 |
0.360 |
0.833 |
0.300 |
| 2 |
0.050 |
0.667 |
0.033 |
| 3 |
−0.077 |
0.500 |
−0.039 |
| 4 |
−0.042 |
0.333 |
−0.014 |
| 5 |
−0.069 |
0.167 |
−0.012 |
| 合计 |
|
|
0.269 |
第 4 步:放大倍数 = 1 + 2 × 0.269 = 1.54
第 5 步:HAC 标准误和 t 值
HAC 标准误 = 0.104% × √1.54 ≈ 0.129%
HAC t = 0.265% ÷ 0.129% ≈ 2.05
同一组数据,普通 t 是 2.53,HAC t 只有 2.05,刚好压在 2 的门槛上。如果门槛设在 2.5,用普通 t 就会得到错误结论。
五、几点注意
-
HAC 不一定让 t 值变小。 相邻日子负相关时(比如反转型策略),HAC 的标准误反而更小。模拟一个相邻相关为 −0.2 的策略:普通 t 2.09,HAC t 2.44。HAC 不是“打折工具”,而是“更准的工具”。
-
HAC 只修正自相关,不修正多重测试。 试了 100 个策略、挑出 HAC t 最高的那个,它的 t 值照样会被选择偏差抬高,需要另外的方法处理,比如 DSR、SPA 等。
-
HAC 不修正厚尾。 少数几天的极端收益撑起来的平均值,HAC t 照样可能很好看。要另外看“去掉最好的几天还剩多少”。
-
样本少时,HAC 本身也不太准。 几十天的数据,自相关都估不稳,HAC t 只能作参考。
-
回归里也一样。 用 α 的 t 值判断“超额是否显著”、用斜率判断“超额是否衰减”时,同样应该用 Newey–West 标准误。主流统计软件都支持:Python 的 statsmodels 里设 cov_type="HAC" 即可。
六、一句话总结
普通 t 检验假设每天都是独立的新信息;量化策略的日收益往往前后相关,信息比天数少,普通 t 值因此虚高。HAC(Newey–West)把相邻日子的协方差算进标准误,给出更诚实的 t 值。
报告 t 值时,顺手写一句“Newey–West,滞后 5 天”,读者就知道这个数是可信的。
参考文献
- Newey, W. K., & West, K. D. (1987). A Simple, Positive Semi-Definite, Heteroskedasticity and Autocorrelation Consistent Covariance Matrix. Econometrica, 55(3), 703–708.
- Newey, W. K., & West, K. D. (1994). Automatic Lag Selection in Covariance Matrix Estimation. Review of Economic Studies, 61(4), 631–653.
- Andrews, D. W. K. (1991). Heteroskedasticity and Autocorrelation Consistent Covariance Matrix Estimation. Econometrica, 59(3), 817–858.
附:核心代码
import math
import numpy as np
def hac_t(x, lags=None):
"""日序列 x 的平均值是否显著不为 0:Newey–West t 值(Bartlett 核)。"""
x = np.asarray(x, dtype=float)
n = len(x)
L = int(4 * (n / 100) ** (2 / 9)) if lags is None else lags
e = x - x.mean()
lrv = e @ e / n
for j in range(1, L + 1):
lrv += 2 * (1 - j / (L + 1)) * (e[j:] @ e[:-j] / n)
return x.mean() / math.sqrt(lrv / n)
用 statsmodels 的话,对常数项做回归即可。关掉小样本修正,结果与上面的函数一致;开着的话,t 值会略小一点:
import statsmodels.api as sm
fit = sm.OLS(x, np.ones(len(x))).fit(cov_type="HAC",
cov_kwds={"maxlags": 5, "use_correction": False})
print(fit.tvalues[0])