2. Tick数据基础
在进入乱序、重复与延迟处理之前,必须先建立对Tick数据本身的准确认知。很多数据质量问题的根源,并不是处理算法不够复杂,而是对Tick的字段语义、时间戳来源和传输链路缺乏清晰理解。本节从定义、数据结构、时间戳类型、数据源与协议四个层面,为后续实战打下基础。
一、Tick的定义与本质
Tick是市场行情的最小可观测事件单元,通常表示一次成交、一次委托簿变更或一次快照推送。它不等同于“每秒一条”或“固定频率数据”,而是由市场事件驱动、时间间隔不均匀的离散记录。理解这一点非常关键:Tick的到达节奏本身带有信息,间隔的疏密、突发与静默都可能反映流动性变化。
从数据工程视角看,Tick是一条带有时间标记的状态变更消息。它至少包含:标的标识、事件类型、价格、数量、方向以及时间信息。不同市场、不同数据源对字段的定义存在差异,但核心语义相对稳定。
核心认知:Tick是事件流,不是时间序列采样点。它的顺序、重复与延迟问题,本质上来自“事件发生时间”与“事件被记录/接收时间”之间的偏差。
二、Tick的典型数据结构
以常见的逐笔成交为例,一条Tick通常包含以下字段:
| 字段 | 含义 | 典型类型 | 注意事项 |
|---|---|---|---|
| symbol | 标的代码 | string | 需统一交易所后缀 |
| exchange_time | 交易所撮合时间 | int64/纳秒 | 权威事件时间 |
| local_time | 本地接收时间 | int64/纳秒 | 受本机时钟影响 |
| price | 成交价 | decimal | 避免浮点误差 |
| volume | 成交量 | int | 单位需确认 |
| side | 买卖方向 | enum | 部分源用主动方标识 |
| seq | 序列号 | int64 | 判断乱序与丢包的关键 |
实际落地时,建议在原始字段之外增加采集时间、来源标识和入库时间,形成可追溯的数据血缘。下面是一个简化的Tick结构定义:
struct Tick {
string symbol;
int64 exchange_time_ns; // 交易所时间
int64 local_time_ns; // 本地接收时间
int64 collect_time_ns; // 采集落盘时间
double price;
int64 volume;
int8 side; // 1买 -1卖 0未知
int64 seq; // 交易所序列号
string source; // 数据源标识
}
三、三类时间戳:交易所时间、本地时间、采集时间
时间戳是Tick处理中最容易出错、也最值得深入理解的部分。同一笔成交在链路上会留下多个时间标记,它们语义不同,不能混用。
- 交易所时间:由交易所撮合系统在事件发生时打上,是判断事件真实顺序的权威依据。它通常精度高、单调递增,但可能因交易所侧批量推送而出现同一时间戳多条记录。
- 本地时间:行情网关或客户端收到消息时打的本地时钟时间。它反映“我什么时候看到”,受本机NTP同步质量影响,可能出现回拨或漂移。
- 采集时间:数据被写入采集队列或落盘时记录的时间。它包含排队、序列化、批处理等内部延迟,适合做链路监控,不适合作为事件排序依据。
实践建议:排序优先使用交易所时间加序列号;本地时间用于延迟统计;采集时间用于排查内部处理瓶颈。三者混用会导致乱序判断失真。
四、常见数据源与传输协议
不同数据源的Tick在完整性、延迟和字段丰富度上差异明显。常见来源包括交易所直连行情、券商柜台转发、第三方数据商以及历史归档文件。传输协议方面,低延迟场景多用UDP组播,可靠场景多用TCP或专有二进制协议,历史回放则常见CSV、Parquet等格式。
# 典型组播接收配置示例
group=239.1.1.1
port=30001
protocol=UDP
buffer_size=8MB
reconnect=false
UDP组播延迟低但可能丢包和乱序,TCP可靠但存在队头阻塞。选择协议时,需要结合业务对延迟与完整性的容忍度。无论哪种协议,都应在入库前保留原始报文或原始字段,便于后续复盘。
五、案例与常见误区
某团队在回测中发现策略收益异常偏高,排查后确认是Tick按本地时间排序,而本地时钟曾发生回拨,导致部分成交被错误地排到未来。改为按交易所时间加序列号排序后,收益回归正常。这个案例说明:时间戳选择直接决定数据顺序,进而影响策略逻辑。
常见误区:把本地时间当作事件时间;忽略序列号导致无法识别丢包;认为同一时间戳一定重复;直接用采集时间做延迟评估却不校准时钟。
总结来看,Tick数据基础决定了后续所有处理逻辑的边界。明确字段语义、区分三类时间戳、理解数据源与协议特性,才能在面对乱序、重复与延迟时做出正确判断,而不是依赖经验猜测。