第二章:高阶特征工程(上)——滚动窗口里的统计学
各位同学,欢迎来到高阶特征工程的第一部分。说实话,很多做量化的人,一开始都盯着价格本身,或者简单的均线、MACD这些。但真正拉开差距的,往往是你对数据「切」和「算」的功夫。今天我们就聊聊,怎么用滚动窗口,把那些藏在数据深处的统计特征挖出来。
2.1 为什么是滚动窗口?
你想想看,市场是动态的。昨天的波动和今天的波动,意义完全不同。静态地算一个全样本的偏度、峰度,那等于把过去十年的数据混在一起,告诉你「嗯,这个股票挺稳定的」。但实际呢?可能最近三个月已经剧烈波动了。
滚动窗口的核心思想很简单:只关注最近一段时间的状态。比如过去20个交易日、60个交易日。窗口滑过去,每个时间点都算一遍特征。这样你得到的,是一组随时间变化的统计量——这才是真正能用于预测的信号。
关键点:滚动窗口的长度选择,直接影响特征的质量。我个人习惯,短周期(如5-10天)捕捉噪声,长周期(如60-120天)捕捉趋势。没有绝对正确,只有适合你的策略。
2.2 偏度与峰度:分布的形状会说话
偏度衡量的是分布的不对称性。正偏度意味着右尾更长,也就是容易出现极端正收益。负偏度则相反。在量化里,偏度可以告诉你:这个资产最近是不是「容易出大事」。
峰度呢?它衡量的是分布的「尖峭」程度。高峰度意味着极端值频繁出现——说白了,就是波动剧烈,肥尾风险大。我记得有一次做CTA策略,某个品种的峰度突然飙升到8以上,我立刻减仓了。结果第二天就出了黑天鹅事件。嗯,这不是玄学,是统计在说话。
import numpy as np
import pandas as pd
def rolling_skewness(series, window=20):
"""滚动偏度"""
return series.rolling(window).skew()
def rolling_kurtosis(series, window=20):
"""滚动峰度(Fisher定义,正态分布为0)"""
return series.rolling(window).kurt()
# 示例:计算某股票日收益率的滚动偏度
returns = pd.Series([0.01, -0.02, 0.03, -0.01, 0.02, ...])
skew_20 = rolling_skewness(returns, 20)
kurt_20 = rolling_kurtosis(returns, 20)
避坑指南:我曾经直接用默认的rolling().kurt(),结果发现它返回的是Fisher峰度(正态分布为0)。而有些文献用的是Pearson峰度(正态分布为3)。两者差一个常数3,千万别搞混。建议统一用Fisher定义,因为更直观——正数就是肥尾。
2.3 高阶矩:不止于偏度和峰度
偏度是三阶矩,峰度是四阶矩。那五阶、六阶呢?理论上你可以算任意阶矩。但在实际交易中,我很少用到五阶以上。为什么?因为高阶矩对极端值极其敏感,样本量不够时,算出来全是噪声。
不过,有一个例外:滚动五阶矩在某些高频策略中,可以用来检测「微小的不对称性」。比如,当五阶矩突然变大,可能意味着市场深度在悄悄变化。但这需要至少500个以上的数据点,否则别碰。
def rolling_moment(series, order, window=20):
"""滚动n阶中心矩"""
mean = series.rolling(window).mean()
centered = series - mean
moment = (centered ** order).rolling(window).mean()
return moment
# 五阶矩示例(注意:需要足够长的窗口)
moment_5 = rolling_moment(returns, 5, 100)
警告:高阶矩的计算非常消耗计算资源。如果你用Python的纯循环去算,100万条数据可能要跑几分钟。建议用numpy的向量化操作,或者用numba加速。我见过有人用for循环算滚动四阶矩,跑了半小时还没出结果……
2.4 分位数特征:比均值更稳健
均值容易被极端值带偏。但分位数不会。比如,滚动20日的90%分位数,告诉你「过去20天里,90%的收益率都低于这个值」。这比均值更能反映真实的尾部风险。
我个人常用的分位数特征有:
- 滚动中位数(50%分位):比均值更稳健的趋势指标
- 滚动90%分位与10%分位的差值:衡量波动范围,比标准差更抗噪
- 滚动分位数斜率:比如计算最近5日分位数的变化方向,可以捕捉趋势加速
def rolling_quantile(series, q=0.9, window=20):
"""滚动分位数"""
return series.rolling(window).quantile(q)
# 计算滚动90%分位与10%分位的差值(分位数范围)
q90 = rolling_quantile(returns, 0.9, 20)
q10 = rolling_quantile(returns, 0.1, 20)
quantile_range = q90 - q10
实战经验:我在做期权波动率交易时,特别喜欢用「滚动分位数偏度」——也就是(90%分位 - 50%分位)与(50%分位 - 10%分位)的比值。这个比值大于1.5时,说明右尾风险显著偏高,这时候卖出虚值看涨期权往往有不错的胜率。
2.5 滚动夏普比率:动态的风险调整收益
传统的夏普比率,算的是整个样本期的平均超额收益除以标准差。但问题是,策略的表现在不同时期差异巨大。滚动夏普比率,就是看最近一段时间内,每单位风险能换来多少收益。
计算方式很简单:用滚动窗口内的平均收益率,除以滚动窗口内的收益率标准差,再乘以年化因子(比如252的平方根)。但要注意:当滚动窗口太短时,夏普比率会剧烈波动,失去参考意义。
def rolling_sharpe(returns, window=60, rf=0.0, periods_per_year=252):
"""滚动夏普比率"""
excess_returns = returns - rf / periods_per_year
rolling_mean = excess_returns.rolling(window).mean()
rolling_std = excess_returns.rolling(window).std()
sharpe = rolling_mean / rolling_std * np.sqrt(periods_per_year)
return sharpe
# 示例:计算60日滚动夏普
sharpe_60 = rolling_sharpe(returns, 60)
我的习惯:我一般用60个交易日(约3个月)作为滚动窗口。太短了噪声大,太长了反应迟钝。另外,我建议同时计算滚动夏普的滚动标准差——如果夏普本身波动很大,说明策略不稳定,这时候要小心。
2.6 把这些特征组合起来
单个特征往往不够。真正有效的,是特征的组合。比如:
- 当滚动偏度为负且滚动峰度很高时,意味着「左尾风险正在积聚」——这是做空波动率的好时机?不一定,但值得警惕。
- 当滚动夏普比率从高位快速回落,同时分位数范围在扩大,说明策略正在失效——我建议减仓。
- 当滚动90%分位持续上升,但中位数没动,说明只有少数极端收益在拉动——这不是健康的上涨。
下面这张图,展示了本章的核心知识体系:
2.7 一个完整的实战案例
假设我们有一个股票池,想用这些特征来预测未来5日的波动率。我会这样做:
- 对每只股票,计算滚动20日的偏度、峰度、90%分位与10%分位差值、滚动夏普比率
- 把这些特征作为输入,训练一个简单的随机森林模型
- 回测时,注意特征之间的相关性——比如偏度和峰度往往高度相关,可以考虑只保留一个
# 伪代码示例
def build_features(df, window=20):
features = pd.DataFrame(index=df.index)
features['skew'] = df['returns'].rolling(window).skew()
features['kurt'] = df['returns'].rolling(window).kurt()
features['q_range'] = (df['returns'].rolling(window).quantile(0.9) -
df['returns'].rolling(window).quantile(0.1))
features['sharpe'] = rolling_sharpe(df['returns'], window)
return features.dropna()
注意:这些特征不是越多越好。我见过有人一口气算了20个滚动统计量,结果模型过拟合得一塌糊涂。建议先算5-8个核心特征,然后做特征重要性分析,再逐步添加。记住:简单模型+好特征,往往比复杂模型+垃圾特征更有效。
好了,这一章的内容就到这里。滚动窗口的特征工程,说白了就是「用最近的数据,算最有效的统计量」。偏度、峰度、分位数、夏普比率——这些工具用好了,你的策略会多出一双「透视眼」。下一章我们会继续聊高阶特征工程的下半部分,包括谱分析、小波变换这些更高级的工具。到时候见。