3、滑动窗口技术:滚动统计量、滚动相关系数、滚动回归与性能优化
滑动窗口,说白了就是给时间序列数据加一个「移动的放大镜」。你想想看,我们做量化交易,最怕的就是用全量数据算出一个静态指标——市场都变天了,你的参数还停留在三年前。我个人习惯,几乎所有时序特征都要用滚动窗口来算。
嗯,这一节我们就把滑动窗口技术拆开揉碎了讲。从最基础的滚动统计量,到滚动回归,再到性能优化,一步到位。
3.1 滚动统计量:均值与标准差
滚动均值是最简单的。但别小看它,我在项目中遇到过一个大坑:直接用 .rolling().mean() 算出来的结果,前 N-1 个值是 NaN。很多新手直接 dropna() 扔掉,结果数据量少了一大截。
min_periods 参数,要么用 expanding() 做前期填充。
import pandas as pd
import numpy as np
# 生成模拟价格数据
np.random.seed(42)
prices = pd.Series(np.cumsum(np.random.randn(1000) * 0.01) + 100)
# 滚动均值与标准差(窗口20)
rolling_mean = prices.rolling(window=20, min_periods=5).mean()
rolling_std = prices.rolling(window=20, min_periods=5).std()
# 构建布林带
upper_band = rolling_mean + 2 * rolling_std
lower_band = rolling_mean - 2 * rolling_std
这里 min_periods=5 的意思是:窗口内至少有5个有效值就计算。这样前期的 NaN 会少很多,回测也更平滑。
3.2 滚动相关系数
滚动相关系数在配对交易里是核心工具。我见过有人用全量相关系数做配对,结果市场风格一切换,相关系数从0.9直接掉到0.3,仓位全爆了。
滚动相关系数能实时捕捉这种变化。Pandas 里直接用 .rolling().corr() 就行,但要注意——它返回的是一个 MultiIndex 的 DataFrame,很多人第一次看到会懵。
# 两个资产收益率序列
returns_a = pd.Series(np.random.randn(1000) * 0.02)
returns_b = returns_a * 0.7 + np.random.randn(1000) * 0.01
# 滚动相关系数(窗口60)
rolling_corr = returns_a.rolling(window=60).corr(returns_b)
# 当相关系数低于阈值时,开仓做配对
threshold = 0.5
signal = (rolling_corr < threshold).astype(int)
3.3 滚动回归:实时计算Beta
滚动回归比滚动相关系数更强大。它能给出截距、斜率、R方等一系列统计量。在因子模型里,我们经常用滚动回归算股票的动态Beta。
Pandas 没有直接提供滚动回归函数,但我们可以用 .rolling().apply() 自己实现。不过要注意性能问题——我当年用纯Python循环跑1000只股票的滚动回归,跑了整整一个周末。
from scipy import stats
def rolling_regression(y, x, window=60):
"""滚动线性回归,返回斜率(Beta)"""
def regress_slice(data):
y_slice = data[:, 0]
x_slice = data[:, 1]
if len(y_slice) < 10: # 最少10个样本
return np.nan
slope, intercept, r_value, p_value, std_err = stats.linregress(x_slice, y_slice)
return slope
# 合并数据
combined = np.column_stack([y, x])
result = pd.Series(
[np.nan] * (window - 1) +
[regress_slice(combined[i-window+1:i+1]) for i in range(window-1, len(combined))]
)
return result
# 使用示例
market_returns = pd.Series(np.random.randn(1000) * 0.015)
stock_returns = market_returns * 1.2 + np.random.randn(1000) * 0.01
rolling_beta = rolling_regression(stock_returns, market_returns, window=60)
3.4 窗口函数性能优化
说到性能优化,这是我最想分享的部分。你想想看,一个策略里可能有几十个滚动统计量,如果每个都跑一遍循环,回测速度会慢到让你怀疑人生。
我总结了几条实战经验:
- 用 NumPy 向量化代替 Pandas rolling: 对于简单的滚动均值、标准差,直接用
np.convolve或np.cumsum实现,速度能快10倍以上。 - 避免在
.rolling().apply()里用复杂函数: 每次 apply 都是 Python 级别的循环,非常慢。能用内置函数(mean, std, corr)就别自己写。 - 用 Numba 加速自定义滚动函数: 如果必须用自定义逻辑,用
@njit装饰器编译成机器码。
import numpy as np
from numba import njit
@njit
def rolling_mean_numba(data, window):
"""用Numba加速的滚动均值"""
n = len(data)
result = np.empty(n)
result[:window-1] = np.nan
# 初始窗口求和
window_sum = np.sum(data[:window])
result[window-1] = window_sum / window
# 滑动更新
for i in range(window, n):
window_sum += data[i] - data[i-window]
result[i] = window_sum / window
return result
# 测试速度
data = np.random.randn(100000)
%timeit rolling_mean_numba(data, 20) # 约0.3ms
%timeit pd.Series(data).rolling(20).mean() # 约2.1ms
3.5 实战中的避坑指南
最后,分享几个我踩过的坑:
- 窗口边界处理: 滚动窗口的前期数据一定要想清楚怎么处理。是用 expanding 填充,还是直接丢弃?我建议在回测里用 expanding,在实盘里用固定窗口。
- 未来信息泄露: 滚动窗口一定要用「过去的数据」算「当前的指标」。我见过有人不小心用了
.shift(-1),结果把未来的数据卷进来了——回测收益高得离谱,实盘直接亏光。 - 内存爆炸: 如果你同时计算几百个股票的滚动统计量,DataFrame 会变得巨大。我建议用
groupby分批计算,或者用dask做分布式处理。
好了,滑动窗口技术就讲到这里。下一节我们会讲「指数加权移动平均(EWMA)」,它其实是滑动窗口的一种变体,但权重设计更巧妙。到时候我会分享一个我用EWMA做趋势跟踪的实战案例。