☰
深交所Level2行情接入:STEP协议与FAST解码全解析
2026/10/18 12:09:58 网站建设 项目流程

简介:深交所Level2行情数据接口规范V1.11是深圳证券交易所发布的最新行情数据接口标准,面向高频交易、量化交易及涨停板交易等追求精确行情的专业人士,系统定义了Level2与STEP格式的行情数据获取和处理规则。文档覆盖集中竞价、盘后定价交易、期权、港股通、债券现券等复杂业务场景,并明确了快照行情、逐笔委托、证券实时状态等消息的字段与频道代码,对用户行情系统的扩展兼容也给出了具体要求。资源为1个PDF文件,压缩包约682KB,内容包含完整的接口修订历史、会话机制、行情类别定义及数据格式说明,可作为行情系统开发与策略研究的一手参考资料。该规范自2013年发布后历经多轮优化更新,当前已有1992人学习,适合从事低频与高频量化、券商接口对接以及交易所数据研究的工程师深入研读。

1. 深交所 Level2 行情接入第一步:先读懂 STEP 接口规范 V1.11

做量化的人第一次接深交所 Level2 行情,十有八九以为拿到授权之后就是开 TCP 端口收数据,结果动手才发现:真正的门槛不是网络编程,而是那一堆 FAST 模板、频道代码和 MDEntryType。我接过几次这套行情,从裸报文到能稳定解析出逐笔委托,每一步都绕不开这份《深交所 STEP 行情数据接口规范 V1.11》。它定义了行情网关 MDGW 和用户系统之间的会话机制、消息结构、频道划分以及逐笔行情重传逻辑,属于那种「看着是标准文档,实际是开发蓝本」的东西。本文按接入开发的顺序拆这份规范,把端口参数、消息结构、重传机制和容易翻车的细节一次讲清,适合准备做 L2 行情接入的量化开发、高频交易团队和信息商系统工程师。

2. 协议选型与消息结构:STEP+FAST 这套组合到底怎么拆

2.1 三层协议的分工逻辑

深交所这套行情接口不是裸 TCP 报文,而是遵循 STEP 1.2 和 FIX 5.0 SP2 协议规范,并在上面叠加了 FAST 编码。很多第一次接触的人会被这三层绕晕,其实分工很清晰:

  • STEP(Securities Trading Exchange Protocol)负责会话层,管登录、心跳、连接恢复这些事,协议基础是 FIX 的扩展。
  • FAST(FIX Adapted for STreaming)负责消息体的编码压缩,把行情字段用模板方式编码,减少带宽占用。
  • 应用层消息则在 STEP 消息层里封装,每条消息的载荷是 FAST 编码后的二进制。

所以收到一个行情包时,拆包顺序是:先按 STEP 消息层格式取出标准头、ChannelNo(频道代码)、RawDataLength 和 RawData,然后再把 RawData 里的 FAST 字节做解码,拿到真正可读的行情字段。

2.2 FAST 模板 ID 的编码区间含义

规范里明确给出了 FAST 模板 ID 的编码规则,这个在做解码器初始化时直接用得上:

模板 ID 区间用途
3000 - 3999公共消息
4000 - 15999实时行情数据

公共消息里包括频道心跳、重传消息、用户信息报告、快照行情频道统计、业务拒绝消息、逐笔占位消息等。实时行情数据则是市场实时状态、证券实时状态、公告、快照行情、逐笔委托和逐笔成交这些真正的高频数据。开发时模板 ID 就是分拣消息类型的依据,收到 FAST 字节后先解码出 TemplateID,就知道该按哪套字段映射去解析。

2.3 STEP 消息层的字段拼装方式

每一条完整的应用层消息,外层结构是统一的 STEP 消息层。标准头、频道代码、FAST 消息体长度、RawData 消息体、标准尾。这里有一个细节值得注意:RawData 里可能包含本频道的多条 FAST 编码消息,而且解码每条 FAST 消息体之前,都必须重置解码器的 FAST 字典前值。这个「重置字典」的要求是个高频踩坑点,后面避坑章节再展开。

2.4 兼容性设计的五个原则

V1.11 规范在兼容性要求上做了五个明确约束,这也是这套接口能在多年迭代中保持稳定的核心:

  • 新增行情频道,不使用的系统可以不作变更自动忽略。
  • 新增应用层消息类型,不使用的系统可以不改造自动忽略。
  • 快照或逐笔行情新增行情类别(MDStreamID),不用的系统可自动忽略。
  • 行情快照消息新增行情条目类别(MDEntryType),不用的系统可自动忽略。
  • 证券实时状态消息新增开关类别(SecuritySwitchType),不用的系统可自动忽略。

