2. 时间戳基础与行情数据格式
在行情推送系统中,时间戳是贯穿整条链路的“时间坐标”。无论是判断一笔成交是否属于当前快照、计算行情延迟,还是做多路数据源的对齐与回放,第一步都必须弄清楚:这个时间字段到底表示什么时刻、用什么单位、由谁产生、精度有多高。本节围绕 Unix 时间戳、毫秒/微秒精度、交易所原生时间字段以及常见行情协议中的时间表示展开,帮助你在拿到一条行情报文时,能准确读出它的时间语义。
一、Unix 时间戳:以“纪元”为原点的绝对时间
Unix 时间戳(Unix Timestamp)定义为自协调世界时 1970 年 1 月 1 日 00:00:00(即 Unix 纪元,Epoch)起经过的时间量。它本质上是一个“绝对时刻”的标量表示,与时区无关——这一点非常关键:时间戳本身不携带时区信息,它记录的是 UTC 时间轴上的一个点。我们平时看到的“北京时间 09:30:00”,只是把这个绝对时刻按东八区偏移量渲染出来的显示结果。
因此,在行情系统中,内部存储与传输应统一使用时间戳(或等价的 UTC 时间),只在面向用户展示时才转换为本地时区。若在传输层就写入“2024-03-01 09:30:00”这类不带时区标识的字符串,接收方无法判断它是北京时间、伦敦时间还是纽约时间,极易造成数小时的错位。
Unix 时间戳的常见单位与量级如下表所示(以示例时刻 2024-03-01 09:30:00 北京时间,即 2024-03-01 01:30:00 UTC 为例):
| 单位 | 含义 | 示例值(约) | 典型来源 |
|---|---|---|---|
| 秒(s) | 10 位整数 | 1709256600 | 部分交易所快照、日志 |
| 毫秒(ms) | 13 位整数 | 1709256600123 | 多数行情 API、WebSocket 推送 |
| 微秒(μs) | 16 位整数 | 1709256600123456 | 高频行情、逐笔委托 |
| 纳秒(ns) | 19 位整数 | 1709256600123456789 | 交易所撮合内核、部分专线 |
识别单位最直接的办法是看位数:10 位通常是秒,13 位是毫秒,16 位是微秒,19 位是纳秒。但不要只依赖位数判断——某些系统会把秒级时间戳存成浮点数(如 1709256600.123),小数部分即亚秒精度。工程上更稳妥的做法是:在协议文档中明确单位,并在解析代码里做显式校验,例如判断数值是否落在合理区间(秒级约 1.7×10⁹,毫秒级约 1.7×10¹²),超出范围即报警。
二、毫秒与微秒精度:精度不等于准确度
行情系统对时间精度的要求差异很大。普通 Level-1 快照通常毫秒级足够;而逐笔成交、逐笔委托、订单簿重建等场景,往往需要微秒甚至纳秒级,因为同一毫秒内可能发生多笔事件,若都截断到毫秒,事件先后顺序就丢失了。
这里要区分两个概念:精度(resolution)指时间字段能表示的最小刻度,准确度(accuracy)指该时刻与真实物理时间的偏差。一个字段写着微秒精度,不代表它准确到微秒——交易所服务器时钟可能只与 NTP 同步到毫秒级,网络传输又引入不确定延迟。因此,在做延迟测量时,必须明确你比较的是“交易所打戳时刻”还是“本地接收时刻”,两者之差包含了时钟偏差与链路延迟,不能简单当作纯网络延迟。
实际处理中还有一个常见陷阱:精度提升带来的数值溢出。32 位有符号整数最大约 2.1×10⁹,秒级时间戳尚可容纳到 2038 年,但毫秒级时间戳早已超出,必须使用 64 位整数(如 Java 的 long、Python 的任意精度整数、C++ 的 int64_t)。若用 32 位存储毫秒时间戳,会发生截断,得到一个完全错误的历史时刻。
# 示例:单位识别与合理性校验(Python 伪代码)
def normalize_ts(ts: int) -> int:
# 统一转换为毫秒
if 1_000_000_000 <= ts < 10_000_000_000: # 秒
return ts * 1000
if 1_000_000_000_000 <= ts < 10_000_000_000_000: # 毫秒
return ts
if 1_000_000_000_000_000 <= ts < 10_000_000_000_000_000: # 微秒
return ts // 1000
raise ValueError(f"时间戳量级异常: {ts}")
三、交易所原生时间字段:同一报文里的多个“时间”
真实行情报文往往不止一个时间字段,它们分别由链路上不同环节产生,语义各不相同。以常见的逐笔成交报文为例,可能同时包含:
- 交易所撮合时间:撮合引擎在成交瞬间打上的时间戳,是事件真实发生时刻的最佳近似,精度通常最高。
- 行情网关发送时间:交易所行情系统把该笔数据写入推送队列或发出时的时刻,通常晚于撮合时间。
- 本地接收时间:客户端网卡收到报文的时刻,由本地时钟产生,受本地时钟同步质量影响。
这三个时间构成了一条“时间链”。用网关发送时间减撮合时间,可粗略估计交易所内部处理与排队耗时;用本地接收时间减网关发送时间,可估计链路与网络延迟(前提是本地时钟已与可靠时间源同步)。若把三者混为一谈,延迟分析就会得出错误结论。工程上建议在解析层就为每个字段建立明确命名,例如 match_ts、gateway_ts、recv_ts,避免后续混用。
四、常见行情协议中的时间表示
不同协议对时间的表达方式差别明显。FIX 协议使用 UTCTimestamp 类型,格式形如 20240301-01:30:00.123,即“日期-时分秒.毫秒”,且明确规定为 UTC,不带时区后缀;其精度由小数位数决定,可到毫秒、微秒甚至纳秒。交易所自有的二进制协议(如部分期货、证券的私有 TCP 流)常直接使用 8 字节整数纳秒或微秒时间戳,解析时需按协议文档指定的字节序(大端/小端)读取。而基于 JSON 的 WebSocket 行情接口,则常见 13 位毫秒整数,偶尔以字符串形式给出以避免 JavaScript 数值精度问题。
下面这张图概括了一条行情报文从交易所到本地的时间字段构成与流转关系,帮助你在解析时对号入座: