3. Tick数据详解
在事件回放与撮合仿真中,Tick 是最小、最不可再分的一条市场事件记录。它既不是K线,也不是快照,而是交易所行情系统对外发布的“原子级”增量消息。理解 Tick 的定义与字段结构,是后续做回放、重演订单簿、验证撮合逻辑的前提。本节围绕四个问题展开:Tick 到底是什么、一条 Tick 里有哪些字段、时间戳精度如何影响回放、逐笔成交与逐笔委托有什么区别与联系。
一、核心原理:Tick 是增量事件,不是状态快照
很多初学者把 Tick 理解为“某一时刻的盘口状态”,这是最常见的误区。实际上,快照(Snapshot)描述的是“当前状态”,而 Tick 描述的是“刚刚发生了什么变化”。例如某只股票最新价从 10.00 变为 10.01,快照会告诉你“现在最新价是 10.01”,而 Tick 会告诉你“有一笔成交,价格 10.01,数量 300 手,买方主动”。
因此,Tick 的本质是事件流。要还原任意时刻的订单簿,必须从初始快照出发,按时间顺序逐条应用 Tick。这也是“事件回放”名称的由来:回放的不是价格曲线,而是事件序列。撮合仿真同样如此——仿真引擎消费 Tick,重建市场状态,再让策略订单与重建出的订单簿交互。
二、字段结构:一条 Tick 通常包含什么
不同交易所的 Tick 字段命名不同,但语义高度一致。以逐笔成交为例,典型字段包括:
- 证券代码:标的唯一标识,如 600519.SH。
- 交易所/频道:区分上交所、深交所,或不同行情频道。
- 时间戳:事件发生时间,精度可能是秒、毫秒、微秒甚至纳秒。
- 序号:交易所发布的连续序列号,用于检测丢包与乱序。
- 成交价:本笔成交价格。
- 成交量:本笔成交数量,单位可能是股或手。
- 成交金额:部分行情源提供,等于价×量。
- 买卖方向:主动买、主动卖或中性,反映谁吃掉了谁。
- 成交类型:集合竞价、连续竞价、大宗交易等。
逐笔委托的字段则不同,它描述的是“订单进入订单簿”这一事件,典型字段包括委托价格、委托数量、买卖方向、订单类型(限价/市价)、订单编号等。下面用一段伪代码展示一条 Tick 的典型结构:
struct Tick {
string symbol; // 证券代码
string exchange; // 交易所
int64 timestamp; // 事件时间,单位由精度决定
int64 seq; // 交易所序号
char type; // 'T'=成交, 'O'=委托, 'C'=撤单
double price; // 成交价或委托价
int64 volume; // 数量
char side; // 'B'=买, 'S'=卖, 'N'=中性
string orderId; // 委托/成交关联的订单号
};
三、时间戳精度:回放确定性的隐形杀手
时间戳精度直接决定回放能否复现真实市场。若原始 Tick 是微秒级,而回放系统只保留毫秒,那么同一毫秒内的多条事件顺序就会丢失,订单簿重建结果可能与真实情况不一致。常见精度层级如下:
| 精度 | 典型来源 | 回放影响 |
|---|---|---|
| 秒级 | 早期行情或日线衍生 | 同秒事件无法排序,回放失真严重 |
| 毫秒级 | 多数 Level-2 快照 | 同毫秒内需依赖序号排序 |
| 微秒级 | 逐笔委托/逐笔成交 | 可较真实还原事件顺序 |
| 纳秒级 | 部分交易所内部或高频源 | 精度最高,但存储与计算成本大 |
工程上建议:回放排序键使用 (timestamp, seq) 二元组,而不是单独使用 timestamp。这样即使时间戳相同,也能保证顺序稳定。
四、逐笔成交与逐笔委托:区别与联系
逐笔成交(Trade)和逐笔委托(Order)是两类不同事件,但共同构成订单簿演化的完整输入。
- 逐笔委托:描述“新订单进入”。它改变订单簿的深度,是订单簿的“增量来源”。
- 逐笔成交:描述“订单被撮合”。它减少订单簿中的挂单,是订单簿的“消耗事件”。
- 撤单:描述“订单被撤销”,同样减少挂单,常与逐笔委托配套出现。
两者的联系在于:成交必然由委托驱动。没有委托,就没有可成交的订单;没有成交,委托只是挂在订单簿上。回放时,若只有逐笔成交而没有逐笔委托,就无法重建完整订单簿,只能重建“成交价序列”。反之,若只有逐笔委托而没有成交,订单簿会不断增厚却永不消耗,同样失真。
下面用一张图总结 Tick 数据从交易所到回放引擎的链路,以及逐笔委托与逐笔成交在订单簿中的不同作用。
五、案例与复盘:一次回放失真排查
某团队在回放某日深市逐笔数据时,发现 10:00:00.123 这一毫秒内订单簿出现异常:卖一价突然消失,随后又恢复。排查后发现,该毫秒内共有 5 条事件,原始数据按交易所序号排列,但回放程序只按时间戳排序,导致撤单事件被排到了成交事件之前。修正方案是将排序键改为 (timestamp, seq),并增加序号连续性校验:
# 回放前校验序号连续性
prev_seq = None
for tick in ticks:
if prev_seq is not None and tick.seq != prev_seq + 1:
log.warning("seq gap: %s -> %s", prev_seq, tick.seq)
prev_seq = tick.seq
# 排序键
ticks.sort(key=lambda t: (t.timestamp, t.seq))
复盘要点有三:第一,时间戳精度不足时,序号是唯一可靠的顺序依据;第二,逐笔委托与逐笔成交必须成对使用,缺一不可;第三,回放结果应与交易所快照定期比对,发现偏差立即定位到具体事件。