这意味着解析端在做字段映射时,不能写死「认得的必须全部处理、认不得就报错」的逻辑,而是要做成白名单过滤加未知字段跳过的模式。我一般会把模板字段表做成配置驱动,新增类别时只加配置不改代码。

3. 会话机制与端口参数:9129/9130 双端口背后的接入节奏

3.1 双服务端口设计:实时与重传分离

行情网关给用户系统提供两个服务端口:实时数据端口默认为 9129,重传服务端口默认为 9130。每一个端口只能建立一个 TCP/IP 连接。而且只有网络版行情网关才提供重传服务端口,现场版使用单向卫星线路,没有数据重传功能。

这个双端口设计对系统架构的影响很直接:实时连接负责全天候收流数据,重传连接只在检测到消息丢失时才发起请求。两个连接的职责不能搞混,也不能在同一个连接上既收实时数据又发重传申请——规范里重传请求是走单独会话的。

3.2 登录参数与通信版本号

建立会话时,登录消息(Logon)的 DefaultApplVerID 字段必须填写当前版本对应的通信版本号,V1.11 接口规范对应的通信版本号是 1.02。字段填错或者缺失,登录基本会被网关拒绝。这块我踩过一次,当时拿旧项目的登录参数直接改 IP 就接新网关,报文结构全对但是版本号写的是上一版的值,网关直接断开连接,排查半天才定位到 DefaultApplVerID。

另外,行情网关和用户系统必须位于同一个安全网络中,两者之间传输的数据是非加密的。所以网络层安全要靠接入方自己的内网保障,做部署规划时得提前把防火墙策略和网段隔离方案想清楚。

3.3 流量控制与断线重连策略

行情网关和用户系统之间设有流量控制机制。如果用户系统处理速度跟不上,导致网关累积的待发送消息超过设定阈值,行情网关会直接断开连接。注意它不会降速、不会丢弃,就是硬断开。

断开之后用户系统应该立即尝试重新建立连接。规范要求在重建会话后,通过应用层的消息恢复机制获取缺失的数据,而不是指望会话层恢复。对于逐笔行情,恢复手段就是记录号检测加重传请求;对于公告,则是用公告概要比对后逐个申请补传。

3.4 会话建立与容错路径

用户系统与行情网关之间最多可建立两个会话:实时数据会话和重传服务会话。会话建立过程遵循《轻量级 STEP 会话层接口规范》。容错逻辑很直接:系统故障后重新建立会话,再走应用层恢复。

我一般在工程上会把连接管理做成独立模块,包含两个连接的状态机:

  • 实时连接断开,立即重连,重连成功后自动发送重传请求拉取断线期间丢失的逐笔数据。
  • 重传连接断开,立即重连,但要等到有重传需求时再发起请求,避免占用网关资源。

4. 行情数据解析:快照、逐笔、公告三类消息的落地要点

4.1 快照行情:MDStreamID 与 MDEntryType 的映射关系

快照行情消息是最常用的数据,频道代码为 101x 到 107x 对应不同的交易品种,其中 x 取值 0-9。股票用 101x、基金 102x、可转债 103x、权证 104x、期权 105x、债券通用质押式回购 106x、债券现券交易行情 107x。另外还有深交所指数快照行情频道 10、国证指数快照行情频道 11、港股实时行情频道 5001。

快照消息里每个行情条目都带 MDEntryType,字段几十种,比如买盘价、卖盘价、加权平均价、参考价、按盘价等。每个快照频道可能包含多个类别的行情,每类行情都有自己的发布频率。解析快照的注意点在于:不能假设每个快照包里的条目顺序相同,必须按 MDEntryType 做分发,而不是按固定偏移量取字段。

下面是快照行情消息解析时的判定逻辑示意:

def dispatch_snapshot(entry_type: int, payload: dict): # MDStreamID 决定行情类别,MDEntryType 决定行情条目 if entry_type == 0: # 买盘价 handle_bid_price(payload) elif entry_type == 1: # 卖盘价 handle_ask_price(payload) elif entry_type == 9: # 加权平均价 handle_weighted_avg(payload) elif entry_type == "xi": # 参考价 handle_reference_price(payload) else: # 新增行情条目:不关心则自动忽略,不抛异常 log_unknown_entry(entry_type)

这段逻辑的核心是:未知条目只记日志不中断,对应规范里「自动忽略新增行情条目类别」的兼容性要求。

4.2 逐笔行情:记录号跳变检测与重传请求

逐笔行情是 Level2 里最有价值的增量数据,分逐笔委托和逐笔成交两类。逐笔委托消息 UA201,逐笔成交消息 UA202。股票、基金、可转债、权证、期权的逐笔频道分别用 201x 到 205x。

