4、特征工程入门:基础统计特征与滚动窗口计算

说实话,很多做量化的人一上来就搞深度学习、LSTM,结果数据都没洗干净,特征也没做扎实。我个人的经验是——基础统计特征才是高频策略的基石。你想想看,一个简单的均值突破策略,如果连均值的计算方式都没选对,那后面再复杂的模型也是白搭。

这一章,我们就来聊聊那些最基础、但最常用的统计特征。我会结合自己在实盘中的踩坑经历,帮你把这些概念真正落地。

4.1 基础统计特征:均值、方差、偏度、峰度

先问个问题:为什么高频数据需要这些统计量?

因为价格序列不是白噪声。它有趋势、有波动、有尖峰厚尾。这些统计量就是用来描述这些特性的。

4.1.1 均值(Mean)

均值是最简单的特征,但也是最容易被误用的。我在做股指期货高频策略时,曾经直接用简单移动平均(SMA)作为信号,结果回测漂亮,实盘一跑就崩。为什么?因为高频数据里经常有跳空和异常值,一个极端价格就能把均值拉偏。

所以,我建议在高频场景下,优先考虑截尾均值中位数。比如去掉最高最低5%的数据再算平均,这样更稳健。

import numpy as np
import pandas as pd

# 生成模拟高频数据
np.random.seed(42)
price = pd.Series(np.random.randn(1000) * 0.5 + 100)

# 普通均值
mean_std = price.mean()

# 截尾均值(去掉两端5%)
from scipy import stats
mean_trimmed = stats.trim_mean(price, proportiontocut=0.05)

print(f"普通均值: {mean_std:.4f}")
print(f"截尾均值: {mean_trimmed:.4f}")
我的小技巧: 在计算均值前,先做一次简单的异常值过滤。比如用3倍标准差法剔除极端值,这样算出来的均值更贴近真实市场状态。

4.1.2 方差与标准差(Variance & Std)

方差衡量的是价格波动的剧烈程度。在高频交易里,方差就是风险,也是机会。

我记得有一次做螺纹钢的夜盘策略,发现白天和晚上的方差差异巨大。白天波动小,晚上波动大。如果用一个固定的阈值去判断突破,白天容易频繁假突破,晚上又容易错过真行情。后来我改用滚动方差来动态调整阈值,效果好了不少。

# 滚动标准差计算
window = 20
rolling_std = price.rolling(window=window).std()

# 动态阈值:均值 ± 2倍滚动标准差
upper = price.rolling(window=window).mean() + 2 * rolling_std
lower = price.rolling(window=window).mean() - 2 * rolling_std
注意: 高频数据的方差往往存在明显的日内季节性。比如开盘和收盘时方差大,午盘时方差小。如果你直接用全天的方差,可能会误判。建议按时间段分别计算。

4.1.3 偏度(Skewness)

偏度描述的是数据分布的不对称性。正偏度意味着右尾更长,负偏度意味着左尾更长。

在期货市场里,偏度能告诉你什么呢?举个例子,如果某品种的收益率序列呈现明显的负偏度,说明它更容易出现大跌(左尾风险)。这时候你再做多,就得小心了。

我曾经在橡胶期货上吃过这个亏。当时只看均值和方差,觉得价格合理,结果一个负偏度事件直接把我止损打穿。后来我养成了一个习惯:每次建仓前,先看一眼过去100笔tick数据的偏度

from scipy.stats import skew

# 计算偏度
skewness = skew(price)
print(f"偏度: {skewness:.4f}")

# 如果偏度绝对值大于0.5,说明分布明显不对称
if abs(skewness) > 0.5:
    print("注意:分布存在明显偏斜!")

4.1.4 峰度(Kurtosis)

峰度衡量的是数据分布的尖峭程度。高峰度意味着数据有更多的极端值(厚尾),低峰度意味着分布更平坦。

金融数据几乎都是高峰度的。这就是所谓的「尖峰厚尾」特征。我见过很多新手用正态分布去建模,结果回测时觉得参数很漂亮,实盘一遇到极端行情就爆仓。说白了,就是没考虑峰度的影响。

from scipy.stats import kurtosis

# 计算峰度(默认是超额峰度,即正态分布为0)
kurt = kurtosis(price)
print(f"超额峰度: {kurt:.4f}")

# 如果峰度大于3,说明尾部风险较大
if kurt > 3:
    print("警告:尾部风险较高,建议使用稳健统计方法!")

4.2 滚动窗口计算

刚才我们一直在说「滚动窗口」,这其实是高频特征工程里最核心的操作之一。为什么?因为市场是动态的,昨天的数据对今天的意义不大,我们需要一个滑动的、局部的视角。

4.2.1 滚动窗口的基本用法

Pandas的rolling()函数是主力工具。但要注意,窗口大小的选择非常关键。窗口太小,噪声大;窗口太大,反应迟钝。

我个人习惯的做法是:先根据品种的微观结构来定。比如股指期货的tick数据,我一般用20笔(约1秒)作为最小窗口;如果是1分钟线,窗口就设在5-20之间。

# 滚动窗口计算多个统计量
window = 20

