3. 数据预处理与滚动切片:时间序列数据的滚动切片实现、避免未来函数、数据对齐与重采样
做量化策略,最怕什么?
我个人最怕的就是——回测跑得漂亮,实盘一塌糊涂。后来我发现,十有八九的问题出在数据预处理上。说白了,数据没处理好,后面的一切都是空中楼阁。
这一章,我们就来聊聊数据预处理和滚动切片。嗯,这两个东西是滚动优化的地基。地基不稳,楼盖得再高也得塌。
3.1 滚动切片:时间序列的“滑动窗口”
滚动优化,核心就是“滚动”二字。什么意思?就是用一个固定大小的窗口,在时间轴上一步一步往前滑。
举个例子。假设我们有2010年到2023年的日线数据。我习惯用3年作为训练窗口,1年作为验证窗口。那么第一次切片:训练集是2010-2012年,验证集是2013年。第二次切片:训练集是2011-2013年,验证集是2014年。以此类推。
核心概念:滚动切片 = 固定窗口 + 固定步长 + 时间顺序
你想想看,这跟机器学习里的交叉验证很像,但有一个本质区别——时间序列不能打乱顺序。你不能把2015年的数据拿到2010年去训练,那叫穿越。
下面是我在项目中常用的切片实现方式:
import pandas as pd
import numpy as np
from typing import List, Tuple
def rolling_window_slice(
df: pd.DataFrame,
train_window: int = 252 * 3, # 3年交易日
val_window: int = 252, # 1年交易日
step: int = 252 # 每年滚动一次
) -> List[Tuple[pd.DataFrame, pd.DataFrame]]:
"""
滚动切片实现
"""
slices = []
total_len = len(df)
start = 0
while start + train_window + val_window <= total_len:
train_end = start + train_window
val_end = train_end + val_window
train_slice = df.iloc[start:train_end].copy()
val_slice = df.iloc[train_end:val_end].copy()
slices.append((train_slice, val_slice))
start += step
return slices
# 使用示例
# slices = rolling_window_slice(price_data)
# for i, (train, val) in enumerate(slices):
# print(f"窗口 {i+1}: 训练 {train.index[0]} ~ {train.index[-1]}, 验证 {val.index[0]} ~ {val.index[-1]}")
这段代码看起来简单,但我曾经踩过一个坑——窗口边界没对齐。有一次我用的是分钟数据,窗口大小设成了2520(10天),结果因为节假日数据缺失,实际窗口只有不到8天。从那以后,我每次切片都会先检查一下实际数据量。
小技巧:切片前先打印一下每个窗口的起止日期和样本量,肉眼扫一遍。虽然土,但管用。
3.2 避免未来函数:量化回测的“生死线”
未来函数,说白了就是你在回测时用到了“未来”的数据。比如你用今天的收盘价去预测今天的涨跌,或者用整个月的数据去计算这个月的均值。
为什么会这样?很多时候是无意的。我记得有一次,我在计算移动平均线时,用了整个序列的均值,结果回测收益高得离谱。后来一查,原来是pandas的rolling().mean()默认是中心窗口,包含了未来数据。
警告:未来函数是回测中最隐蔽、最致命的错误。它会让你的策略看起来完美无缺,实盘却一败涂地。
避免未来函数,我总结了三条铁律:
- 计算指标时,只用历史数据。比如计算20日均线,只能用前20天的数据,不能用当天或未来的。
- 滚动切片时,确保切片边界清晰。训练集和验证集不能有重叠。
- 重采样时,注意数据对齐。别把未来的信息带进当前时刻。
来看一个常见的错误示例:
# ❌ 错误:使用了未来数据
df['ma_20'] = df['close'].rolling(window=20, center=True).mean()
# ✅ 正确:只使用历史数据
df['ma_20'] = df['close'].rolling(window=20, center=False).mean()
嗯,这里要注意,center=True会让窗口以当前点为中心,前后各取10天。这在回测里就是典型的未来函数。我刚开始做量化时,这个坑踩了不下三次。
3.3 数据对齐与重采样:让不同频率的数据“说同一种语言”
实际项目中,数据来源五花八门。日线数据、分钟数据、财报数据、宏观经济数据……频率不一样,怎么对齐?
我的做法是:统一降频到最低频率。比如日线策略,就把所有数据都对齐到日线级别。
重采样时,有几点要注意:
- 价格数据:用收盘价,或者用最后一条记录
- 成交量数据:用总和
- 财务数据:用最新值(前向填充)
- 宏观经济数据:用插值或前向填充
下面是我常用的重采样函数:
def resample_to_daily(df: pd.DataFrame, freq: str = 'D') -> pd.DataFrame:
"""
将任意频率数据重采样到日线
"""
# 价格类用最后一条
price_cols = ['open', 'high', 'low', 'close']
price_resampled = df[price_cols].resample(freq).last()
# 成交量用总和
volume_resampled = df['volume'].resample(freq).sum()
# 其他指标用前向填充
other_cols = [c for c in df.columns if c not in price_cols + ['volume']]
other_resampled = df[other_cols].resample(freq).ffill()
# 合并
result = pd.concat([price_resampled, volume_resampled, other_resampled], axis=1)
return result
数据对齐还有一个容易忽略的点——时区问题。我曾经处理过美股和A股的数据,一个用美东时间,一个用北京时间。如果不做时区转换,直接对齐,那结果就是牛头不对马嘴。
建议:所有数据统一使用UTC时间,或者统一使用交易所本地时间。别混用。
3.4 本章知识体系
下面这张图,是我自己总结的滚动切片与数据预处理的核心流程。你看一眼,应该就能明白整个逻辑。
这张图里,最关键的其实是第二步——数据预处理。很多人一上来就切片,结果数据没对齐,未来函数没去掉,后面全白干。我建议你,每次做滚动优化前,先花30分钟把数据检查一遍。
3.5 实战中的避坑指南
最后,分享几个我踩过的坑,希望能帮你少走弯路:
- 坑一:切片时用了
df.sample()随机抽样。时间序列不能随机抽样,必须按时间顺序切。 - 坑二:重采样时用了
interpolate()插值。插值会引入未来信息,除非你用的是前向插值。 - 坑三:数据对齐时忽略了节假日。A股和美股节假日不同,直接对齐会导致数据缺失或错位。
我曾经有一个策略,回测年化收益30%,实盘只有5%。查了三天,最后发现是数据对齐时,把美股的非交易日数据用前向填充填上了,导致信号滞后了一天。嗯,从那以后,我对数据对齐这件事就格外小心了。
好了,这一章的内容就到这里。数据预处理和滚动切片,是滚动优化的基本功。练好了,后面的路就好走了。