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。但注意,有些数据源(尤其是历史数据回放时)可能会把这个字段放在包头的扩展区,而不是包体里。
我遇到过一种情况:某个数据源在凌晨切换日期时,包头的日期已经变了,但包体里的逐笔成交时间戳还是前一天的。如果你不做交叉校验,回测结果会多出一些“幽灵交易”。
if (header.date != body.date) { skip(); }
3.3.2 证券代码:6位代码的“陷阱”
证券代码通常是6位ASCII字符串。但这里有个细节:上交所的代码前面会补0,比如“600001”就是6位。深交所的代码也是6位,比如“000001”。
但有些老的数据源,可能会用整数存储,比如600001。如果你直接当整数用,没问题。但如果你要拼接成“SH.600001”这种格式,就得小心了。我建议统一转成字符串,并补齐前导零。
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 一张图看懂解析流程
说了这么多,不如一张图来得直观。下面是我自己画的一个解析流程图,展示了从原始字节流到结构化数据的完整过程。
3.5 实战中的“坑”与“解”
最后,分享几个我在工程化处理中踩过的坑,希望能帮你省点时间。
坑1:字节序问题
沪深交易所的数据包,大部分字段是小端序(Little-Endian)。但有些老版本的接口,或者某些特殊字段,可能是大端序。我建议在解析时,统一用 ntohs 或 ntohl 函数做一次转换,确保万无一失。
坑2:时间戳的“回滚”
在极少数情况下,你可能会收到时间戳比上一笔还小的数据包。这通常是因为交易所的时钟同步问题,或者数据重传导致的。我的处理方式是:如果时间戳小于上一笔,直接丢弃,不做任何处理。
我的个人习惯: 在解析代码里,我会加一个“数据质量监控”模块。每收到1000个包,就统计一下时间戳是否递增、证券代码是否合法、价格是否在合理范围内。一旦异常率超过1%,立刻报警。这比事后复盘要有效得多。
好了,关于接口规范和包结构,就聊到这里。记住,解析只是第一步,真正考验人的,是如何在毫秒级的时间内,把这些原始数据变成可用的信号。
公众号:蓝海资料掘金营,微信deep3321