features = pd.DataFrame({
    'price': price,
    'rolling_mean': price.rolling(window).mean(),
    'rolling_std': price.rolling(window).std(),
    'rolling_skew': price.rolling(window).skew(),
    'rolling_kurt': price.rolling(window).kurt()
})

# 前window-1个值为NaN,需要处理
features = features.dropna()
核心要点: 滚动窗口计算时,前N-1个数据点会变成NaN。实盘中一定要处理好这些缺失值,否则策略会报错。我一般用min_periods参数来设置最小有效数据量。

4.2.2 滚动窗口的实战技巧

这里分享一个我踩过的坑。有一次我在做铁矿石的跨期套利,用了滚动窗口计算价差的均值和标准差。结果发现,每次换月的时候,价差会出现巨大跳变,导致滚动窗口里的统计量全部失效。

后来我加了一个换月检测逻辑:在换月前后,重置滚动窗口,或者使用更长的窗口来平滑过渡。嗯,这个细节虽然小,但直接影响策略的稳定性。

# 处理换月时的滚动窗口重置
def rolling_with_reset(series, window, reset_indices):
    """
    reset_indices: 需要重置窗口的位置列表(如换月时间点)
    """
    result = series.rolling(window).mean()
    for idx in reset_indices:
        # 在重置点之后,重新开始计算
        result.iloc[idx:idx+window] = np.nan
    return result

4.3 时间衰减权重

滚动窗口有个问题:窗口内的所有数据权重相同。但现实中,越近的数据越重要。比如5秒前的价格比5分钟前的价格更有参考价值。

这就是时间衰减权重的意义。

4.3.1 指数加权移动平均(EWMA)

EWMA是最常用的时间衰减方法。它给近期的数据更高的权重,远期的数据权重指数级下降。

我记得在2018年做原油期货时,用SMA做趋势跟踪总是滞后,换成EWMA后信号提前了约3-5个tick。别小看这几个tick,在高频交易里,这就是盈亏的分水岭。

# 指数加权移动平均
span = 20  # 相当于窗口大小
ewma = price.ewm(span=span, adjust=False).mean()

# 指数加权标准差
ewm_std = price.ewm(span=span, adjust=False).std()

# 对比SMA和EWMA
sma = price.rolling(window=span).mean()
print(f"SMA最后值: {sma.iloc[-1]:.4f}")
print(f"EWMA最后值: {ewma.iloc[-1]:.4f}")
参数选择建议: span参数相当于窗口大小,alpha=2/(span+1)是衰减因子。我一般用span=20或span=50,具体取决于品种的微观结构。如果你想要更快的衰减,可以减小span。

4.3.2 自定义时间衰减函数

有时候EWMA的指数衰减不一定适合所有场景。比如在某些高频策略里,你可能希望前10笔数据权重快速下降,之后保持一个平稳的衰减速度。这时候就需要自定义权重函数。

# 自定义线性衰减权重
def linear_decay_weights(window):
    """线性衰减:越近权重越大"""
    weights = np.arange(1, window+1, dtype=float)
    return weights / weights.sum()

# 自定义指数衰减权重
def exp_decay_weights(window, alpha=0.1):
    """指数衰减"""
    weights = np.exp(-alpha * np.arange(window))
    return weights / weights.sum()

# 应用自定义权重
window = 20
weights = linear_decay_weights(window)
weighted_mean = price.rolling(window).apply(
    lambda x: np.dot(x, weights), raw=True
)

4.4 本章知识体系

说了这么多,我们来梳理一下。下面这张图展示了本章的核心逻辑:

特征工程入门:知识体系 基础统计特征 均值 (Mean) 截尾均值、中位数 方差/标准差 波动率、动态阈值 偏度 (Skewness) 分布不对称性 两种核心计算方法 滚动窗口计算 等权重、滑动视角 窗口大小选择、换月处理 时间衰减权重 EWMA、自定义权重 近期数据更高权重 目标:构建稳健、动态的高频特征

4.5 实战建议与避坑指南

最后,我总结几条实战中的经验,希望能帮你少走弯路:

  • 不要迷信单一统计量。均值、方差、偏度、峰度要结合起来看。比如均值上升但方差也在扩大,那这个上升可能不可靠。
  • 滚动窗口的大小要动态调整。市场波动大的时候,窗口可以小一点;波动小的时候,窗口可以大一点。我一般用自适应窗口,根据ATR(平均真实波幅)来调整。
  • 时间衰减权重不是万能的。EWMA对突变反应快,但也容易受噪声影响。如果你发现信号太频繁,可以试试双指数平滑(Holt-Winters)。
  • 回测和实盘要统一。我曾经在回测里用了未来数据(不小心用了全量数据的均值),结果实盘亏得很惨。记住:滚动窗口计算时,只能用历史数据

一句话总结: 基础统计特征就像盖房子的地基。地基不牢,再漂亮的策略也是空中楼阁。把均值、方差、偏度、峰度吃透,把滚动窗口和时间衰减用好,你的高频特征工程就成功了一半。


公众号:蓝海资料掘金营,微信deep3321