3、Tick级数据处理:Tick数据格式与存储、数据清洗与对齐、时间同步与时钟偏差处理、回放引擎设计
做高频做市,说白了就是跟时间赛跑。你策略再牛,数据不对齐、时间戳有偏差,结果就是白忙活。今天咱们聊聊Tick级数据怎么处理,这块我踩过的坑不少,希望能帮你少走弯路。
3.1 Tick数据格式与存储
先说说Tick数据长什么样。交易所推过来的原始数据,一般包含这些字段:
| 字段 | 含义 | 示例 |
|---|---|---|
| Symbol | 合约代码 | IF2409 |
| Timestamp | 交易所时间戳(纳秒级) | 1693123456789123456 |
| Price | 成交价 | 3725.6 |
| Volume | 成交量 | 12 |
| BidPrice1~5 | 买一到买五价格 | 3725.4, 3725.2, ... |
| AskPrice1~5 | 卖一到卖五价格 | 3725.8, 3726.0, ... |
| BidVol1~5 | 买一到买五量 | 100, 200, ... |
| AskVol1~5 | 卖一到卖五量 | 150, 180, ... |
存储格式我推荐用两种:
- 二进制格式(.bin):用于实盘回放,速度快、体积小。我习惯用自定义的二进制结构体,每个Tick固定长度,方便随机读取。
- Parquet格式:用于离线分析,压缩率高,支持列式存储。用Pandas读起来也方便。
核心原则:实盘用二进制,研究用Parquet。别混着用,否则你会后悔的。
3.2 数据清洗与对齐
交易所来的数据,其实没那么干净。我遇到过的情况包括:
- 重复Tick:同一个时间戳出现两次相同数据
- 缺失Tick:某几毫秒的数据突然没了
- 异常价格:价格突然跳到天上或地下
- 乱序Tick:后发的数据先到,先发的后到
清洗流程我一般这么搞:
def clean_ticks(ticks):
# 1. 去重:按时间戳+价格+量去重
ticks = ticks.drop_duplicates(subset=['Timestamp', 'Price', 'Volume'])
# 2. 排序:按时间戳升序
ticks = ticks.sort_values('Timestamp')
# 3. 异常值过滤:价格超出3倍标准差就干掉
mean_price = ticks['Price'].mean()
std_price = ticks['Price'].std()
ticks = ticks[(ticks['Price'] > mean_price - 3*std_price) &
(ticks['Price'] < mean_price + 3*std_price)]
# 4. 缺失填充:用前一个Tick的价格填充(别用均值,会引入未来信息)
ticks['Price'] = ticks['Price'].ffill()
return ticks
注意:缺失填充千万别用均值或插值。你想想看,回测时用未来数据填充,实盘时你拿不到未来数据,策略表现会差一大截。我吃过这个亏,后来再也不敢了。
数据对齐这块,主要是把不同来源的数据对齐到同一个时间轴上。比如你同时有交易所的Tick数据和自己的订单簿快照,时间戳可能差几微秒。我的做法是:
- 以交易所时间戳为主轴
- 其他数据按时间戳就近匹配
- 匹配窗口控制在1毫秒以内
3.3 时间同步与时钟偏差处理
这是高频做市里最头疼的问题之一。你的机器时钟和交易所时钟,永远有偏差。哪怕你用PTP(精确时间协议),偏差也可能在微秒级。
时钟偏差的影响有多大?举个例子:
- 你的机器比交易所快1微秒
- 你看到卖一价是3725.8,觉得可以买入
- 实际上交易所的卖一价已经变成3725.6了
- 你按3725.8买入,结果成交在3725.6,亏了
处理方案我总结了几种:
| 方法 | 精度 | 成本 | 适用场景 |
|---|---|---|---|
| NTP | 毫秒级 | 低 | 低频策略、研究 |
| PTP(硬件时间戳) | 微秒级 | 中 | 中频做市 |
| GPS时钟+FPGA | 纳秒级 | 高 | 高频做市 |
| 软件时间戳校正 | 微秒级 | 低 | 回测、离线分析 |
我个人习惯用软件时间戳校正。原理很简单:
- 记录本地时钟和交易所时钟的差值
- 用线性回归拟合偏差趋势
- 实时校正本地时间戳
def calibrate_timestamp(local_ts, exchange_ts):
# 假设我们有一组配对的时间戳
# local_ts: 本地收到数据的时间
# exchange_ts: 交易所生成数据的时间
diff = local_ts - exchange_ts
# 用滑动窗口计算平均偏差
avg_diff = np.mean(diff[-100:])
# 校正后的时间戳
calibrated_ts = local_ts - avg_diff
return calibrated_ts
小技巧:回测时,我建议把时间戳偏差也模拟进去。比如在回放数据时,给每个Tick加一个随机偏差(范围在±5微秒),看看策略能不能扛得住。这样实盘时心里更有底。
3.4 回放引擎设计
回放引擎,说白了就是模拟交易所给你推数据。设计得好,回测结果才可信。我设计的回放引擎核心逻辑是这样的:
class TickReplayEngine:
def __init__(self, tick_file, speed=1.0):
self.ticks = load_ticks(tick_file) # 加载二进制Tick数据
self.speed = speed # 回放速度倍率
self.current_idx = 0
self.last_replay_time = None
def next_tick(self):
if self.current_idx >= len(self.ticks):
return None
tick = self.ticks[self.current_idx]
# 时间控制:按真实时间间隔回放
if self.last_replay_time is not None:
real_interval = (tick.timestamp - self.last_replay_time) / self.speed
time.sleep(real_interval) # 等待真实时间
self.last_replay_time = tick.timestamp
self.current_idx += 1
return tick
回放引擎的几个关键点:
- 时间控制:按真实时间间隔回放,别一股脑全推出来。否则你的策略逻辑在回测里跑得飞快,实盘时根本来不及处理。
- 速度可调:支持1倍、10倍、100倍速回放。调试时用慢速,批量回测时用快速。
- 随机偏差:模拟网络延迟和时钟偏差,让回测更接近实盘。
- 事件驱动:每个Tick触发一个事件,策略订阅后处理。别用轮询,效率太低。
我的经验:回放引擎一定要支持「暂停」和「单步执行」。调试策略时,你肯定想看看某个Tick进来后,策略到底做了什么。没有这个功能,调试起来会非常痛苦。
最后,画一张图总结一下Tick级数据处理的整体流程:
嗯,以上就是Tick级数据处理的全部内容。从数据格式到清洗对齐,从时间同步到回放引擎,每一步都有坑。我当年做第一个高频策略时,就是因为在时间同步上没处理好,回测曲线漂亮得不行,实盘直接亏了两个月。后来才发现是时钟偏差的问题。
记住一句话:数据质量决定策略上限。Tick级数据处理做扎实了,后面的策略开发才能事半功倍。