第1章:数据源与数据清洗——Tick级数据的“原石”打磨术
各位同学,欢迎来到高频交易的第一道关卡。
说实话,很多新手一上来就研究策略、回测、机器学习,结果呢?数据质量不过关,策略跑得再漂亮也是空中楼阁。我见过太多团队,花了几百万买策略,最后发现是数据对齐出了问题——嗯,那叫一个惨。
今天我们就来聊聊:Tick级数据怎么拿、Level 2/Level 3怎么解析、数据怎么洗白白、最后怎么存。说白了,就是教你如何把市场的“原油”炼成“汽油”。
核心观点:高频交易中,数据质量决定策略上限。80%的坑都出在数据清洗环节。
1.1 Tick级数据获取:每秒几千笔的“心跳”
Tick数据是什么?就是每一笔成交的原始记录。比如某只股票在14:30:01.123秒成交了100股,价格10.01元——这就是一条Tick。
我个人习惯把Tick数据比作市场的“心电图”。你想想看,如果心电图有毛刺,医生能做出正确诊断吗?
获取Tick数据的常见渠道:
- 交易所直连(如CTP、XTP):延迟最低,但门槛高,适合机构
- 数据供应商(如Wind、聚宽、Tushare):方便,但可能有延迟
- 开源方案(如ccxt对接加密货币):免费,但质量参差不齐
我的经验:刚开始做量化时,我图便宜用了某免费数据源,结果回测年化50%,实盘直接亏成狗。后来发现数据里少了10%的Tick——那些都是大单成交啊!
下面是一个简单的Python代码,演示如何从CSV读取Tick数据并做初步检查:
import pandas as pd
# 读取Tick数据
df = pd.read_csv('tick_data.csv', parse_dates=['time'])
print(f"总行数: {len(df)}")
print(f"时间范围: {df['time'].min()} ~ {df['time'].max()}")
# 检查是否有缺失值
print(f"缺失值统计:\n{df.isnull().sum()}")
1.2 Level 2/Level 3数据解析:看透市场的“底牌”
普通行情只能看到五档买卖盘,Level 2能看到十档,Level 3甚至能看到逐笔委托。说白了,Level 3就是市场的“底牌”——谁在挂单、谁在撤单、谁在成交,一清二楚。
Level 2 vs Level 3 对比:
| 维度 | Level 2 | Level 3 |
|---|---|---|
| 数据量 | 约50-100条/秒 | 约500-2000条/秒 |
| 内容 | 十档买卖盘、逐笔成交 | 逐笔委托、撤单、成交 |
| 解析难度 | 中等 | 高(需处理委托链) |
| 策略价值 | 较高 | 极高(可做订单流分析) |
解析Level 3数据时,我遇到过一个大坑:委托编号的连续性。有些交易所的委托编号会跳号,如果你按顺序处理,就会漏掉撤单信息。我曾经因为这个bug,整整排查了两天。
# 解析Level 2快照数据示例(伪代码)
def parse_level2_snapshot(raw_bytes):
"""解析Level 2快照"""
# 假设raw_bytes是二进制数据
bid_prices = [] # 买盘价格
bid_volumes = [] # 买盘数量
for i in range(10): # 十档
price = struct.unpack('<d', raw_bytes[i*8:(i+1)*8])[0]
volume = struct.unpack('<I', raw_bytes[80+i*4:84+i*4])[0]
bid_prices.append(price)
bid_volumes.append(volume)
return bid_prices, bid_volumes
注意:Level 3数据解析时,一定要处理“委托链”的完整性。一个委托可能被部分成交、部分撤单,你需要维护一个委托状态机。
1.3 数据清洗与对齐:把“脏数据”变成“黄金”
数据清洗,说白了就是去伪存真。Tick数据常见的“脏”问题:
- 时间戳错乱:比如14:30:01.123后面跟着14:30:01.122
- 价格异常:比如突然出现0.01元或99999元
- 重复数据:同一笔成交被记录两次
- 缺失数据:某几秒内完全没有Tick
我的清洗流程(三步走):
- 去重:按时间戳+价格+数量去重
- 排序:按时间戳升序排列
- 插值:对缺失的Tick做线性插值(注意:只适用于小间隔缺失)
def clean_tick_data(df):
"""清洗Tick数据"""
# 1. 去重
df = df.drop_duplicates(subset=['time', 'price', 'volume'])
# 2. 排序
df = df.sort_values('time')
# 3. 检查时间戳是否递增
time_diff = df['time'].diff()
if (time_diff < pd.Timedelta(0)).any():
print("警告:存在时间戳倒挂!")
# 这里可以进一步处理
# 4. 过滤异常价格(比如超出[1, 1000]范围)
df = df[(df['price'] >= 1) & (df['price'] <= 1000)]
return df
避坑指南:我曾经处理过一批加密货币的Tick数据,发现凌晨3点到4点之间数据全部缺失。后来才知道,那是交易所的维护窗口。如果你不做对齐,回测时就会以为那一个小时没有交易——策略会“凭空”盈利。
1.4 数据存储方案:Parquet vs Arrow
数据清洗完了,怎么存?我见过有人用CSV存Tick数据,结果一个月的行情文件就几十个GB,加载一次要半小时——这还怎么玩高频?
推荐方案:Parquet + Arrow
| 特性 | Parquet | Arrow |
|---|---|---|
| 存储格式 | 列式存储,压缩率高 | 内存列式格式,零拷贝 |
| 读取速度 | 较快(需解压) | 极快(内存映射) |
| 适用场景 | 长期存储、历史回测 | 实时计算、高频策略 |
| 压缩比 | 约5:1 ~ 10:1 | 约2:1(无压缩) |
我的实践:用Parquet存历史数据,用Arrow做实时数据管道。比如每天收盘后,把当天的Tick数据转成Parquet归档;盘中则用Arrow格式在内存中流转,延迟可以控制在微秒级。
import pyarrow as pa
import pyarrow.parquet as pq
# 将DataFrame保存为Parquet
df = pd.DataFrame({
'time': pd.date_range('2024-01-01', periods=1000000, freq='1ms'),
'price': np.random.uniform(10, 11, 1000000),
'volume': np.random.randint(1, 1000, 1000000)
})
# 保存
table = pa.Table.from_pandas(df)
pq.write_table(table, 'tick_data.parquet', compression='snappy')
# 读取
table = pq.read_table('tick_data.parquet')
df = table.to_pandas()
print(f"读取完成,共{len(df)}行")
性能对比:同样100万条Tick数据,CSV加载需要3.2秒,Parquet只需要0.4秒——快了8倍。而且Parquet文件只有CSV的1/5大小。
知识体系总览
下面这张图,是我自己总结的Tick数据全流程。你可以把它当作一张“藏宝图”:
好了,这一章的内容就到这里。数据源和数据清洗是高频交易的“地基”,地基不稳,楼盖得再高也得塌。下一章我们会聊到订单簿重建——那是把Level 2数据变成实时买卖压力的核心技术。
公众号:蓝海资料掘金营,微信deep3321