
简介深交所发布的《Level2行情数据接口规范V1.11》是一份面向高频交易、量化策略及涨停板交易场景的官方技术文档系统定义了STEP行情会话、快照与逐笔委托/成交消息、行情类别及交易阶段代码等核心规则。压缩包内共1个文件为PDF格式电子文档大小约682KB适合作为开发与运维人员查阅接口字段、排查行情解析问题的随身参考。该版本完整覆盖2013年至2021年的历次修订如盘后定价大宗交易、债券现券与竞买行情、港股开市前时段、期权备兑转换及转融通出借等开关类别并明确了接口兼容性要求便于在不改造现有系统的前提下平滑升级。此外还补充了逐笔行情发送模式、占位消息及债券相关频道调整等细节解析者可直接对照字段说明校验数据。目前已有1990人学习下载适合量化开发、券商系统对接及交易所行情协议研究人员快速掌握深交所Level2数据规范。1. 深交所Level2行情数据接口规范V1.11拿到文档后最容易忽略的不是报文而是心跳“深交所level2行情数据接口规范V1.11”是一份二进制行情链路协议它规定了登录、心跳、订阅、快照、逐笔等消息的交互顺序和字段布局是接入深交所十档行情、逐笔成交与委托队列的必经入口。很多团队拿到文档后一头扎进报文解析却在联调时反复翻车登录成功但很快被断开、快照中间缺包、价格成了天文数字。问题大多不在权限而在没按规范处理心跳、消息长度和字节序。本文按实际接入流程拆开这份规范适合量化开发者、行情服务商和极速交易系统的工程师参考。2. 协议框架与二进制报文从登录到快照的链路2.1 消息头是解析一切的锚点V1.11的报文结构可以理解为“固定长度消息头 可变长消息体”。无论是登录请求、心跳还是快照服务端推送的第一个字段组都是消息头解析器只有把消息头拆正确才能知道后面跟着多少字节、是哪一类消息、当前序号是多少。深交所官方文档里会给出完整的字段表我在这里画一个接入时常用的最小集合消息长度、会话编号、消息类型、消息序号、时间戳和保留字段。字段常见长度作用消息长度4字节指明本条消息总长度TCP粘包切包时靠它定位边界会话编号4字节服务端在登录应答下发的会话标识后续消息都带它消息类型2字节区分登录、心跳、快照、逐笔成交等具体编号以规范为准消息序号4字节发送方递增计数器接收方用来检测是否丢消息时间戳4或8字节通常是交易日0点起的毫秒数不能当Unix时间戳用保留字段若干做对齐或兼容解析时直接跳过但要占位消息头之所以重要是因为TCP是流协议不是按消息边界传输的。一次recv可能只读到半个消息头也可能一口气收了几条消息。如果不按消息长度字段去截包报文的“分帧”就乱了。我第一次实现时图省事直接在socket循环里假设“每次recv等于一条消息”结果快照数量怎么都对不上最后抓包才发现一条TCP包里有三份快照另一条只有半个。把长度字段读对以后问题立刻消失。这段血泪经验是我建议所有接入者优先实现消息头解析的原因。2.2 登录、心跳与订阅的状态机整个连接过程是一条明确的状态链TCP建连、发送登录请求、等待登录应答、收到会话编号、发送订阅指令、进入行情接收循环同时按空闲周期发送心跳。这里每一步都不能跳尤其不能为了省事跳过登录应答直接去订阅因为会话编号和网关能力位都是应答里带来的。登录请求里最常见的字段是用户名、密码、客户端类型、软件版本号、消息序号。登录应答则带有会话编号、登录结果码、失败原因文本。订阅指令一般要指定订阅类型十档快照、逐笔成交、逐笔委托、委托队列证券范围可以是列表也可以是全市场。常见做法是先订阅全市场快照等跑通链路后再按需订阅逐笔降低早期解析压力。提示V1.11规范里登录请求的消息序号必须从1开始。断线后重连序号不能重置为1而要从上次会话已用的最后一个序号加1继续。这是联调中翻车概率最高的地方。心跳是容易被轻视的一环。行情活跃时每秒几十笔推送会让连接“看起来活着”但中午休市或清淡时段链路可能几十秒没有行情这时如果双方不互发心跳网关会把连接当成僵尸断开。我一般按规范给出的服务端空闲超时阈值的1/3作为心跳周期比如服务端120秒没收到数据就断开客户端40秒发一次心跳。这样可以留足网络延迟余量又不至于频繁发送造成负载。2.3 字段精度、时间基线与字节序Level2行情的核心字段几乎都不是人来读的浮点数。价格字段通常用整数承载再用缩放因子换算成带小数位的价格成交量字段则要区分“股”还是“手”。换算错误会造成价格差几十倍在风控里是事故级别的问题。所以我拿到规范后做的第一件事是整理一张“字段精度对照表”标明每个价格字段的缩放因子和单位而不是直接写解析代码。时间戳也有自己的基线。很多行情字段的时间戳是从当日0点开始的毫秒数或者微秒数不是Unix纪元。如果直接塞进datetime.fromtimestamp得到的是1970年附近的时间。正确做法是组装出“交易日期 当日偏移毫秒”。不同快照类型有的带日期有的不带解析时必须把日期状态维护在会话上下文里。字节序建议用样例报文先探测。国内金融接口有大端也有小端V1.11文档会写清楚。我一般先用第一条登录应答做试探如果解出来的消息长度不是合理值比如几千万字节就翻转字节序再试。这个办法简单有效且不需要等行情推送几分钟内就能确认。还有一点容易被忽略消息长度字段的单位到底是字节还是“字节 头”要看清。有的规范写的是“消息总长度含消息头”有的写“消息体长度”。我用过一个网关文档写的是总长度但实际代码用的是消息体长度导致每次多读16字节序号不断偏移。最终是在线下联调环境里用一条样例报文比对才发现文档和网关行为不一致。3. 用Python跑通最小接入登录、收快照、正常退出3.1 搭建最小连接环境接入前先确认自己拿到了哪个环境的服务器地址和端口。深交所Level2行情一般分生产、测试、灾备环境参数不一样。我习惯于把配置放进环境变量或独立的config模块避免把账号密码写死到代码仓库里。尤其是团队共用代码库账号密码一旦提交到git历史里就是泄漏事故。网络层面要看清楚这台服务器能不能直连行情网关。很多公司内部是隔离网段需要在防火墙上放行TCP端口或者通过指定的跳板机转发。如果只给了一个IP和一个端口先Telnet或nc测通再去谈报文格式。不要上来就跑客户端不然连不上时你很难分清是网络原因还是协议错误。3.2 最小客户端代码从建连到收第一条快照这里给出一个只依赖Python标准库的示例。它演示了如何按消息长度字段完整地读取一条消息并将消息类型、序号和消息体返回给上层。实际布网时你要用深交所V1.11文档里的字段定义替换示例中的消息头结构。import socket import struct # 消息头总长度(4字节) 会话编号(4) 消息类型(2) 消息序号(4) 保留(2) # 以下字段格式只是演示真实字段以深交所V1.11规范PDF为准 HEADER_FMT IIHI2s HEADER_LEN struct.calcsize(HEADER_FMT) LOGIN_TYPE 1 # 示例值登录请求 LOGIN_ACK_TYPE 2 # 示例值登录应答 HEARTBEAT_TYPE 3 # 示例值心跳 SNAPSHOT_TYPE 11 # 示例值十档快照 def recv_exact(conn, n): 读满n字节TCP半包时继续等收不到则抛异常。 buf b while len(buf) n: chunk conn.recv(n - len(buf)) if not chunk: raise ConnectionError(连接已关闭) buf chunk return buf def recv_message(conn): 先读消息头再按总长度读完消息体解决粘包/半包。 header recv_exact(conn, HEADER_LEN) total_len, session_id, msg_type, seq, _ struct.unpack(HEADER_FMT, header) body_len total_len - HEADER_LEN body recv_exact(conn, body_len) if body_len 0 else b return msg_type, session_id, seq, body def build_login(user, passwd, seq): # 示例字段用户名16字节、密码32字节、客户端类型4字节 user_b user.encode(utf-8)[:15].ljust(16, b\x00) pwd_b passwd.encode(utf-8)[:31].ljust(32, b\x00) body struct.pack(16s32sI, user_b, pwd_b, 1) header struct.pack(HEADER_FMT, HEADER_LEN len(body), 0, LOGIN_TYPE, seq, b\x00\x00) return header body def build_subscribe(session_id, codes, seq): # 示例订阅类型2字节 证券数量2字节 每个证券代码6字节 body struct.pack(HH, 1, len(codes)) for code in codes: body code.encode(ascii)[:6].ljust(6, b\x00) header struct.pack(HEADER_FMT, HEADER_LEN len(body), session_id, 10, seq, b\x00\x00) return header body def parse_snapshot(body): 按规范逐字段解析快照这里留作接入点。 # 你可以在这里用 struct.unpack 拆出证券代码、十档价格、十档量等 pass def main(): host 127.0.0.1 # 替换为测试环境地址 port 8080 # 替换为行情服务端口 conn socket.create_connection((host, port), timeout5) # 第一步发送登录请求字段需按V1.11修正 conn.sendall(build_login(user, pass, seq1)) # 第二步等待登录应答 while True: msg_type, session_id, seq, body recv_message(conn) if msg_type LOGIN_ACK_TYPE: print(登录成功会话编号, session_id) break # 第三步发送订阅请求订阅前请确认证券代码合规 conn.sendall(build_subscribe(session_id, [000001, 000002], seq2)) # 第四步进入接收循环 while True: msg_type, session_id, seq, body recv_message(conn) if msg_type SNAPSHOT_TYPE: parse_snapshot(body) elif msg_type HEARTBEAT_TYPE: conn.sendall(build_heartbeat(session_id, seq 1))代码的核心是recv_exact和recv_message这两个函数。recv_exact保证读满指定字节处理了TCP半包recv_message先读消息头然后用长度字段计算剩余字节处理了粘包。很多初学者直接调用recv读消息读不满就丢读多了就破坏下一帧这两个函数是解决这类问题的常规解法。参数上要说明几点HEADER_FMT用的是小端符号“”正式环境要先用样例报文验证total_len如果规范定义为“消息体长度”body_len就直接取total_len不需要减HEADER_LENbuild_login和build_subscribe里的字段顺序只是示意你需要对照规范把登录账号、密码、订阅证券代码按二进制格式填进去。3.3 登录和订阅报文的常见字段清单登录请求和订阅请求虽然不能直接套用上面的代码但字段结构有共性。登录请求体通常包含客户端登录名、密码、客户端类型、协议版本号订阅请求体包含订阅类型、证券数量、证券代码列表。下面是一份我在联调时用的对照表具体偏移以规范为准。报文字段示例值说明登录请求登录名USER01不足长度补0登录请求密码密文或明文按规范要求加密登录请求客户端类型1行情接收端订阅请求订阅类型11十档快照订阅请求证券数量2用2字节表示订阅请求证券代码“000001”ASCII字符前导零保留3.4 运行验证看序号是否连续把代码跑起来后最直接的验证是打印每一条消息的序号确认当前会话下序号严格递增。如果序号出现跳号说明上游有丢包或者你的解析截断了。此时可以加大socket接收缓冲区例如设置socket的recv缓冲为1MB以上。同时用Wireshark抓包对比确认客户端发送的报文和规范上样例一致。另外把日志级别调整好。我一般把每条消息的长度、类型、序号打出来但不要在行情高峰用print逐条打快照数据会拖慢程序。建议只打统计信息例如每隔10秒输出一次消息数再手动抽样解析一条快照字段。这样既能发现问题又不至于把自己淹没在日志里。4. 关键参数与联调规范心跳、重连、队列长度怎么设4.1 心跳与超时参数别把默认值当保命符Level2接入中第一组要拍板的参数是心跳周期和超时阈值。服务端通常有一个“空闲断开时间”超过这个时间没收到任何数据就断开客户端。规范里会给建议值但生产环境还要考虑网络中间设备例如负载均衡和防火墙它们的空闲连接超时可能比网关更短。常见做法是取网关阈值的1/3作为心跳周期既不会因为网络抖动误判也不会因为过于频繁造成额外负载。参数建议取值说明心跳周期服务端阈值的1/3取30秒、40秒、60秒都常见按环境压测超时阈值心跳周期的3倍连续没收到心跳回应再触发重连重连间隔1秒起步指数退避1s、2s、4s、8s最大30s接收缓冲区1MB以上行情峰值大过小会触发内核丢包消息序号校验严格递增跳号时记录并告警会话历史保留最后序号重连后登录序号 最后序号 1这里要特别留意休市场景。中午休市或者夜间时段几乎没有行情客户端必须照常发心跳不能因为“没数据”就以为链路正常。我见过一个团队把心跳逻辑写成了“收到行情才重置计时器”结果休市半小时后连接被网关断开下午开盘前没有自动重连错过了整个集合竞价时段。血泪教训是心跳计时器必须基于系统时钟而不是基于收包事件。4.2 重连与消息补偿指数退避和断点续传断线重连不能简单地“等一秒再连”。如果多个客户端同时掉线大家一起秒级重连网关会瞬间拥塞。我一般用指数退避每次失败后把间隔翻倍最大到30秒同时加入随机抖动避免所有客户端在同一时刻重试。重连成功后的第一步不是立刻订阅行情而是先重新登录拿到新的会话编号。原会话里订阅过的类型要重新发一遍。这里有一个更隐蔽的点消息序号怎么处理。按V1.11常见要求客户端登录序号需要保持单调递增不能回绕。如果你在断开前已经发到序号1000重连后的登录请求应该从1001继续而不是重新从1开始。有些网关如果发现序号比上次记录的还小会判定为“旧消息重放”直接拒绝。所以一定要在本地持久化最近一条已发序号重连时读取并加一。数据补偿方面并不是所有环境都支持断点补数。有的行情网关带缓存服务可以按“会话编号消息序号”请求补发缺口有的则不带丢了就丢了。对于不能补数的环境我的策略是解析侧检测到跳号后立即告警同时记录缺口范围等收盘后用行情中间件的归档数据做二次对齐。不要试图在实时链路上硬找缺口那是把时间浪费在不可控的地方。4.3 联调规范把测试用例和git提交规范绑在一起接入V1.11不只是写一个客户端整个团队要有一份可执行的测试联调规范我们参照计算机软件测试规范来组织测试层级。我把它拆成三层单条报文解析用例、链路交互用例、长时间连续运行用例。单条用例用抓包导出的样例报文做golden master断言每个字段值链路用例跑“登录-订阅-收三条行情-心跳-正常退出”长时间用例至少跑一个完整交易时段记录序号跳号、重连次数、内存增长。代码提交时要有一个规矩凡是改动消息头结构、字段偏移、缩放因子的提交必须带上对应规范章节的引用以及一份“生效前/生效后”的样例报文对比。我们团队把这条写进git提交规范commit message里必须带“L2V1.11-3.2”这种章节号方便后来者回看变更动机。没有关联issue的协议改动不予合并否则两周后没人记得当初为什么改。每次提交前还会跑一遍代码检查规范里的静态检查确认新增的二进制解析分支没有未绑定的异常。另外每次交易所更新规范版本比如V1.10升V1.11不要直接替换解析库。先对照字段表找差异把新增字段在demo里解析出来再用历史录制的行情文件跑一遍回归。这条习惯救过我很多次因为表面上“版本号小版本升级”实际可能悄悄改了消息类型编号或者时间戳单位。5. 接入深交所Level2行情的5个常见坑与排查要点5.1 登录成功但很快被踢心跳参数只设了一半现象是客户端登录正常能看到登录应答但几十秒后连接被网关断开。重启后依然如此。原因很典型只设置了心跳发送周期却没有设置“心跳超时”的本地判定服务端其实已经空闲超时断开客户端还在傻等。解决方式是把超时阈值和心跳周期配套客户端按周期发心跳同时维护一个“最后收到回包时间”超过3个心跳周期没收到任何服务端消息就认为连接失效主动重连。另外检查服务端对心跳的类型要求有的网关要求客户端回发相同类型有的要求回发心跳应答类型字段不能发明。5.2 快照序号跳号问题未必在网络在“长度单位”现象是行情高峰期消息数量看起来没少但序号从100直接跳到200中间没有100到199。原因往往不是网络丢包而是消息头里的长度字段单位解析错了。比如规范写的是“消息体长度”你却按“总长度”截包导致每条消息都少读或多读几个字节解析器逐渐错位把原本不存在的消息当成了下一帧。解决方式是一帧一帧地对样例报文。先用Wireshark抓一段TCP逐包与规范字段比对记录第一条消息的实际字节、第二条消息的起始位置。如果第二条消息的起始位置和你的解算位置差了多少就知道是长度单位问题还是偏移问题。不要把错误归咎于“网络抖动”这个理由在排障里最不值钱。5.3 价格解析成天文数字缩放因子和字节序双杀现象是快照里买一价变成4294967295这种值明显不是行情。原因通常是两个一是字节序不对int32被反向读取二是把价格整数没有按缩放因子换算。有时候两个问题同时存在如果先修正字节序价格又从几百亿变成几百那就还要看缩放因子。解决方式是写一张字段映射表把每个价格字段的位宽、符号、字节序、缩放因子列全。上线前用交易所提供的标准样例报文做一次全字段断言不要只验证一笔报价。特别是负价位或者零价位有些行情用特殊值表示无报价比如int32的最小值表示“不存在”解析时要注意保留这种语义。5.4 休市期间“死连接”心跳要考虑行情空窗期现象是早上连上后一切正常到中午休市后再看连接已经不在了客户端却没有任何报错直到下午开盘才发现断线。原因是行情推送停止后客户端也没有发心跳网关认为空闲超时。解决方式是把心跳完全独立于行情推送。用一个后台线程或定时器严格按系统时间发送心跳。测试时故意设置一个40秒的静默期观察客户端是否还在发心跳、服务端是否回应。如果服务端对心跳回应了本地要更新最后回包时间而不是只看发送时间。这个细节在长时间运行中比很多大功能都重要。5.5 重连后订阅没恢复订阅列表要显式重建现象是断线重连成功了登录也通过了但“漏订”了某几只证券的逐笔数据。原因是重连后会话是全新的网关不会记住你之前订阅过什么。你必须在收到新会话编号后把完整的订阅请求重新发一遍。解决方式是把订阅列表做成配置在登录应答成功后自动触发全量订阅。订阅请求要遵循幂等设计同一只证券订阅两遍不应导致重复推送或者报错。重连时不要只重发一笔增量订阅宁可全量重订后面再按需停订。曾经我把订阅列表放在内存里重连时不慎清掉了一半证券导致某个策略盘中没有数据这类问题只有靠流程才能拦住。6. 最后用样例报文和守恒校验验证接入质量接入跑通不等于接入正确。我习惯在收盘后用两招自检一是用交易所下发的样例报文做黄金标准测试把每条字段的期望值写进测试用例任何代码改动跑一遍二是做“守恒校验”把逐笔成交的手数累加与十档快照里的累计成交量比对误差应该为零。如果对不上解析逻辑大概率有字段错位或精度问题。我还在pre-commit里加了“规范变更扫描”的小脚本每次提交前检查是否涉及消息类型和字段偏移并在commit信息里强制注明V1.11章节号。这个习惯源于一次惨痛经历上线前把缩放因子从10000改成1000只改了配置没改测试结果盘中被风控发现价格偏差10倍。从那以后我把所有协议相关参数都收进独立配置模块并用样例报文锁住回归。建议你也把这条验证链建起来解析库单元测试、链路日志、守恒校验。这样每次升级V1.11小版本都有后悔药重连和心跳也不再是玄学。希望帮到你。本文还有配套的精品资源点击获取