☰
深交所Level2行情接口V1.11:会话机制、FAST解码与量化接入实战
2026/10/6 10:52:35 网站建设 项目流程

简介:深圳证券交易所发布的《Level2行情数据接口规范V1.11》是一份面向高频交易、量化交易及涨停板交易者的官方技术标准。文件为PDF格式,共1个文件,压缩包大小约682KB,内容涵盖STEP行情接口的会话机制、快照行情、逐笔委托与逐笔成交、证券实时状态等消息定义,并完整记录了2013年至2021年的历次修订说明。规范中详细解释了Level2行情特有的五档盘口、买卖队列及新增的盘后定价大宗交易、债券现券竞买、期权备兑等开关类别,同时明确了用户行情系统对新增条目的兼容性处理方式。对于需要自主开发或优化行情接入模块的量化团队与接口开发人员,这份PDF可直接作为接口对接、字段排查与版本差异对照的权威参考。已有1990人学习下载,适合熟悉证券交易业务且希望精确掌握深交所Level2数据格式的技术人员。

1. 深交所Level2行情接口V1.11:做量化先看懂这份数据合同

做涨停板盯盘或者自研量化行情引擎的人,第一次翻《深交所Level2行情数据接口规范V1.11》通常会被目录劝退:三十多页的工程技术标准,从会话机制一路写到FAST模板ID。但你做的策略越依赖盘口,这份规范就越是绕不开的“数据合同”。它定义了程序怎么连行情网关MDGW、快照和逐笔行情按什么结构推送、消息丢了怎么重传。适合三类人:写接入程序的工程师、做量化策略的研究员,以及想搞清楚Level2比Level1多了哪些有效信息的进阶交易者。把这份规范啃明白,你才敢把交易决策托付给自研的数据管道。

2. STEP会话与消息封装:双端口分工、FAST解码与登录参数

规范的会话机制部分常常被跳过,这个习惯在别的项目里没事,在行情接入里会害人。后面的消息定义全建立在会话稳定这个前提上,连接参数和超时判不好,后面全白搭。我拆这份规范时,最先做的是把两个端口和各自的能力边界画出来,再决定程序怎么组织线程。

2.1 连接参数:9129实时、9130重传,一个端口只能建一条连接

行情网关MDGW给接入用户提供两个服务端口:实时数据端口默认9129,重传服务端口默认9130。规范写得很死:每一个端口只能建立一个TCP/IP连接。这意味着你没法像连数据库那样开连接池分摊流量,架构上只能一条连接跑到底。现场版行情网关走单向卫星,没有重传端口,只有网络版才支持9130。这个差别直接决定你的补数方案,后面避坑章节会细讲。

连接的安全模型也要提前知道:网关和VSS必须处于同一个安全网络中,传输不带加密,数据安全靠接入用户自己的网络保障。所以别指望在公网上裸连MDGW,券商或信息商生产环境里一般走内网或专线。流量控制是另一个容易被低估的规则:如果VSS处理不过来,网关待发送消息堆积超过阈值,它会直接断开连接而不是慢慢等你。换句话说,收包线程的消费速度必须跟得上峰值行情,否则重连不是可选操作而是必然操作。

2.2 消息封装:STEP消息层包FAST消息体,解码前必须先重置字典

表4-1给出的STEP消息层结构是理解整个协议的地基:每个应用消息都有Standard Header,然后是10201 ChannelNo频道代码、95 RawDataLength记录FAST部分字节数、96 RawData装FAST编码后的消息体。96里可以包含本频道的多条FAST消息,解码前要求先重置FAST字典前值。我第一次实现时偷懒没重置,结果偶发出现价格跳变的脏数据,查了两天才定位到是前值串了。

FAST模板ID编码规则也要背下来:3000-3999是公共消息,4000-15999是实时行情数据。频道心跳模板ID是3001,重传消息是3002,用户信息报告是3003。解码器拿到一个包,按模板ID分支就够了。FAST消息层定义里标注了域名称、FAST操作符和占位标志。操作符为default且占位为Y的字段,一定会出现在字节流里;占位为N的字段可以省略,解码器要按默认值生成,少处理一个占位标志都会让后续字段错位。

2.3 登录与心跳:通信版本号填1.02,心跳间隔3秒,超时翻倍

