3、数据准备:获取历史行情数据、数据清洗、缺失值处理、时间对齐
数据准备这件事,说重要吧,很多人觉得不如调参数刺激;说不重要吧,我见过太多回测跑得漂亮、实盘直接翻车的案例。问题根源往往不在策略逻辑,而是数据入口就脏了。
我个人认为,数据准备占整个回测工作量的 60% 以上。这期内容我就把这块掰开揉碎讲清楚,包含数据获取、清洗、缺失值处理和时间对齐四个环节。每个环节我都会结合自己的实战经验来讲。
3.1 数据获取:选对源,拿对货
要分析行情,首先得有数据。市面上数据源很多,交易所直连、第三方数据商、开源数据集……我一般用交易所的 REST API 拉取现货数据,辅以 CSV 离线备份。
举个最简单的例子,用 Python 从币安的 public API 获取 BTC/USDT 的 1 分钟 K 线:
import requests
import pandas as pd
url = "https://api.binance.com/api/v3/klines"
params = {
"symbol": "BTCUSDT",
"interval": "1m",
"limit": 1000
}
resp = requests.get(url, params=params)
data = resp.json()
columns = [
"open_time", "open", "high", "low", "close", "volume",
"close_time", "quote_vol", "count", "taker_buy_vol",
"taker_buy_quote_vol", "ignore"
]
df = pd.DataFrame(data, columns=columns)
df["open_time"] = pd.to_datetime(df["open_time"], unit="ms")
print(df.head())
拿到原始数据后,第一件事就是检查字段完整性和时间范围。我在项目中遇到过 API 返回的数据漏了几小时,结果回测的入场信号全偏了。
3.2 数据清洗:除掉那些"一眼假"的脏数据
数据清洗到底在洗什么?说白了,就是去掉那些明显不合理的数据点。
常见的脏数据包括:价格为 0 或负数、成交量异常突刺、时间戳重复或倒挂。这些数据如果不处理,策略回测时会引入大量噪音。
我个人习惯用几个简单的规则来清洗:
- 价格范围检查: 当前价格偏离前 N 根 K 线的均值超过 N 个标准差时,标记为异常。
- 成交量突刺过滤: 成交量超过前 20 根均值的 10 倍,大概率是数据错误。
- 时间戳连续性检查: 相邻 K 线的时间差必须在预期间隔内(比如 1 分钟 K 线的时间差应为 60 秒)。
代码实现也很直接:
# 去除价格为 0 或负数的行
df = df[df["close"] > 0]
# 去除成交量异常突刺(超过前20根均值的10倍)
mean_vol = df["volume"].rolling(20).mean()
df = df[df["volume"] < mean_vol * 10]
# 检查时间戳连续性
df["time_diff"] = df["open_time"].diff().dt.total_seconds()
df = df[df["time_diff"] == 60] # 1分钟K线间隔应为60秒
我曾经因为没过滤成交量突刺,导致一个简单均线策略在回测里年化收益虚高 20%。后来排查才发现,是某根 K 线的 volume 字段多了一个 0。嗯,从那以后我对清洗环节再也不敢马虎了。
3.3 缺失值处理:别让空值悄悄坑了你
遇到缺失值怎么办?这是数据准备里最常被问的问题。我的答案是:先搞清楚缺失的原因,再选处理方法。
缺失值的常见原因有三类:
- 停牌或流动性枯竭: 某个时间段没有交易发生,交易所不会产出 K 线。
- 网络或 API 故障: 拉取数据时丢包,导致某些时间点没有记录。
- 数据源对齐丢失: 多数据源拼接时,某个源缺少特定时间段的数据。
下面这个表格总结了常用的处理方法:
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 前向填充(ffill) | 流动性枯竭、停牌 | 保持市场状态连续,实现简单 | 延长了旧数据的有效期 |
| 后向填充(bfill) | 数据拉取延迟导致开头缺失 | 填补开头空档 | 会引入未来信息,回测慎用 |
| 线性插值 | 价格连续、缺失段较短 | 平滑过渡,不引入跳跃 | 不适用于成交量等离散指标 |
| 删除缺失行 | 缺失比例低(<5%) | 保留原始数据分布 | 会破坏时间序列的连续性 |
我个人习惯用前向填充,尤其是在 tick 级别数据里。为什么?因为市场在大部分时间是没有交易的,前向填充最贴合真实的市场状态。
# 前向填充示例
df = df.set_index("open_time").asfreq("1min")
df[["open", "high", "low", "close", "volume"]] = df[["open", "high", "low", "close", "volume"]].ffill()
3.4 时间对齐:让所有数据用同一套时钟
做多品种策略时,不同币种、不同交易所的数据时间戳往往不一致。有的用 UTC,有的用本地时间;有的精确到毫秒,有的只到秒。时间对齐就是把它们统一到同一个时间坐标系里。
我之前在做一个跨交易所套利策略时,A 交易所的时间戳比 B 交易所快了 30 毫秒。一开始没发现,结果回测显示每分钟都能套利,实盘一跑全是滑点亏损。
标准做法分为三步:
- 统一时区: 全部转为 UTC 时间,避免夏令时等问题。
- 统一精度: 全部截断或四舍五入到同一毫秒/秒精度。
- 重采样对齐: 使用 pandas 的 resample 或 asfreq 将数据对齐到固定时间网格。
# 时间对齐示例:统一到 UTC 并按 1 分钟重采样
df["open_time"] = pd.to_datetime(df["open_time"], unit="ms", utc=True)
df = df.set_index("open_time").asfreq("1min")
# 如果多个品种,先分别处理再按 index 合并
df_btc = df_btc.resample("1min").last()
df_eth = df_eth.resample("1min").last()
combined = pd.concat([df_btc["close"], df_eth["close"]], axis=1, keys=["BTC", "ETH"])
3.5 数据质量检查:交付前的最后一道关
清洗、补缺、对齐都做完后,别急着跑回测。我建议做一次最终的数据质量检查,覆盖以下几个维度:
- 完整性检查: 时间序列是否连续,有没有意外的大段缺失。
- 极值检查: 价格是否落在合理范围内,有没有离谱的跳空。
- 统计检查: 均值、标准差、最大最小值和历史统计对比,看有没有明显偏差。
- 可视化检查: 快速画一张价格走势图,肉眼扫一遍。
这些检查看起来简单,但非常有效。我曾经因为数据源切换漏了一周数据,就是通过可视化发现走势图上有个"断崖"才揪出来的。
数据准备这个环节,看起来琐碎,但它是整个回测 pipeline 里最值得花时间打磨的部分。数据干净了,后面的阈值设定、策略回测才能站得住脚。