第一章:数据获取与清洗——Level2行情数据解析、Tick级数据清洗、数据对齐与重采样、缺失值处理实战
做量化交易这些年,我踩过最大的坑,就是数据问题。你策略再牛,模型再炫,数据是脏的,结果全是扯淡。尤其是高频策略,Level2行情里藏着太多细节,处理不好,回测曲线漂亮得像假的,实盘一跑就崩。
今天咱们就聊聊数据获取与清洗。说白了,就是怎么把交易所吐出来的原始数据,变成能喂给策略的干净食材。我习惯把这一步叫「厨房准备工作」——菜不洗好,大厨来了也没辙。
核心观点:数据清洗占量化研究60%以上的时间。别嫌烦,这是基本功。
1.1 Level2行情数据解析
先说说Level2行情。很多新手以为Level2就是「比Level1多几档买卖盘」,其实远不止这么简单。Level2包含了逐笔成交、委托队列、快照数据等,信息量是Level1的几十倍。
我在项目中遇到过最典型的问题:有人拿Level2的快照数据直接算订单流不平衡,结果发现和逐笔成交对不上。为什么?因为快照是「某一瞬间的切片」,而逐笔成交是「连续的事件流」,两者天然有时序差异。
解析Level2数据,我一般分三步走:
- 原始二进制解析——交易所给的通常是二进制流,需要按协议拆字段
- 字段映射——把二进制位映射成可读的字段名,比如price、volume、side
- 时间戳校准——交易所时间戳精度到微秒,但网络传输有延迟,需要做对齐
举个简单的解析示例(伪代码风格):
# Level2 逐笔成交解析示例
def parse_trade_tick(raw_bytes):
# 假设协议:前8字节时间戳,后4字节价格,后4字节成交量
timestamp = struct.unpack('>Q', raw_bytes[0:8])[0]
price = struct.unpack('>I', raw_bytes[8:12])[0] / 10000.0
volume = struct.unpack('>I', raw_bytes[12:16])[0]
return {'ts': timestamp, 'price': price, 'volume': volume}
小技巧:解析时别直接用float存价格,用整数存(比如乘以10000),算完再转浮点。精度损失少,速度还快。
1.2 Tick级数据清洗
Tick级数据,就是每一笔交易或每一次报价变动。这个级别的数据,脏起来能让你怀疑人生。
我见过最离谱的:某只股票在14:59:59.999突然出现一笔成交价是0.01元,明显是交易所测试数据没清干净。还有更隐蔽的——价格不变但成交量异常大,可能是拆单合并的问题。
清洗Tick数据,我总结了一套「三筛法」:
- 第一筛:价格合理性——剔除涨跌停板外的异常价格,比如价格突变超过N个标准差
- 第二筛:时间连续性——检查时间戳是否单调递增,有没有跳变或回退
- 第三筛:成交量合理性——单笔成交量不能超过该股票流通盘的某个比例(比如0.1%)
嗯,这里要注意:涨跌停板期间的数据不是脏数据,是正常数据。别一刀切删了。
def clean_tick_data(df):
# 第一筛:价格异常
price_mean = df['price'].mean()
price_std = df['price'].std()
df = df[(df['price'] > price_mean - 5*price_std) &
(df['price'] < price_mean + 5*price_std)]
# 第二筛:时间单调性
df = df.sort_values('timestamp')
df = df[df['timestamp'].diff() >= 0]
# 第三筛:成交量异常
df = df[df['volume'] < df['volume'].quantile(0.999)]
return df
警告:千万不要在清洗前做任何聚合操作。先清洗,再重采样。顺序错了,脏数据会被「洗白」到聚合数据里,查都查不出来。
1.3 数据对齐与重采样
不同数据源的时间戳精度不一样,有的精确到秒,有的精确到微秒。你想想看,如果策略里同时用逐笔成交和Level2快照,时间戳对不上,算出来的指标全是错的。
数据对齐,说白了就是让不同频率的数据「对上点」。我常用的方法有两种:
| 对齐方式 | 适用场景 | 注意事项 |
|---|---|---|
| 向前对齐(backward fill) | 低频数据对齐到高频时间轴 | 会引入未来信息,回测时慎用 |
| 向后对齐(forward fill) | 高频数据对齐到低频时间轴 | 最常用,不会引入未来信息 |
| 线性插值 | 价格类数据,变化平滑 | 成交量不要用插值 |
重采样就更常见了。把Tick级数据聚合成1分钟、5分钟K线,是每个量化人的日常。但这里有个坑:用收盘价还是用加权平均价?
我个人习惯:做趋势策略用收盘价,做做市策略用VWAP(成交量加权平均价)。为什么?趋势策略看的是方向,收盘价代表最终博弈结果;做市策略看的是成本,VWAP更贴近实际成交。
# 1分钟K线重采样
def resample_to_minute(df):
df['minute'] = df['timestamp'].dt.floor('1min')
ohlc = df.groupby('minute').agg({
'price': 'first', # 开盘价
'high': 'max', # 最高价
'low': 'min', # 最低价
'price': 'last', # 收盘价
'volume': 'sum' # 成交量
})
return ohlc
避坑指南:我曾经在重采样时忘了处理隔夜数据,结果每天开盘第一根K线包含了前一天的最后一笔成交。那段时间回测收益高得离谱,后来才发现是数据泄露。记住:每天收盘后要重置时间窗口。
1.4 缺失值处理实战
缺失值,是数据清洗里最烦人的事。Tick级数据缺失的原因五花八门:网络丢包、交易所故障、数据商抽风……
处理缺失值,我分三种情况:
- 连续缺失少于3个Tick——用线性插值,简单有效
- 连续缺失3-10个Tick——用前一个有效值填充,或者用同一天的均值
- 连续缺失超过10个Tick——直接标记为缺失区间,策略里跳过这段时间
为什么超过10个就不建议填充了?因为市场在10个Tick里可能发生了重大变化,你填进去的值全是猜测,不如不填。
def handle_missing_ticks(df, max_fill=10):
# 标记连续缺失区间
df['missing_group'] = df['price'].isna().cumsum()
# 短缺失:插值
df['price'] = df['price'].interpolate(limit=max_fill)
# 长缺失:标记
long_missing = df.groupby('missing_group').size() > max_fill
df.loc[df['missing_group'].isin(long_missing[long_missing].index), 'price'] = np.nan
return df
注意:缺失值处理一定要记录日志。我每次清洗完数据都会生成一份缺失报告,写明哪些时间段、哪些股票、缺失了多少数据。这样回测出问题时,能快速定位是数据问题还是策略问题。
知识体系总览
下面这张图,是我做数据清洗时的思维框架。每次拿到新数据,我都会按这个流程走一遍:
这张图把整个流程串起来了。从原始数据源开始,经过解析、清洗、对齐重采样,最后处理缺失值。每一步都有对应的技术要点,缺一不可。
最后说一句:数据清洗没有银弹。不同市场、不同品种、不同数据商,脏数据的「脏法」都不一样。多积累经验,多写清洗日志,慢慢就能练出「数据洁癖」——看到脏数据就浑身难受,那说明你入门了。