3. Level2行情接口规范:沪深交易所的“数据暗号”

做量化交易,最怕什么?不是策略亏钱,而是数据源出了问题,你还在傻傻地跑回测。

我个人习惯,拿到任何Level2数据源,第一件事不是看K线,而是先拆包。说白了,你得先搞清楚交易所到底给你发了什么。沪深交易所的Level2行情接口,就像一套加密的“数据暗号”,你得先学会解码,才能拿到真金白银的信息。

3.1 沪深交易所的“脾气”不一样

上交所和深交所,虽然都叫Level2,但接口标准完全不同。我当年刚入行时,以为两边差不多,结果把上交所的解析代码直接套到深交所数据上,跑出来一堆乱码。嗯,那次教训挺深刻的。

简单来说:

  • 上交所(SSE):用的是FAST协议(FIX Adapted for STreaming)。这是一种压缩过的二进制协议,解析起来稍微有点麻烦,但传输效率高。
  • 深交所(SZSE):用的是标准的二进制流协议。结构相对清晰,字段对齐比较规整,我个人觉得深交所的包结构对新手更友好。

你想想看,两个交易所的数据源,解析方式都不一样。如果你做全市场策略,代码里必须做两套解析引擎。

3.2 行情数据包结构:拆开“信封”看内容

不管是上交所还是深交所,数据包的结构都遵循一个通用逻辑:包头(Header)+ 包体(Body)

这就像你收到一封信。信封(包头)上写着谁寄的、什么时候寄的、有多长。信纸(包体)才是真正的内容。

3.2.1 包头(Header):先看“信封”

包头的长度通常是固定的。我以深交所的逐笔成交数据包为例,它的包头结构大致如下:

字段名 长度(字节) 说明
起始标识 2 固定为0x55AA,用于校验
包长度 2 整个数据包的长度(含包头)
保留字段 4 一般填0
数据块数量 2 包体里包含了几条记录
数据块类型 1 比如0x01表示逐笔成交

这里有个坑。我曾经在解析上交所的FAST包时,发现包长度字段有时候会包含一些填充字节。如果你直接按这个长度去读,可能会多读几个字节,导致后续解析全部错位。我的建议是:永远先校验起始标识,如果不对,直接丢弃这个包。

3.2.2 包体(Body):真正的“情报”

包体就是一条条具体的行情记录。每条记录的结构也是固定的。我们以深交所的逐笔成交为例,一条记录大概长这样:

字段名 长度(字节) 说明
交易日期 4 YYYYMMDD格式的整数
证券代码 6 ASCII字符串,如"000001"
时间戳 4 自当日0点起的毫秒数
成交价格 4 整数,需除以10000得到实际价格
成交数量 4 整数,单位是股
成交金额 8 整数,单位是分
买卖方向 1 'B'表示买,'S'表示卖

3.3 关键字段解析:这三个字段必须吃透

在这么多字段里,有三个字段是重中之重。如果你搞不清楚它们,后面的所有计算都是错的。

3.3.1 交易日期:别把今天的数据当成昨天的

这个字段看起来简单,但容易出问题。它通常是一个整数,比如20231027。但注意,有些数据源(尤其是历史数据回放时)可能会把这个字段放在包头的扩展区,而不是包体里。

我遇到过一种情况:某个数据源在凌晨切换日期时,包头的日期已经变了,但包体里的逐笔成交时间戳还是前一天的。如果你不做交叉校验,回测结果会多出一些“幽灵交易”。

避坑指南: 我曾经在实盘环境中,因为没校验交易日期,导致策略在午夜12点后,把前一天的最后一笔成交和当天的第一笔成交混在一起计算了VWAP。结果第二天开盘直接买在了高点。从那以后,我每次解析都会加一句:if (header.date != body.date) { skip(); }

3.3.2 证券代码:6位代码的“陷阱”

证券代码通常是6位ASCII字符串。但这里有个细节:上交所的代码前面会补0,比如“600001”就是6位。深交所的代码也是6位,比如“000001”。

但有些老的数据源,可能会用整数存储,比如600001。如果你直接当整数用,没问题。但如果你要拼接成“SH.600001”这种格式,就得小心了。我建议统一转成字符串,并补齐前导零。

小技巧: 在C++或Python里,可以用 sprintf(code, "%06d", raw_code)f"{raw_code:06d}" 来保证永远是6位。

3.3.3 时间戳:精度决定一切

Level2行情的时间戳,精度通常是毫秒级。但不同交易所、不同数据类型的精度可能不一样。

  • 逐笔成交:通常是毫秒级,甚至微秒级(深交所的逐笔成交已经支持微秒)。
  • 快照行情:通常是秒级或毫秒级。

时间戳的存储方式也五花八门。最常见的是:自当日0点起的毫秒数。比如上午9:30:00.000,对应的值就是 9 * 3600 * 1000 = 32400000。

解析时,你需要把这个整数转换成可读的时间格式。我习惯的做法是:

// 伪代码示例
int timestamp_ms = 34200000; // 假设这是从包体里读出来的值
int hours = timestamp_ms / 3600000;
int minutes = (timestamp_ms % 3600000) / 60000;
int seconds = (timestamp_ms % 60000) / 1000;
int millis = timestamp_ms % 1000;
// 输出:09:30:00.000

这里有个容易忽略的点:时间戳的基准时间。绝大多数情况下是当日0点,但极少数历史数据可能会用Unix时间戳(1970年1月1日)。如果你发现解析出来的时间不对,先检查一下基准时间。

3.4 一张图看懂解析流程

说了这么多,不如一张图来得直观。下面是我自己画的一个解析流程图,展示了从原始字节流到结构化数据的完整过程。

Level2行情数据包解析流程 原始字节流 解析包头(Header) 校验起始标识 校验失败 丢弃该包 解析包体(Body) 结构化行情数据 关键校验点: • 起始标识 0x55AA • 包长度是否合理 • 数据块类型匹配 关键字段提取: • 交易日期 • 证券代码 • 时间戳

3.5 实战中的“坑”与“解”

最后,分享几个我在工程化处理中踩过的坑,希望能帮你省点时间。

坑1:字节序问题

沪深交易所的数据包,大部分字段是小端序(Little-Endian)。但有些老版本的接口,或者某些特殊字段,可能是大端序。我建议在解析时,统一用 ntohsntohl 函数做一次转换,确保万无一失。

坑2:时间戳的“回滚”

在极少数情况下,你可能会收到时间戳比上一笔还小的数据包。这通常是因为交易所的时钟同步问题,或者数据重传导致的。我的处理方式是:如果时间戳小于上一笔,直接丢弃,不做任何处理。

我的个人习惯: 在解析代码里,我会加一个“数据质量监控”模块。每收到1000个包,就统计一下时间戳是否递增、证券代码是否合法、价格是否在合理范围内。一旦异常率超过1%,立刻报警。这比事后复盘要有效得多。

好了,关于接口规范和包结构,就聊到这里。记住,解析只是第一步,真正考验人的,是如何在毫秒级的时间内,把这些原始数据变成可用的信号。


公众号:蓝海资料掘金营,微信deep3321