登录消息的DefaultApplVerID要填通信版本号1.02,这是规范反复强调的。注意文档版本V1.11和通信版本号1.02不是一个东西:V1.11是这份文档的版本,1.02是登录报文的版本号,两者别混。体系内可以建两个会话,一个实时数据、一个重传数据,登录后各自独立。规范本意是兼容FIX标准会话协议,但特别点了一句:会话层恢复机制不能作为真正的消息恢复机制,缺失数据要靠应用层重传消息UA002。

频道心跳发送间隔3秒,由10201 ChannelNo指定频道,同时带上1350 ApplLastSeqNum最后一条行情消息记录号和10205 EndOfChannel频道结束标志。每个频道空闲时发独立心跳,心跳本身没有记录号。判断网关故障的标准是超过两倍心跳间隔没收到数据包,也就是6秒。我一般把最后接收时间戳记在频道级别,任何一个频道超过6秒没动静就触发断线重连流程。

2.4 容错与恢复:断开重连走应用层补数,别指望会话层

会话层恢复是给FIX兼容用的,真正的补数是重传消息UA002。这里有个普遍误用,我见过不止一个团队用会话层序列号判断行情丢没丢,最后对不上账。逐笔行情唯一可靠的完整性依据是频道内的消息记录号,记录号从1递增,出现跳号才代表丢失。断线重连之后,先等新数据流回暖,再根据记录号缺口发起重传请求。实时数据会话和重传数据会话是两条独立TCP连接,重传请求走9130,网关按到达顺序排队处理。所以批量重传请求要控制并发,别一口气塞几十个,一个没处理完不会处理下一个。

3. 行情数据类别与关键字段:快照、逐笔、公告怎么选频道

3.1 频道规划:从代码区间看懂深交所的行情布局

表3-1的频道表信息量很大。市场实时状态和证券实时状态在频道1,公告走频道2。10是深交所指数与成交量统计指标快照,11是国证指数快照。101x到105x按品种分开:股票、基金、可转债、权证、期权,x是0到9的数字,说明股票这种大流量品种用了一整段频道。逐笔行情对应201x-205x,与快照的品种段一一对应。盘后定价大宗交易快照在300x,盘后定价交易快照在301x,债券分销在3021。债券质押式回购快照106x、匹配成交逐笔206x,债券现券快照107x、匹配成交逐笔207x。401x是报价与大额逐笔通道,包含债券匹配大额逐笔申报及成交、意向申报、点击成交报价及成交、询价成交及协商成交,1.11版本还加入了竞买委托和竞买成交。5000是信息商的用户信息报告,5001是港股实时行情。

从这个频道表能得出一个选型结论:做A股量化,最少要订阅101x加201x,再加上频道1的实时状态;做债券就要看106x、107x和206x、207x。每个行情网关可以配置只接收某些频道,登录前要确认网关侧给你开了哪些频道的权限,收不到数据先查这个。

3.2 快照行情消息:MDStreamID、MDEntryType与TradingPhaseCode

快照行情消息的标准消息类型是W,STEP层带10201频道代码,FAST层里MDStreamID标识行情类别。从修订历史看,MDStreamID的取值持续在扩:2016年加了港股实时行情630,2020年加盘后定价交易类别,2021年又加债券现券交易业务行情快照和竞买行情。快照行情是定时发布、不能重传的,每类行情有各自的发布频率,所以同一个频道里不同MDStreamID的数据到达节奏也不一样,客户端要做时间戳对齐,而不是假设同时到达。

MDEntryType是行情条目类别,也就是盘口上具体有什么条目。规范演进里顺序加了加权平均价(9)、加权平均价涨跌BP(xj)、昨收盘加权平均价(xk)、按盘价(xh)、参考价(xi),2020年港股开市前时段又加了买盘上限价xr、买盘下限价xs、卖盘上限价xt、卖盘下限价xu。传统Level2的五档买卖盘在这些类别里只是最基础的。我的习惯是解码器维护一张MDEntryType映射表,表里没有的类别走默认分支忽略并计数,而不是直接抛异常,这样新增行情条目不会把整个程序打挂。

TradingPhaseCode是产品所处交易阶段代码。规范修订里,第0位增加过“A=盘后交易”,后来加过“波动性中断(V)”。这个字段是量化风控的重要输入:盘中看到V,说明触发了波动性中断,这时候不能按正常流动性模型下单。我把TradingPhaseCode变化当事件处理,任何非连续交易状态的切换都强制触发一次策略状态复位。

3.3 逐笔委托、逐笔成交:UA201/UA202消息流与记录号对账

