第三章:数据准备与清洗——获取历史行情数据,处理缺失值与异常值
数据准备,说白了就是量化交易的「地基」。
我见过太多人,策略逻辑写得天花乱坠,结果一跑回测,亏得底裤都不剩。为什么?数据有问题。你想想看,地基没打好,楼盖得再漂亮也是危房。今天我们就来聊聊,怎么把历史行情数据这块地基夯实。
3.1 数据来源与获取
做套利策略,数据源的选择很关键。我个人习惯用这几种:
- 交易所官方API:比如Binance、OKX的REST API。数据最原始,但需要自己处理限频。
- 第三方数据平台:像QuantConnect、聚宽。封装好了,拿来就用,但可能有延迟。
- 本地CSV/Parquet文件:我经常把历史数据存成Parquet格式,读取快,省空间。
嗯,这里要注意:千万别直接用免费的聚合数据源。我在项目中遇到过,某平台提供的BTC/USDT 1分钟K线,居然有30%的缺失。你拿这种数据做套利,结果可想而知。
核心原则:数据源越原始越好,宁可自己清洗,也别用别人洗过的「二手货」。
3.2 数据清洗流程
拿到原始数据后,第一步不是分析,是清洗。我一般按这个流程走:
- 格式统一:时间戳转成UTC,价格统一成浮点数。
- 去重:同一时间戳出现多条记录,保留第一条或最后一条。
- 排序:按时间升序排列,这是必须的。
- 缺失值处理:这个我们重点讲。
- 异常值检测:这个也是重点。
为什么会这样排序?因为不先去重就排序,你会得到一堆乱序数据。我刚开始做的时候吃过这个亏,排序后发现时间戳居然倒着走,排查了半天才发现是去重没做好。
3.3 缺失值处理
缺失值在历史行情里太常见了。交易所宕机、网络波动、数据源抽风,都可能导致某根K线消失。
处理缺失值,我常用的方法有这几种:
| 方法 | 适用场景 | 缺点 |
|---|---|---|
| 前向填充(ffill) | 短时间缺失(1-2根K线) | 会引入滞后 |
| 线性插值 | 价格走势平稳时 | 剧烈波动时不准 |
| 删除缺失行 | 缺失比例小于1% | 会破坏时间连续性 |
| 模型预测填充 | 缺失比例较大时 | 计算成本高 |
我个人最常用的是前向填充。为什么?因为套利策略对实时性要求高,用前向填充至少保证了「最新可用数据」。我曾经试过线性插值,结果在闪崩行情里插出了负价格,你说离谱不离谱?
小技巧:处理缺失值时,建议先统计缺失比例。如果某只币种缺失超过5%,我建议直接放弃这个品种。不值得为它浪费时间。
3.4 异常值检测
异常值比缺失值更隐蔽。你看着数据挺完整,但里面可能藏着「炸弹」。
常见的异常值类型:
- 价格跳变:比如上一秒100,下一秒10000。这通常是数据录入错误。
- 成交量异常:突然暴增100倍,可能是交易所数据推送bug。
- 买卖价差为负:买一价高于卖一价,这在正常市场里不可能出现。
检测方法,我推荐用Z-score:
import numpy as np
import pandas as pd
def detect_outliers_zscore(data, threshold=3):
z_scores = np.abs((data - data.mean()) / data.std())
return z_scores > threshold
# 示例:检测价格异常
df['price_outlier'] = detect_outliers_zscore(df['close'])
outliers = df[df['price_outlier'] == True]
print(f"发现 {len(outliers)} 个异常价格点")
嗯,这里要注意:Z-score的阈值不要设得太死。3倍标准差是经典值,但我在处理BTC数据时,经常调到4倍。因为BTC波动大,3倍会把很多正常波动误判为异常。
避坑指南:我曾经用Z-score检测ETH的异常值,阈值设成2.5,结果把2017年牛市的所有上涨都标记成了异常。你想想看,那还做什么策略?直接空仓得了。所以阈值一定要根据品种特性来调。
3.5 实战:完整的数据清洗流水线
说了这么多,我们来写一个完整的清洗函数。这是我实际项目中用的版本,简化了一下:
def clean_ohlcv_data(df):
"""
清洗OHLCV数据
"""
# 1. 去重
df = df.drop_duplicates(subset='timestamp')
# 2. 排序
df = df.sort_values('timestamp').reset_index(drop=True)
# 3. 缺失值处理(前向填充)
df = df.fillna(method='ffill')
# 4. 异常值检测(价格)
for col in ['open', 'high', 'low', 'close']:
z_scores = np.abs((df[col] - df[col].mean()) / df[col].std())
df.loc[z_scores > 4, col] = np.nan
# 5. 再次填充被标记为异常的值
df = df.fillna(method='ffill')
# 6. 检查买卖价差
if 'ask' in df.columns and 'bid' in df.columns:
df['spread'] = df['ask'] - df['bid']
df = df[df['spread'] >= 0]
return df
这个函数看起来简单,但我在项目里跑了两年,基本没出过问题。唯一一次翻车,是某个小交易所的数据里出现了负成交量,我忘了加成交量检查。从那以后,我就在函数里加了一行 df = df[df['volume'] > 0]。
3.6 数据质量评估
清洗完数据,别急着跑策略。先评估一下数据质量。我一般看这几个指标:
- 完整率:实际数据量 / 理论数据量。比如1分钟K线,一天应该有1440根,如果只有1400根,完整率就是97.2%。
- 异常比例:被标记为异常的数据占比。超过2%就要警惕。
- 时间连续性:相邻时间戳的间隔是否一致。如果出现间隔突然变大,说明有数据缺失。
你想想看,如果完整率只有90%,你拿这个数据做套利,回测结果能信吗?反正我是不敢信。
我的经验:数据质量评估应该做成自动化脚本,每次获取新数据后自动跑一遍。别手动检查,人眼看不出来的。我吃过这个亏,手动检查了三天,结果漏了一个时间戳错位的问题,导致回测结果完全失真。
3.7 知识体系总览
下面这张图,是我对数据准备与清洗这个环节的总结。你可以把它当成一个检查清单:
这张图把整个流程串起来了。你从数据获取开始,经过清洗,最后评估质量。每一步都不能跳过。我见过有人直接跳过清洗,拿原始数据跑回测,结果策略收益高得离谱——后来发现是数据里有个价格少了个小数点,把100块变成了10块,买入信号满天飞。
最后一句:数据清洗这件事,做一次不难,难的是每次获取新数据都做。我建议你把清洗流程写成脚本,每次跑数据前自动执行。别偷懒,偷懒的代价是回测结果不可信,策略上线就亏钱。