每个逐笔行情消息里都带频道代码和消息记录号 ApplSeqNum。记录号在频道内从 1 开始顺序递增。收到消息后做跳变检测:如果收到的记录号大于已收到的最大记录号加 1,说明中间丢了消息。比如当前最大记录号是 10,新收到一条记录号是 12,那记录号 11 的消息就是丢了。

def check_seq_gap(channel_no: int, new_seq: int, last_seq: int) -> bool: # 跳变检测:记录号等于或小于 last_seq 视为重复,忽略 if new_seq <= last_seq: return False # 记录号大于 last_seq + 1 说明出现缺失,需重传 if new_seq > last_seq + 1: missing_from = last_seq + 1 missing_to = new_seq - 1 send_resend_request(channel_no, missing_from, missing_to) return True

重传请求通过 UA002 消息发送,关键字段包括 ResendType(1=逐笔行情数据,2=公告信息)、ChannelNo、ApplBegSeqNum、ApplEndSeqNum。ApplEndSeqNum=0 时行情网关会把它设置为收到重传请求时该频道数据在内存中的最大值。每个频道的逐笔行情发送完毕后,行情网关会发送一笔频道结束消息 EndOfChannel。

4.3 公告消息:公告概要与逐文件重传

公告消息和逐笔行情不同,每个公告文件都有一个唯一 ID。新到达行情网关的公告文件会推送给用户系统;但建立连接之前可能丢失的公告文件,只能主动申请重传。

规范给出的方式是:完成登录后立即申请重传公告概要。公告概要里包含所有已发布公告文件的 ID,拿到概要后逐个比对,发现本地缺失或版本变化的文件,再按照 ID 申请重传具体公告文件。

这一段流程如果做晚了,可能出现盘中临时公告漏收的情况。我现在的做法是在登录成功回调里直接发一个 ResendType=2、NewsID 为空的重传请求,先把概要拿回来再决定后续动作。

4.4 公共消息:心跳、频道统计与用户信息报告

公共消息里有几条容易被忽略但很影响稳定性的:

  • 频道心跳 UA001:每 3 秒发送一次,心跳消息没有记录号。用户系统如果超过两倍心跳间隔没收到任何数据包,应该判定行情网关故障,断开并重连。
  • 快照行情频道统计:每个快照频道会周期性上报统计数据,可以用来监控频道数据完整性。
  • 用户信息报告 UA003:只有信息商用户系统需要发送,固定频道 5000,上报当前连接的终端用户数目。券商自用系统不需要发。

用户信息报告的字段包括 OrigTime 数据生成时间、VersionCode(01=现场版,02=网络版)、UserNum 用户数目。如果是做信息商转发系统,这个定时上报一定记得实现,不然网关端会收不到用户量统计。

4.5 消息流维度:业务拒绝与逐笔占位

V1.11 还增加了业务拒绝消息和逐笔占位消息。业务拒绝消息用于行情网关在业务层面拒绝某些请求时返回原因,文本在 Text 域中返回。逐笔占位消息用于逐笔行情在某个时间点没有实际成交或委托数据时占位,保证序列号连续或频道时间轴完整。解析程序遇到这两类消息不要当异常处理,直接按模板 ID 分到对应逻辑分支即可。

5. 接入开发避坑指南:记录号跳变、字典重置与重连排查

5.1 现象:FAST 解码偶发乱码,重启后恢复

原因:多条 FAST 编码消息打包在同一个 RawData 里,解码器没有在每条消息前重置字典前值。FAST 协议里大量字段用 previous value 作为基准,字典状态不重置会导致字段错位、数值完全错乱。

解决:在循环解码 RawData 中的多条消息时,每次解码新消息前强制重置解码器的字典状态。代码上不要只调一次解码接口,要把重置逻辑放在循环体内。

def decode_fast_messages(raw_data: bytes): decoder.reset() # 必须:每条消息解码前重置字典 length = len(raw_data) offset = 0 while offset < length: template_id, fields = decoder.decode(raw_data, offset) handle_message(template_id, fields) offset = decoder.consumed_bytes()

5.2 现象:逐笔数据偶尔缺失但程序不报错

原因:只检查了新记录号是否大于旧记录号,没有做「大于旧记录号 + 1」的跳变检测。如果行情网关重连后重新从记录号 N 开始发送,而本地已有记录号 N,那么新到的 N 会被当成重复包忽略,但中间 N+1、N+2 的数据已经永久丢失。

解决:严格按规范走。收到记录号等于或小于本地最大记录号的直接忽略;大于本地最大记录号 + 1 的,立即发起重传请求补齐缺口。

5.3 现象:连接断开后重连,但数据一直对不上

原因:重连成功后直接进入实时接收,没有先申请断线期间的缺失数据。实时通道恢复的只是当前时刻的数据流,断线期间产生的逐笔数据不会自动补发。