逐笔委托消息的消息类型是UA201,逐笔成交是UA202。记录号规则是核心:消息记录号在一个频道内从1开始顺序递增,出现跳变就说明消息丢失,客户端通过UA002申请补传。具体判断是,收到的记录号小于等于本频道已收到的最大记录号,直接忽略;收到的记录号大于最大记录号加1,比如最大是10来了个12,那11就丢了,要补。每个频道的逐笔消息发送完毕后,网关会发频道结束消息EndOfChannel,标记一个周期结束。心跳消息没有记录号。

逐笔委托消息还有联系人Contactor(tag=10184)和联系方式ContactInfo(tag=10185),这两个是1.00β之后加的,比较冷门,做券商内部系统对接时偶尔会用到。另外1.07版本开始,逐笔行情支持两种不同的发送模式,具体模式配置在网关侧,客户端要能识别通道上实际是哪种模式,再决定怎么组装逐笔委托和成交的关联关系。这块不要做假设,以实际收到的消息流为准。

3.4 证券实时状态消息:SecuritySwitchType开关类别的用法

证券实时状态消息走频道1,带一组安全开关。修订历史里能看到开关类别的演进:表决权、股票质押式回购、备兑开仓、做市商报价、港股通整手买、整手卖、零股买、零股卖、34回售撤销、35转融通出借、36债券回售转售,中间还删除过18转股撤单、19回售撤单。开关的变化对策略影响很直接,比如做期权备兑的策略看到备兑开仓开关变化,就要重新校验持仓对应的标的券状态;港股通策略要看整手零手买卖的开关。需要特别注意:你不关心的开关类型发生变化,系统要能自动忽略,这也是规范兼容性要求的一部分。

公告消息(B)走频道2,每个公告文件有唯一ID。新公告由网关主动推给VSS,登录前的丢失公告要通过重传消息先申请公告概要,概要里是所有已发布公告ID,再逐个补缺失的。规范建议登录完成后立即申请公告概要,这个建议值得写死进默认流程,因为公告里可能就藏着当天交易安排变化的关键信息。

4. 接入实战:从TCP建连到FAST解码的VSS客户端框架

4.1 连接与登录:socket、超时和Logon构造

先把最基础的连接代码搭起来。

import socket import struct def connect_mdgw(host: str, port: int = 9129, timeout: float = 10.0) -> socket.socket: s = socket.create_connection((host, port), timeout=timeout) s.settimeout(3.0) # 心跳3秒一次,读超时低于3秒容易误判 return s

代码逻辑说明:create_connection负责TCP握手,超时10秒给首次建连的宽限。连接建立后把读超时压到3秒,是为了配合心跳节奏:每3秒至少应该有应用层数据进来,读超时触发后不一定是断线,但说明链路异常,要进重连流程。这里的关键参数是3秒,网关负载高或链路抖动时我一般放宽到4秒再观察,但绝不能放到30秒,否则网关重启后你还在等数据。

登录消息按轻量级STEP会话层规范构造,核心是把DefaultApplVerID设为1.02。代码示意:

