简介:深圳证券交易所2021年8月发布的《Level2行情数据接口规范V1.11》,是量化交易、高频交易及行情系统开发人员必须掌握的技术文档。规范以STEP协议和FAST编码为基础,完整定义了Level2行情快照、逐笔委托、逐笔成交等消息格式,并详细说明频道代码、开关类别、业务拒绝消息以及盘后定价交易、港股通、期权备兑、债券现券等业务场景的字段约定,对追求精确挂单和涨停板策略的开发者尤其关键。文档保留了2013年至2021年的完整修订历史,包括2014年增加快照行情频道统计消息,2016年支持港股通开关,2020年加入盘后定价大宗交易,2021年新增债券竞买行情等关键变更,同时明确了接口兼容性要求,帮助读者理解演进脉络、规避字段兼容性问题。资源包为单份PDF,大小仅682KB,内容为深交所官方原文,适合与最新交易所文档对照查阅,可用于开发解码器、验证行情网关连接及排查字段错误。该规范在CSDN已有1991人学习浏览,是深交所Level2行情接入开发中的高价值参考资料。
1. 深交所Level-2行情数据接口规范:从读懂V1.11到跑通第一帧行情
拿到一份深交所level2行情数据接口规范V1.11,第一反应往往不是从头读文档,而是先去找现成的代码示例。这类行情接口规范有个共同特点:每个字节怎么排列、每种消息怎么定义都写得很细,却几乎不提供完整可运行样例。做量化回测的人想拿逐笔委托还原盘口,做行情展示的人想解析十档快照,做风险监控的人想实时统计资金流向,三类人读的是同一份规范,落地复杂度却相差数倍。这里想按协议结构、联调路径、数据解析、高频避坑四个环节展开,目标是让新手能按步骤接进来,让熟手能对照排查自己的问题。
2. 协议骨架先于数据字段:会话管理、消息头与订阅通道
2.1 把V1.11拆成三层来读:链路、消息、业务
读深交所level2行情数据接口规范V1.11,我一般不会从第一页顺序往后翻,而是先看目录,把整份规范按功能拆成三层。链路层描述TCP连接怎么建立、怎么保活、断线重连有什么限制;消息层定义每条消息都带有的公共头部,通常包含消息类型、消息长度、序列号;业务层才是真正有价值的行情数据,快照、逐笔委托、逐笔成交都在这一层定义。
三层拆法最大的价值是定位问题快。数据乱码先查消息头,行情断流先查链路层,字段值对不上再回业务层比对定义。很多新手第一遍读规范就把精力放在逐笔成交的字段清单上,到真正拼报文时才发现消息头里长度字段的语义理解错了,整包数据都解不开。我习惯边读边做一张消息头速查卡:把消息头各字段的偏移、宽度、类型抄在一张纸上,标注清楚是定长还是变长、字节序是小端还是大端。这张纸在联调阶段比规范原文翻得还频繁,排查问题时能省掉大半翻文档的时间。
消息头里最重要的三个字段是消息类型、消息长度和序列号。消息类型决定消息体按哪张字段表解释;消息长度决定当前消息的边界,是解决TCP分包和粘包的核心依据;序列号用来检查消息是否丢包、是否乱序。需要注意,这三个字段在V1.11里的位置、宽度和语义要以文档自身定义为准,不要拿其他市场的接口经验直接套用,字段宽度差一个字节,解出来的数值就会完全错乱。
另外要提醒一句,读规范时脑子里始终带着"我最终要解出哪个字段、用来做什么"这个问题。目标是算实时买卖压力,就重点读逐笔委托的买卖方向、价格和数量;目标是画分时成交,就重点读逐笔成交的成交价格和成交量。带着目标读,比从头到尾抄字段效率高得多,也能更快发现规范里哪些字段对你没用、哪些字段是必须的。
2.2 登录、心跳与重连:会话生命周期里的三个关键参数
接口联调的第一步不是收行情,而是登录。登录报文通常包含账号、令牌和若干协商参数,校验字段的计算方式一般是账号加令牌配合约定算法生成。规范对登录失败的定义往往很细:账号错误、令牌过期、并发连接数超限、IP不在白名单,各有各的错误码。实际联调中遇到最多的情况是登录明明成功却收不到任何行情数据,这类问题九成出在订阅环节,而不是登录本身。
登录报文里有一个容易看走眼的细节:很多行情接口的登录响应不是立即返回,而是隔一小段时间才到达。如果客户端在登录请求发出后立刻进入等待推送状态,可能把稍后到达的登录响应当成未知消息丢掉,导致会话状态错乱。联调时应该在登录请求发出后的几秒内保持对响应的监听,把登录响应的消息类型也纳入分发逻辑,不要只处理行情消息。
登录之后是心跳。行情接口是长连接,服务端不会无限等待一个沉默的客户端,常见机制是客户端定期发送心跳包,服务端在超时窗口内没收到任何字节就主动断开。心跳间隔这个参数值得认真对待:设太短,心跳报文挤占带宽,一台机器接入多个行情源时尤其明显;设太长,一次网络抖动就可能超过服务端超时导致掉线。我一般把心跳间隔取为规范允许最短值的1.5到2倍,同时在应用层做独立的超时统计,连续几个心跳周期没有任何数据到达,就主动发起重连。
重连策略是经常被忽略的第三个参数。网关侧通常有连接数限制,断线后立即重连很容易被判定为频繁连接而拒绝。常见做法是退避重连:第一次失败等1秒,第二次3秒,第三次5秒,之后封顶30秒。联调阶段这套策略显得多余,但到了盘中连续掉线时,它直接决定程序是自动恢复还是一直撞墙。
| 参数 | 建议取值 | 说明 |
|---|---|---|
| 心跳间隔 | 规范允许最短值的1.5~2倍 | 太短浪费带宽,太长容易被中间设备清理 |
| 应用层超时 | 连续3~5个心跳周期 | 用于本地主动断链,不等服务端踢人 |
| 重连退避 | 1秒→3秒→5秒→封顶30秒 | 避免触发网关的频繁连接限制 |
2.3 订阅通道:快照、逐笔委托、逐笔成交的取舍
登录之后,客户端需要向网关发送订阅请求,网关才会推送行情。深交所Level-2的行情数据按内容可分成三类:快照、逐笔委托、逐笔成交。快照是周期性生成的盘口截面,相当于每隔固定时间拍一张市场状态照片;逐笔委托是每一笔挂单明细;逐笔成交是每一笔成交明细。规范会为这些数据类型分配一个或多个消息类型编码,订阅请求里要填的正是这些编码。
订阅这一步有两个容易踩的坑。第一个是账号权限:不是所有Level-2账号都有权限收到全部类型数据,订阅了没有权限的消息类型,网关通常不会报错,只会默默不推送。排查收不到逐笔时,第一件事不是改代码,而是确认账号权限覆盖范围。第二个坑是订阅维度:按股票代码订阅和按市场全量订阅,报文格式差别很大。全量订阅简单,但数据量大,处理不过来时消息会在客户端积压;按代码订阅灵活,却要维护动态代码表,处理停牌、代码变更这些边缘情况。
联调初期不要一上来就同时订阅全部类型,先只订阅快照,把一条消息的解析链路跑通,再增加逐笔委托,最后加逐笔成交,每增加一类就验证一次解析结果。这个顺序能让问题范围缩得很小,不至于三类数据同时推过来,出了错都不知道该查哪条链路。V1.11对订阅请求的格式和参数范围有明确约定,联调前值得把这一节反复读两遍。
3. 逐笔委托与逐笔成交:Level-2盘口还原的数据基础
3.1 快照消息:十档盘口与行情统计字段
普通行情一般只有五档盘口,Level-2快照的核心提升是十档:买一到买十、卖一到卖十的价格和委托量都会推送。快照消息体里除了盘口,还包含最新价、成交量、成交额、开高低收等行情统计字段。需要注意,成交量这类字段通常是累计值,从零开始累加,不是每帧之间的增量。要做增量计算,得自己用相邻两帧求差,这个差值才对应这一帧时间间隔内的成交。
快照是按周期推送的,不是事件驱动。这个性质在做盘口重建时影响很大:快照只是某个时刻的截面,两次快照之间发生的逐笔变动,最终都被下一帧快照校准。我一开始接入时就养成一个习惯:把快照当基准,把逐笔当增量,二者不一致时以快照为准做校准,这个习惯让后续的盘口还原少走了很多弯路。
快照消息里还有一个必须关注的字段是行情生成时间。不同版本规范里,这个时间字段的精度和语义不完全一样,有的精确到毫秒,有的精确到百微秒。做历史回放时如果拿客户端的本地接收时间做K线切片,会引入网络延迟误差,用行情自带的时间字段才是正确做法。字段精度是多少,要回到V1.11的字段定义里确认,不能想当然。
| 字段类别 | 典型内容 | 解析注意 |
|---|---|---|
| 盘口类 | 买一至买十价量、卖一至卖十价量 | 价格和数量的单位、特殊值编码以规范为准 |
| 成交类 | 最新价、成交量、成交额、开高低收 | 成交量多为累计值,增量需自行求差 |
| 统计类 | 加权均价、换手率等 | 不同版本口径不同,回测前先做口径核对 |
| 时间类 | 行情生成时间 | 精度随版本变化,回放对齐用该字段 |
快照还有一个应用方向是计算资金流向。用逐笔成交按买卖方向累加,可以算出主动买入、主动卖出的规模,这个指标在普通Level-1行情里算不出来。但计算前要确认规范里的买卖方向字段语义:有的版本用买和卖两个枚举,有的版本用主动买、主动卖、未知三态,语义不同,统计结果会完全不同。
3.2 逐笔委托与逐笔成交:从字段复原每笔交易
逐笔委托记录每一笔挂单的细节,核心字段包括委托号、委托价格、委托数量、买卖方向、委托类型。委托号是不是全局唯一,要看规范具体说明,很多行情接口的委托号只在某个范围内唯一。做历史数据落库时,建议直接用委托号加股票代码加日期做联合主键,否则长期数据里很容易撞键,后续关联查询会莫名多出重复行。
逐笔成交记录每一笔成交,核心字段包括成交号、成交价格、成交量、买方委托号、卖方委托号等。它把一笔成交拆成买方和卖方两个委托号,天然适合还原谁和谁在什么价位成交了多少。但如果只是要算累计成交量、成交额,就不要去关联委托表,直接按成交价和成交量累加即可。关联两张大表是重活,性能开销大,而且一旦某个委托号在数据流里缺失,关联结果会非常难查。
还有一个细节:逐笔委托和逐笔成交是两条独立数据流,成交记录里的委托号与委托记录里的委托号存在引用关系,但两条流未必严格同步。做跨流关联之前,一定要先评估数据完整性,比如先统计每条成交是否能找到对应的委托。如果完整性达不到预期,就要调整方案,不要硬做关联,否则只会得到一份难以解释的半对账结果。
3.3 盘口重建:快照基准与逐笔增量的对账逻辑
把Level-2数据用出价值,最常见的一件事是重建盘口:维护一个十档队列,逐笔委托到达就更新队列,逐笔成交到达就扣减对应挂单量。听起来直接,实际做起来会发现快照和逐笔很难对得齐。问题出在两类数据的推送时序不同。快照是周期推送,逐笔是事件推送,某一笔委托可能发生在上一次快照生成之后、下一次快照生成之前,也可能刚好跨过快照边界。如果拿一帧快照初始化盘口,然后把之后到达的逐笔全部应用一遍,盘口会持续累积偏差:已经成交但未撤掉的挂单残留在盘口上。
见过三种处理方案。第一种以快照为基准,只应用快照之后到达的逐笔,同时每隔一段固定时间用新快照强制校准,把漂移纠正回来。第二种是同时缓存快照和逐笔,用行情自带时间戳把两类消息对齐到同一个时间窗口,再按窗口顺序重放。第三种完全不用快照,只用逐笔自建盘口,灵活性最高,但要求逐笔数据绝对连续,中间丢一条记录,盘口就永久错位且无法自查。实盘环境里前两种更稳,第三种适合历史数据研究场景。
不管选哪种方案,都要在本地维护一份已处理序列号的记录。快照和逐笔各有各的序列号空间,本地状态记录到哪个位置,重连续传时才能接着处理。很多重连之后数据错乱的案例,本质都是没记住处理进度,重连后又从新数据开始处理,中间漏了一段,本地状态和真实市场状态从此不一致。
4. 联调最小路径:从登录到解析一帧快照的完整闭环
4.1 联调前要准备的四样东西
第一样是一个权限覆盖目标数据的Level-2账号。所谓覆盖目标数据,是指这个账号能订阅到计划解析的全部消息类型,尤其是逐笔。先用权限齐全的账号把链路跑通,再换受限账号验证权限控制逻辑,这个顺序最省时间,也能避免一开始就把问题引向权限。
第二样是网关地址和端口。一份规范会对应多个服务地址,行情接入、行情文件下载、状态查询用的往往是不同端口。联调时把配置整理到单独配置项里,不要硬编码在代码中,否则切换测试环境和真实环境时要改一堆地方,还容易漏改漏配。
第三样是规范文档和自制的消息头速查卡。PDF版规范用来按字段名快速检索,速查卡用来在做报文解析时对照偏移量。速查卡上我会额外标注每个字段的取值范围,这些信息在排查异常值时非常有用,比如价格字段解出个负数,能立刻联想到方向字段取值错误。
第四样是能打印收发原始字节的调试工具。最低要求是在程序里加一个开关,把收到的每条消息前十六个字节用十六进制打印出来。别只在出错时才加,一开始就开着,等链路跑通了再关掉,你会少踩很多坑,因为很多解析问题只有对着原始字节才能定位。
联调顺序上,建议先把登录和心跳跑通,再订阅快照,最后才加逐笔。每加一种消息类型,都确认前一种没有被影响。这个顺序看起来慢,实际上能省下来回排查的时间,也让每一步的失败点都足够明确。
4.2 登录、心跳与消息分发的最小代码骨架
下面这段是最小联调框架,覆盖登录后的主循环:读消息头、按长度取消息体、按消息类型分发。字节序、消息头长度和字段偏移在V1.11里怎么定义,就以它为准,代码里的数值只是联调占位。
import socket import struct # 联调最小框架:登录后循环读消息头,按消息类型分发 # 常量在联调时按V1.11规范的实际定义替换,不要直接上线 HEADER_LEN = 16 # 消息头总字节数(占位值) MSG_TYPE_OFFSET = 0 # 消息类型字段在头部中的偏移 MSG_LEN_OFFSET = 2 # 消息长度字段在头部中的偏移 SNAPSHOT_TYPE = 1 # 快照消息类型(占位值) ORDER_TYPE = 2 # 逐笔委托消息类型(占位值) TRADE_TYPE = 3 # 逐笔成交消息类型(占位值) class Level2Client: def __init__(self, host, port, user, token): self.sock = socket.create_connection((host, port), timeout=10) self.buffer = b"" self.user = user self.token = token def login(self): # 登录报文按规范逐字段拼装,校验字段通常由账号+令牌计算 # 注意登录响应可能在几秒后才返回,主循环要能处理它 pass def subscribe(self, codes): # 订阅报文核心是消息类型编码列表,权限不足时网关会静默丢弃 pass def _recv_exact(self, n): # 关键点:先收满 n 个字节,解决TCP粘包和半包 while len(self.buffer) < n: chunk = self.sock.recv(65536) if not chunk: raise ConnectionError("连接已断开,需要进入重连流程") self.buffer += chunk data, self.buffer = self.buffer[:n], self.buffer[n:] return data def next_message(self): header = self._recv_exact(HEADER_LEN) # <H 表示小端序读16位无符号整数,字节序以规范为准 msg_type = struct.unpack_from("<H", header, MSG_TYPE_OFFSET)[0] msg_len = struct.unpack_from("<H", header, MSG_LEN_OFFSET)[0] body = self._recv_exact(msg_len - HEADER_LEN) return msg_type, body def handle_snapshot(self, body): pass def handle_order(self, body): pass def handle_trade(self, body): pass def run(self): while True: msg_type, body = self.next_message() if msg_type == SNAPSHOT_TYPE: self.handle_snapshot(body) elif msg_type == ORDER_TYPE: self.handle_order(body) elif msg_type == TRADE_TYPE: self.handle_trade(body) # 未定义类型不要直接丢弃,先打印头部字节再决定逻辑说明:_recv_exact是这套框架的核心,它保证每次调用都取到完整指定长度的字节,把TCP粘包和半包问题挡在业务逻辑之外。struct.unpack_from按规范声明的字节序和宽度,从消息头固定偏移处解出消息类型和消息长度,再用消息长度减去消息头长度,精确知道消息体有多长。
参数说明:HEADER_LEN、MSG_TYPE_OFFSET、MSG_LEN_OFFSET三个常量,联调时必须改成V1.11里的实际值。字节序用小端还是大端,不要凭经验猜,直接看规范说明。登录和订阅函数先留空实现,但主循环的分发逻辑要先把未知消息类型打印出来,联调时很多诡异问题都是靠这一行打印定位的。
4.3 快照与逐笔解析的三个差异点
快照和逐笔在解析方式上有三处明显差异,处理不好会埋下隐患。第一处是消息体结构:快照通常是单条大消息,字段多、定长字段为主;逐笔则是一条消息里包含多条记录,需要先从消息体里读出记录条数,再循环解析每条记录。记录条数的计算方式在规范里有明确约定,通常是消息体长度除以单条记录长度,也可能有单独的条数字段。
第二处差异在更新方式。快照消息到达后,本地十档盘口整体替换;逐笔消息到达后,是逐条增量更新。增量更新对消息的连续性和顺序要求极高,一旦漏了一条委托,本地盘口就再也回不到真实状态。相比之下,快照随时能把盘口纠正回来,这就是前面反复强调快照是基准的原因。
第三处差异是性能边界。逐笔数据量远大于快照,一条逐笔消息里可能包含几十条记录,盘中高峰期的消息速率会很高。如果解析逻辑落在接收线程里,很容易出现积压。常见做法是接收线程只做分包和字节拷贝,把解析和业务处理丢给独立线程池,两者之间用有界队列衔接,队列满了就记录丢弃并预警。
| 对比维度 | 快照 | 逐笔委托/逐笔成交 |
|---|---|---|
| 消息体结构 | 单条大消息,字段多 | 一条消息含N条记录 |
| 本地更新方式 | 整体覆盖 | 逐条增量 |
| 解析重心 | 字段偏移和填充字节 | 记录条数循环与内存复用 |
| 性能瓶颈 | 低频,压力小 | 高频,需异步处理 |
5. Level-2接口落地避坑:五个高频问题与排查顺序
联调Level-2接口的过程,本质上是在跟字节序、时序和权限做斗争。以下五条是实践中反复出现的问题,每条按现象、原因、解决的顺序写,排查时建议按这个顺序来,能少走很多弯路。
5.1 登录成功却收不到任何行情
现象:登录报文返回成功,心跳保持正常,但行情通道完全静默,连一帧快照都收不到。
原因:九成是订阅环节的问题。订阅请求没发出去、订阅的消息类型编码填错、或者账号没有对应数据类型的权限。网关对无权限的订阅通常不报错,只是不推送,这让问题更难定位。
解决:先在代码里确认订阅请求确实发到了网关上。再把订阅范围改成全市场、全类型,排除股票代码维度的问题。最后联系行情服务提供方确认账号权限,这一步最容易被忽略,也最常是根因。
5.2 盘中行情断流,重连后又恢复
现象:程序运行几十分钟或几个小时后,行情停止推送,应用层没有任何报错。手动重启连接后恢复正常,但过一阵又断。
原因:中间网络设备的空闲超时把长时间没有字节流动的连接回收了。很多行情网关的心跳只作用于控制连接,行情数据通道没有独立心跳,低频时段没有行情可推,连接就被设备判定为空闲并清理。
解决:缩短心跳间隔,让连接始终有字节在流动;同时在应用层做独立超时检测,连续几个心跳周期收不到任何字节,主动断开重连。断流后的续传要处理好顺序:先重连、再订阅、然后等待下一帧快照校准本地状态,不要一恢复就处理增量。
5.3 解析出来的字段值明显异常
现象:个别价格字段解出来是负数或者天文数字,时间字段完全不对,字符串字段乱码。这类问题查出来之前像玄学,查出来之后只能怪自己没先翻规范。
原因:最常见的是字节序反了,小端数据按大端解,或者反过来,数值会完全错乱。其次是消息头长度字段理解错,导致消息体多读或少读,后续所有字段偏移全错位。还有一种情况是把规范里的保留字段当成了业务字段。
解决:抓一帧已知内容的原始字节,手工按规范逐字段拆解,和程序解析结果对比。不要用模拟数据测,直接用网关推送的真实报文。排查时先确认消息头三个字段解对了,再往下看业务字段。
5.4 逐笔和快照数量对不上
现象:同一只股票,把全天逐笔成交的成交量累加起来,和快照里的总成交量不一致,差出不少。
原因:两类数据的推送时序不同,快照是周期截面,逐笔是事件流。如果拿本地接收时间做归类统计,网络延迟会直接把一部分逐笔划到错误的快照区间里,两个数字自然对不上。另外逐笔数据若在采集或传输中丢失,累计值也会偏小。
解决:统一用行情自带的时间字段做统计,不要用本地接收时间。用一个可复现的时段做交叉验证:取某只股票某个交易日的数据,按行情时间重放,逐笔累计成交量应该与收盘后快照总成交量基本一致。差异超过合理范围,就说明有丢包,需要检查采集链路。
5.5 程序跑一会儿后延迟越来越大
现象:刚启动时行情延迟只有几毫秒,运行一段时间后延迟涨到几十毫秒甚至秒级,消息积压越来越严重。
原因:接收线程里做了太多事情:解析、落库、推送,任何一个环节变慢都会拖垮接收。逐笔高峰期消息量很大,单线程处理不过来,队列越积越长。另一种隐蔽原因是日志打印过多,在盘中高频行情下,每条消息打一行日志都会成为性能黑洞。
解决:接收线程只负责读字节和按长度分包,解析放到独立线程,落库用批量写入,下游不消费就丢弃并告警。日志联调时全开,实盘直接降到错误级别,这个习惯能让延迟问题减少一半。
6. 验证解析正确性:用行情自洽性做回归测试的两个技巧
6.1 用全天累计值做交叉验证
验证解析是否正确,不需要去对什么外部参考值,行情数据自身就是自洽的。拿一只股票一个交易日的数据,把逐笔成交的成交量、成交额分别累加,再和当天最后一帧快照里的累计成交量、成交额对比。差异如果在一个很小的比例内,说明逐笔解析和快照解析基本都对了;如果对不上,先检查是不是有丢包,再检查字段位宽和时间精度。
这个验证最好做成独立校验脚本,每次改完解析代码都跑一遍历史数据。我习惯把校验数据固定选在几个成交活跃、成交清淡、盘中停牌过的股票上,覆盖不同数据形态。这样每次重构解析逻辑,几分钟就能确认没有把行情解坏。
6.2 用下一帧快照校准盘口重建结果
另一个自洽性校验是盘口重建:用一帧快照初始化本地十档,应用这帧快照之后到达的逐笔,然后把重建结果和下一帧真实快照逐价位对比。同价位上挂单量基本一致,说明逐笔应用逻辑正确;如果出现系统性偏差,多半是委托类型里的撤销、撤单处理没做对。
实际项目里的做法是把对比结果按价位记录成差异表,跑完一个时段后人工抽查几个时点。这个方法抓出过一次很隐蔽的问题:某种特殊委托类型在逐笔委托里出现时,被当成普通挂单处理,导致盘口挂单量持续偏高。如果没有快照校准,这类问题几乎不可能被发现,等上线了再想补就来不及了。
最后说一个习惯:接任何Level-2行情接口,我都会保留一份原始报文的抓包存档,日期、股票、时段标注清楚。所有解析代码的回归测试都基于这份存档做,而不是依赖实盘环境。线上环境追求稳定,回归测试的确定性只能靠存档来保证。这套方法在几次实际联调里帮我省下了大量排查时间,希望帮到你。
本文还有配套的精品资源,点击获取