解决:重连成功后的第一件事,不是等第一笔行情,而是用 UA002 发起逐笔数据重传。根据本地记录号的最大值和断线时间估算范围,或者用一个较大的 ApplEndSeqNum=0 让网关补到当前内存最大值。

5.4 现象:公告文件收不到,很久以后才发现缺失

原因:只在盘中靠推送接收公告,没有在登录后主动申请公告概要。连接建立之前推送的公告文件不会重复发送一遍。

解决:登录成功后立即发送 ResendType=2、NewsID 为空的 UA002 消息申请公告概要。收到概要后建一个本地公告 ID 列表,与概要比对,缺失的逐个按 NewsID 申请重传。规范的措辞是「建议完成登录动作后立即申请」,我实际用下来这应该是强制项而不是建议项。

5.5 现象:行情网关没发数据,本地却迟迟不重连

原因:把「没有任何数据包」和「没有行情数据」混为一谈了。频道空闲时会有独立的心跳消息,只要连接还活着,心跳就会持续到达。超过两倍心跳间隔(即 6 秒)完全没有数据,才说明网关侧可能出了问题。

解决:监控模块以「最后收到任意消息的时间」为基准,超过 6 秒无消息才触发断开和重连逻辑。不要用「最后收到行情数据的时间」做判断。

5.6 现象:现场版行情网关重传请求无响应

原因:现场版使用单向卫星通信,没有地面网络回传通道,因此没有重传服务端口 9130,自然不支持重传。重传请求在这种情况下是无效的。

解决:接入前先确认自己用的是现场版还是网络版。现场版接入方案要考虑数据完整性策略,一般做法是加一个备用地面网络通道交叉验证,或者靠快照行情做全场校准。V1.11 规范的修订历史里多次提到两版差异,接入前这一项必须先确认。

6. 把说明书翻译成行情系统:模板 ID、心跳监控与升级习惯

规范最终要落到工程实现上才算数。我做了几套 Level2 行情接入之后,沉淀下来几个固定的处理习惯,分享出来供参考。

模板 ID 的注册机制是解码端的地基。FAST 模板消息种类几十个,不同频道同名消息还有消息层和业务层的区别。我一般把模板 ID 做成一张注册表,每条消息类型绑定一个处理函数,新增模板只注册不修改既有逻辑。收到一个新模板 ID 时,如果注册表里没有对应处理函数,就记未知消息日志,但绝不能抛异常拖垮整个接收线程。这一条和规范里的兼容性要求完全一致,做成机制之后后续版本升级基本不用改主流程。

心跳监控的粒度建议做到频道级别,而不是连接级别。每个频道每 3 秒发送一次心跳,连接的健康状态其实由频道心跳兜底。连接层断了有 TCP 保证,但频道静默才是真正危险的状态——连接还在,数据却停了。所以我每个频道单独维护一个 last_recv_time,每秒巡检一次,超过 6 秒未收到该频道任何消息就告警并触发重连。这个监控成本很低,但能兜住大多数网关侧的隐性故障。

订阅过滤的配置化也值得提前做。频道代码区间那么多,一个系统不一定全要:股票逐笔、期权逐笔、港股快照、公告频道、国证指数,各取所需。把订阅列表做成配置文件,解析器只管按频道代码路由,需要新增一个分类时只改配置不改代码。

字段映射的兼容层设计,是 V1.11 修订历史里反复强调的事。这些年行情规范经历了:证券实时状态消息的字段替换、逐笔委托消息删除三个字段、盘后定价大宗交易更名、新增债券现券交易行情、新增债券竞买行情。这些变化如果不做前向兼容,每次版本升级都要折腾一次全量回归。我现在的做法是:所有 FAST 字段映射通过模板描述文件(XML 或 JSON)驱动,解析器在运行时加载字段定义,而不是在 Python 或 C++ 代码里硬编码字段 tag。这样新版本出来后,只需要更新模板描述文件,代码基本不动。

最后说一个让我吃过亏的习惯。每次拿到新版本规范,我先不看正文,而是先翻修订历史。V1.11 相比 V1.10 增加了债券现券交易业务竞买行情,相比 V1.09 增加了债券现券交易业务快照和逐笔行情、调整了债券相关行情频道、增加了逐笔占位消息。这些变更对应的是新增模板、新增频道、新增消息类型,我的系统必须先知道哪个模板 ID 会变、哪个频道代码会新增,再去做兼容层适配。从那以后,我每次拿到新版本规范都强制自己先过一遍修订历史,再逐章核对模板定义和频道说明,这份 V1.11 的接入蓝本就是这样一点点啃下来的。希望帮到你。

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

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

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

立即咨询