def build_logon(sender_comp_id: str, target_comp_id: str = "MDGW", heart_bt_int: int = 30, appl_ver_id: str = "1.02") -> bytes: tags = [ ("35", "A"), # MsgType=Logon ("49", sender_comp_id), # SenderCompID ("56", target_comp_id), # TargetCompID ("108", str(heart_bt_int)), # HeartBtInt ("DefaultApplVerID", appl_ver_id), # 通信版本号,上线前替换为数字tag ] body = "".join(f"{k}={v}\x01" for k, v in tags).encode() return struct.pack(">H", len(body)) + body

逻辑说明:这里用可读字段名代替数字tag,真实实现要按STEP规范的标签表换算成数字。返回的包是“两字节长度+消息体”的帧结构,两字节长度是STEP分帧常见做法,也可以按运行环境调成4字节,但要在连接参数里和网关对好。HeartBtInt我习惯给30秒,行情网关对会话层心跳的容错范围在轻量级STEP规范里有约定,登录后的应用层心跳是UA001,和会话层HeartBtInt不是一回事,别重叠配置。

4.2 收包循环:长度分帧、消息分发、队列隔离

收包循环做三件事:拼帧、解STEP层、分发FAST解码。

class MdgwClient: def __init__(self, sock: socket.socket): self.sock = sock self.buf = b"" self.queue = queue.Queue(maxsize=10000) def recv_loop(self): while True: chunk = self.sock.recv(65536) if not chunk: raise ConnectionError("连接被对端关闭") self.buf += chunk while len(self.buf) >= 2: msg_len = struct.unpack(">H", self.buf[:2])[0] if len(self.buf) < 2 + msg_len: break step_msg = self.buf[2:2 + msg_len] self.buf = self.buf[2 + msg_len:] channel_no, raw_data = parse_step(step_msg) self.queue.put((channel_no, raw_data))

逻辑说明:外层循环把TCP流切成一帧帧STEP消息。内层while处理一个chunk里可能残留的多帧。parse_step返回ChannelNo(tag=10201)和RawData(tag=96),这样解码器能知道这条FAST数据属于哪个频道。queue容量设10000是给快照和逐笔做背压,满了之后丢弃旧策略比无限堆积安全,宁可丢弃触发重传,也比内存撑爆被网关断线强。

def parse_step(step_msg: bytes): # 简易解析:顺序找tag=10201和tag=96 d = parse_tags(step_msg) channel_no = int(d.get("10201", 0)) raw_len = int(d.get("95", 0)) raw_data = d.get("96", "") if len(raw_data) != raw_len: raise ValueError("RawDataLength与实际数据长度不符,检查分帧逻辑") return channel_no, raw_data

注意:RawDataLength必须和实际长度一致,这一行校验能拦截掉大部分分帧错位的问题。我遇到过一版代码把长度多算了2字节,结果FAST解码器整天报错,靠这条校验才定位到。

4.3 FAST解码:模板ID分发、前值字典重置、静默忽略未知字段

FAST解码器推荐的落地方式是用XML模板驱动,模板ID从999字段取。

class FastDispatcher: TEMPLATES = {3001: HeartBeat, 3002: ResendMsg, 3003: UserInfoReport} def __init__(self): self.decoder = FastDecoder() # 加载协议模板文件后实例化 def decode_raw(self, channel_no: int, raw: bytes): # 一个RawData体里可能有多条FAST消息,循环解 for fast_msg in self.decoder.decode_all(raw): template_id = fast_msg.template_id handler = self.TEMPLATES.get(template_id) if handler: handler(channel_no, fast_msg).run() else: # 未知模板统一跳过,只计数 stats.record_unknown_template(template_id)

这里的关键点是TEMPLATES字典只注册你关心的消息类型,没注册的模板ID走else分支静默跳过。规范兼容性要求里明确说过,新增消息类型对不改动的VSS要能自动忽略,这里就是落到实处的位置。解码器每次处理一个新的RawData前要重置字典前值,这个不能在handler里做,必须放在decode_raw入口:因为一个RawData字节块是一个完整的FAST消息流,块与块之间的前值字典不允许串。

4.4 参数与线程模型:快照和逐笔的消费隔离

参数项推荐值说明
实时端口9129默认实时数据端口
重传端口9130仅网络版提供,现场版无此端口
心跳间隔3秒频道心跳UA001
断线判据6秒无应用层数据两倍心跳间隔
读超时3-4秒配合心跳节奏
队列上限10000条超限走丢弃+重传

线程模型上,我强烈建议接收线程只做分帧和解STEP层,FAST解码可以放解码线程,业务消费再隔一层。快照频道和逐笔频道的处理要分开:逐笔量级大,解码慢一点就会堵心跳;快照又不能重传,一旦被逐笔堵住,当天都别想拿到干净的快照数据。

5. 避坑指南:记录号跳变、重传失败与静默兼容的5个实战问题

5.1 消息记录号跳变,补传请求还回了个“部分完成”

现象:逐笔频道收到的记录号从10直接跳到12,发起UA002请求补11,结果网关返回ResendStatus=2(部分完成),11还是没拿到。

原因:逐笔消息在网关内存里的保留窗口有限,超出窗口的旧数据已经被清理。记录号跳变的实时位置离请求时间越久,补回概率越低。

解决:检测到跳号立即发起重传请求,不要攒批。同时把ResendStatus=2当成常规事件处理,记日志而不是直接告警。如果频繁补不回来,回查自己的收包线程是不是在高峰时段被GC或日志拖慢。血泪经验是:逐笔补传的黄金窗口很短,请求越快,补回来的希望越大。

5.2 登录成功但收不到指定频道,查权限而不是查网络

现象:TCP连接建好了,Logon也回了成功,但101x股票快照频道一直没有消息。

原因:行情网关按配置下发频道,你的账号或网关这一侧没开这个频道的权限。规范写了“每个行情网关可以配置只接收某些频道的交易行情数据”,指的就是这个。

解决:联系网关管理员确认频道权限配置。我见过不少团队在网络层、解码层折腾半天,最后发现是频道没开。建议登录后加一个频道清单自检流程,把期望频道的ChannelNo列成配置,启动时和实际收到的心跳对一遍:有心跳就有数据,没心跳先查权限。

5.3 心跳一直有,但行情数据停更,界面显示“已连接”很误导

现象:频道心跳3秒一次很规律,但快照的W消息和逐笔的UA201/UA202都不动了,监控面板还显示连接正常。

原因:心跳是每个频道空闲时独立发送的,它只证明TCP链路和你订阅的频道还活着,不证明业务消息在持续生产。网关侧某个业务模块卡住时,心跳照样发。

解决:监控里把“最近一条业务消息时间”和“最近一条心跳时间”分开统计。我现在每个频道统计行情消息到达间隔,超过该频道发布周期的3倍就报警。判断网关是否故障用两倍心跳间隔,判断业务是否停摆用该频道的正常发布周期,这两个指标不能混用。

5.4 新增MDEntryType导致解码器崩溃,违反兼容性要求

现象:某天行情正常,突然某个快照包让解码器抛出未知字段异常,处理线程退出,后续数据全断。

原因:解码器用硬编码的字段枚举表,遇到新增的行情条目类别比如加权平均价(9)这类新值直接抛错,没有走忽略逻辑。

解决:规范第四条兼容性要求写得很清楚,快照或逐笔新增MDEntryType,不关注这些新条目的系统应能自动忽略。实现上把MDEntryType解析改成通用map结构,键值对读出来,不需要的类别直接丢弃。后来我为每个未知字段类型单独计数并定期输出统计,这样既不丢数据,又能感知交易所新增了什么类别。

5.5 现场版网关没有重传端口,补数方案完全失效

现象:接入环境从网络版换到现场版后,UA002重传请求全部石沉大海,日志里永远等不到重传应答。

原因:现场版使用单向卫星通信,没有数据重传功能,规范明确“只有网络版行情网关提供重传服务端口”,所以9130端口根本不存在。

解决:接入前先确认环境是现场版还是网络版。现场版环境下,逐笔缺失只能靠重建会话后的新数据流自然补齐,快照本来就不能重传。我的对策是在策略层做容错:逐笔成交丢失时降低高频策略的仓位上限,用快照的五档挂单作为降级信号,别让策略在数据残缺状态下裸跑。

6. 进阶用法:拿修订历史反推解码器兼容性清单,预研策略不再怕新字段

6.1 把修订历史变成解码器回归用例

规范的修订历史其实是最实用的一张兼容性图纸。V1.00到V1.11,每一次更新都对应一类新数据字段或新交易场景:1.01加港股通开关和港股实时行情630;1.02加加权平均价、昨收盘加权平均价;1.04加国证指数行情;1.06加盘后定价大宗交易;1.07支持港股开市前时段,新增xr/xs/xt/xu四类MDEntryType;1.08加参考价;1.09加债券现券行情和逐笔占位消息;1.11再加竞买行情。把这条演进线整理成一张表,就是解码器的兼容性验证清单。

我的做法是每收到一份新版本规范,先拉一次修订差异,把新增的MDEntryType、SecuritySwitchType和频道代码追加到一个测试用例文件里,然后用这些用例喂解码器,验证它走的是“静默忽略”分支而不是异常分支。

新版字段对应版本验证动作
港股实时行情6301.01快照模板加MDStreamID分支
加权平均价9/xj、昨收盘加权平均价xk1.02快照条目忽略测试
港股开市前xr/xs/xt/xu1.07条目解析不做硬编码
债券现券107x/207x频道1.09频道心跳覆盖测试
竞买委托与成交1.11逐笔消息类型扩展测试

这个技巧的价值不在于测试本身,而在于让接入系统的每次升级都有据可依,不用等上线了才被新行情类别打个措手不及。另外还有一个很实际的用法:把修订历史当作策略研究的风向标。交易所新增哪类行情,往往意味着这个方向在交易制度和市场服务上有变化。盘后定价交易、债券现券行情、港股开市前时段,都是先出现在接口规范里,再逐步成为策略圈讨论热点的。提前把解码器兼容性做扎实,等这些数据真正成为策略标的时,你的数据管道已经准备好了。

现在每拿到新版规范,我第一件事就是让解码器跑一遍新增字段用例,跑通之后再谈业务逻辑。这套习惯帮我把上线前的深夜电话挡掉了一大半,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询