高频数据基础:Tick数据、Level2数据、数据清洗与对齐
做量化交易这些年,我越来越觉得一个道理:策略的上限,往往取决于数据的下限。你模型再漂亮,喂进去的数据是脏的、乱的、不对齐的,结果就是纸上富贵一场空。
今天咱们聊聊高频数据的基础。说白了,就是Tick数据、Level2数据,以及怎么把它们收拾干净、对齐好。嗯,这部分内容看着基础,但我在项目里踩过的坑,十有八九都跟数据清洗有关。
一、Tick数据:市场最原始的呼吸
Tick数据是什么?就是每一笔成交的记录。每一笔交易,交易所都会吐出一条Tick。它包含:
- 成交时间(精确到毫秒甚至微秒)
- 成交价格
- 成交量
- 成交方向(主动买还是主动卖)
我刚开始做高频策略时,觉得Tick数据就是价格序列嘛,直接拿来用就行。后来发现,Tick数据里藏着很多微观结构的信息,比如买卖压力、订单流不平衡等等。
关键点:Tick数据不是均匀时间间隔的。市场活跃时,一秒钟可能几百笔;冷清时,几秒钟才一笔。这种非均匀性,是后续处理的核心难点。
举个例子,某只股票在09:30:01.123秒成交了一笔,下一笔可能在09:30:01.456秒,再下一笔可能在09:30:02.001秒。时间间隔完全随机。
二、Level2数据:看得更深一点
Level2数据,也叫深度行情数据。它比Tick数据多了一层信息——订单簿的逐笔委托。
说白了,Tick告诉你「谁成交了」,Level2告诉你「谁在排队等着成交」。
Level2数据通常包含:
- 十档买卖盘口:买一到买十的价格和数量,卖一到卖十的价格和数量
- 逐笔委托:每一笔挂单、撤单的明细
- 成交明细:比Tick更细,有时会包含订单编号
我个人习惯把Level2数据看作市场的「心电图」。Tick是心跳,Level2是心电波形——能看到心跳是怎么形成的。
小技巧:如果你只有Tick数据,可以尝试用「成交量加权价格」来近似Level2的买卖压力。但精度嘛,差不少。有条件还是直接上Level2。
三、数据清洗:脏数据是策略的毒药
这里我要重点说说。数据清洗听着枯燥,但我在项目中遇到的最大回撤,十有八九是数据问题导致的。
高频数据常见的脏数据类型:
- 时间戳异常:比如某条Tick的时间戳比前一条还早,或者时间戳跨天了
- 价格异常:价格突然跳到离谱的值,比如正常10块钱的股票,突然出现1000块的成交
- 重复数据:同一笔成交被推送了两次
- 缺失数据:某段时间内完全没有Tick,但市场明明在交易
我曾经遇到过一个坑:某数据商的Tick数据,在每天收盘前最后一分钟,会重复推送最后一条Tick。我用了三个月才发现,回测业绩虚高了5%。嗯,从那以后,我养成了对数据做「重复检测」的习惯。
清洗流程我一般这么走:
# 伪代码示例:Tick数据清洗流程
def clean_tick_data(df):
# 1. 排序:按时间戳升序
df = df.sort_values('timestamp')
# 2. 去重:同一毫秒内相同价格和数量的成交,只保留一条
df = df.drop_duplicates(subset=['timestamp', 'price', 'volume'])
# 3. 价格过滤:剔除超过N个标准差的价格
mean_price = df['price'].mean()
std_price = df['price'].std()
df = df[(df['price'] > mean_price - 5*std_price) &
(df['price'] < mean_price + 5*std_price)]
# 4. 时间连续性检查:如果间隔超过阈值,标记为缺失
df['time_gap'] = df['timestamp'].diff()
df['gap_flag'] = df['time_gap'] > pd.Timedelta(seconds=5)
return df
警告:价格过滤的阈值不要设得太死。比如5倍标准差,在极端行情下可能把真实的价格波动也过滤掉了。我一般会结合市场状态动态调整阈值。
四、数据对齐:让不同频率的数据「对上话」
数据对齐,说白了就是把不同时间频率的数据,统一到同一个时间轴上。
比如,你的策略需要同时用到:
- Tick数据(毫秒级)
- Level2快照数据(每秒一次)
- 分钟级K线数据
这三者的时间戳不一样,怎么对齐?
我常用的对齐方法有两种:
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 前向填充 | 低频数据对齐到高频 | 简单、不会引入未来信息 | 可能使用过时的数据 |
| 线性插值 | 连续型指标(如价格) | 平滑、更精确 | 可能引入未来信息(需注意) |
| 时间桶聚合 | 将Tick数据聚合到固定时间窗口 | 标准化、便于计算 | 丢失微观结构信息 |
举个例子,假设你有一个Level2的买卖盘口数据,每100ms更新一次。你的Tick数据是随机时间点的。你想知道「在每一笔Tick成交时,当时的买卖盘口是什么样」。
这时候就用前向填充:找到Tick时间戳之前最近的一条Level2快照,用那个快照的数据。
# 前向填充对齐示例
def align_tick_to_level2(tick_df, level2_df):
# 将Level2的时间戳作为索引
level2_df = level2_df.set_index('timestamp')
# 对Tick的每个时间戳,找到Level2中最近的前一个时间点
tick_df['aligned_bid'] = tick_df['timestamp'].apply(
lambda t: level2_df['bid_price'].asof(t)
)
tick_df['aligned_ask'] = tick_df['timestamp'].apply(
lambda t: level2_df['ask_price'].asof(t)
)
return tick_df
避坑指南:我曾经在回测中用了线性插值来对齐数据,结果发现策略在实盘时表现差很多。为什么?因为线性插值用到了「未来」的数据点,在回测中相当于偷看了未来。所以,回测中尽量用前向填充,别用插值。
五、知识体系总览
说了这么多,咱们用一张图来梳理一下本章的核心逻辑:
这张图把咱们今天讲的内容串起来了。Tick和Level2是原始数据,经过清洗后,再通过不同的对齐方法,变成可供策略使用的标准化数据。
嗯,数据这块儿,说白了就是「垃圾进,垃圾出」。你花再多时间在策略模型上,如果数据基础没打好,一切都是白搭。我个人习惯在每次回测前,先跑一遍数据质量检查脚本,确认没问题了再开始跑策略。这个习惯帮我避免了好几次「假回测」的尴尬。
好了,高频数据的基础就聊到这儿。下一节咱们会深入讲讲订单簿的微观结构,看看那些排队等着成交的订单,到底藏着什么秘密。