☰
深交所Level2行情接入:STEP协议、FAST解码与记录号重传全解析
2026/10/18 10:18:37 网站建设 项目流程

简介:《深交所Level2行情数据接口规范V1.11》是由深圳证券交易所发布的官方技术标准,面向高频交易、量化策略及Level2行情接入开发人员,提供STEP协议下完整的数据接口定义与通信规则。资源为1个PDF文档,压缩包总大小682KB,内容涵盖修订历史、会话机制、行情类别、行情条目及开关类别等章节,便于查阅。该规范发布至今已有1992人浏览学习,是接入深交所Level2行情的重要参考。文档详细梳理了2013年至2021年多个版本的更新要点,包括快照行情、逐笔委托、盘后定价交易、港股通、期权备兑、债券竞买等新增消息与字段,并明确接口兼容性要求,帮助开发者快速理解规范变更、正确适配行情解析程序。对于需要精确盘口数据的交易系统开发而言,这本规范能有效降低对接门槛,提升开发效率。

1. 深交所Level2行情接入:会话、频道与FAST消息三层结构一次拆透

做行情接入的开发者多半有过这种经历:拿到接口规范,第一反应是翻快照行情字段表,找五档盘口的Tag。真正连上行情网关才发现,卡住你的根本不是盘口字段,而是会话层的登录版本号、FAST模板的解码方式、记录号跳变后的重传逻辑。这份《深交所Level2行情数据接口规范V1.11》把这三件事一次性讲完了:它定义了VSS(接入用户行情系统)通过STEP协议接入行情网关MDGW的完整链路,覆盖集中竞价、盘后定价、债券、期权、港股实时行情在内的快照与逐笔数据。适合正在做量化行情、高频落地或给信息商做分发系统的开发团队,按着规范能把接入流程跑通,也能把解析链路拆明白。

2. 整体架构:行情网关、频道代码与消息嵌套

一份行情接入规范的骨架,其实只回答三个问题:数据从哪来、按哪些通道来、到了以后长什么样。V1.11版在三个问题上分别给出的答案是:行情网关MDGW、频道代码ChannelNo、STEP消息层包FAST消息体的两层嵌套。这三层理顺了,后面所有消息定义都能对号入座。

2.1 两个端口两个会话:实时与重传分离的设计

规范把行情网关分成现场版和网络版两种形态。现场版走单向卫星线路,没有数据重传功能;网络版走地面网络,具备数据重传能力。两种网关都给用户提供两个服务端口:实时数据端口默认9129,重传服务端口默认9130。每个端口只能建立一个TCP/IP连接,所以一个VSS与行情网关之间最多保持两个会话:一个实时数据会话,一个重传服务会话(重传服务会话只有网络版才有)。

这个设计第一眼看上去平平无奇,真正跑起来才能体会到实时与重传分离的用意。实时会话只负责推送行情,不要在它上面塞重传请求;补数据的请求全部走第二个会话,这样哪怕重传量大,也不会阻塞实时链路,盘中瞬时行情高峰时实时链路能始终保持轻载。另外要留意,实时链路和重传链路是两条独立的TCP连接,需要分别做心跳和断开重连处理,不能只盯着实时连接。

流量控制机制也在连接这一节里做了明确约定:如果VSS处理不过来,网关侧累积的待发送消息超过设定阈值,行情网关会立即断开连接。也就是说行情接入不是"纯读",消费速度跟不上一样会被踢下线,断开之后VSS要主动尝试重新连接。这是接入阶段最容易翻车的地方,后面避坑章节会再展开。

2.2 频道代码体系:一张表看懂1到5001都发了什么

交易行情数据按业务内容分成多个类别,每个类别根据数据量大小可能用一个或多个频道发送,每个频道用ChannelNo标识。V1.11版里频道代码已经铺到5001,我把主要区间整理成一张表:

频道代码区间频道内容
1证券实时状态消息(f)、市场实时状态消息(h)
2公告消息(B)
10 / 11深交所指数与成交量统计快照 / 国证指数快照
101x-105x股票、基金、可转债、权证、期权的集中竞价快照行情
201x-205x对应品种的集中竞价逐笔委托(UA201)与逐笔成交(UA202)
300x / 301x盘后定价大宗交易快照 / 盘后定价交易快照
3021债券分销快照行情
400x综合金融服务逐笔行情
106x/206x、107x/207x债券质押式回购、债券现券的快照与匹配成交逐笔
401x报价及大额逐笔,含竞买委托与竞买成交
5000 / 5001用户信息报告 / 港股实时行情快照

表里的x是0到9的数字,也就是说光股票集中竞价快照就预留了1010到1019十个频道,方便按数据量扩容。VSS按需订阅,行情网关可以配置成只接收某些频道,跟业务无关的频道根本不用建解码器。对接的第一步,是把频道列表和自身业务对齐:只做股票量化的,订阅101x和201x就够;涉及债券的,还要把107x、207x、401x纳入;做港股的再单独挂5001。

2.3 FAST消息体:模板ID决定解析方式的嵌套结构

规范里定义的所有应用层消息,都是STEP消息层加FAST消息层两层嵌套。STEP消息层的结构非常固定:标准消息头(含MsgType)、ChannelNo(tag 10201)、RawDataLength(tag 95)、RawData(tag 96)和标准消息尾。RawData即FAST消息体,里面可能装的是本频道的多条FAST编码消息。FAST编码消息的第一个字段基本是TemplateID(tag 999),它决定后续字段按哪套模板解释。

模板ID的编码区间分成两块:3000到3999是公共消息,4000到15999是实时行情数据。解码器拿到FAST字节流,第一步是解出TemplateID,然后按对应的模板定义做字段解码。这里必须注意规范里的一句话:解码FAST消息体前应该重置解码器的FAST字典前值。FAST协议是依赖前值的流式压缩协议,同一条消息里的字段可能引用前一条消息的数值做增量编码,前值一旦串了,解出来就是错数据。

我在工程里一般按频道建独立的解码器实例,每个频道一个,互不共享字典,进入新的RawData时先调用解码器的重置操作。代码骨架长这样:

class FastDecoder: def __init__(self, template_map): self.templates = template_map self.previous = {} def reset(self): # FAST 字典前值必须在每个 RawData 之前重置 self.previous.clear() def decode(self, raw_data: bytes): template_id = self.read_int(raw_data, tag=999) if template_id not in self.templates: return None # 未知模板按兼容性要求直接忽略 return self.templates[template_id].decode(raw_data, self.previous)

这个骨架的核心处理是reset和read_int。reset清掉前值表,保证每条RawData内的FAST消息从干净状态开始解;read_int在读模板ID之外还要负责把增量字段跟前值做合并。实际开发里模板定义从FAST模板文件生成,但每个频道一个解码器实例这个习惯,比任何优化都值得先养成。它能让频道之间的前值彻底隔离,排查脏数据问题的时候,至少能快速排除"串频道"这个方向。

3. 核心消息拆解:从心跳、快照到逐笔的字段级解读

V1.11把消息分成公共消息和实时行情两大类。公共消息是管道设施,负责心跳、重传、统计上报;实时行情才是业务数据的主体,分快照和逐笔两条线。这两类消息的字段设计逻辑完全不同,拆开看更清楚。

3.1 公共消息:心跳、重传与用户信息报告

频道心跳(UA001,TemplateID=3001)是公共消息里最容易被忽视的一个。它每3秒发一次,携带三个关键信息:ChannelNo(哪个频道在心跳)、ApplLastSeqNum(该频道最后一条行情消息的记录号)、EndOfChannel(频道结束标志)。EndOfChannel出现时表示这一轮消息发完了,VSS可以借此判断一个频道的消息边界。

重传消息(UA002,TemplateID=3002)是整个会话机制里最核心的公共消息。VSS发起重传请求,网关按请求到达顺序依次处理,处理完成后同样用重传消息应答。请求里的ResendType区分两类:1表示逐笔行情数据,2表示公告信息。逐笔行情时用ApplBegSeqNum(tag 1182)和ApplEndSeqNum(tag 1183)指定记录号范围,结束序号填0表示让网关填当前内存中的最大值;公告信息时用NewsID(tag 1472)申请指定公告文件,NewsID为空则表示申请公告概要。网关应答时会带ResendStatus:1完成、2部分完成、3无权限、4数据不可用,失败原因在Text域里返回。

重传请求的FAST消息体组装,按文档字段可以这样写:

def build_resend_request(channel_no: int, beg_seq: int, end_seq: int) -> bytes: msg = FastEncoder() msg.add_field(999, 3002) # TemplateID = 3002 重传消息 msg.add_field(10077, 1) # ResendType = 1,逐笔行情 msg.add_field(10201, channel_no) # ChannelNo 目标频道 msg.add_field(1182, beg_seq) # 起始记录号 msg.add_field(1183, end_seq) # 结束记录号,传 0 表示拉到底 return build_step_envelope("UA002", channel_no, msg.bytes())

build_step_envelope负责包STEP消息头尾,里面要填MsgType、10201、95和96四个核心域。重点说一下参数语义:beg_seq是已收到的最大记录号加1,end_seq一般填0,让网关按当前内存最大值返回,这样一次请求就能补全所有缺口,不用自己算结束位置。ResendType和Range字段只在对应场景生效,组装时别混填,比如公告重传不需要序号范围,填了反而可能被网关拒绝。

用户信息报告(UA003,TemplateID=3003)只对信息商VSS有要求:必须定时向网关上报当前连接的客户端数目,ChannelNo固定5000。字段包括OrigTime(数据生成时间)、VersionCode(01现场版、02网络版)、UserNum(当前连接的终端用户数)。量化团队自己用不涉及这个约束,但如果做的是行情分发服务,这个上报不能省。

3.2 快照行情:行情类别、条目类别与交易阶段代码

快照行情消息(MsgType=W)是Level2数据的核心载体。一次快照里,先由MDStreamID标记行情类别,比如集中竞价交易行情、盘后定价交易行情、港股实时行情(630)、国证指数行情;再由MDEntryType标识具体的行情条目。规范反复强调:新增行情类别、新增条目类别时,VSS如果不关心,应该能自动忽略、不做改造。

条目类别一直在扩充:先有买卖档位的0、1、2、3,后续版本陆续加了按盘价(xh)、参考价(xi)、加权平均价(9)、加权平均价涨跌BP(xj)、昨收盘加权平均价(xk),2020年又为港股开市前时段增加买盘上限价(xr)、买盘下限价(xs)、卖盘上限价(xt)、卖盘下限价(xu)。解析器里维护一张条目类别字典,新版本上线后主要是往字典里追加映射:

ENTRY_TYPES = { "0": "bid_price", "1": "offer_price", "2": "bid_size", "3": "offer_size", "9": "vwap", "xh": "nominal_price", "xi": "reference_price", "xj": "vwap_bp", "xk": "prev_close_vwap", "xr": "hk_bid_limit", "xs": "hk_bid_floor", "xt": "hk_offer_limit", "xu": "hk_offer_floor", }

这只是一张映射表,实际解析时还要按MDEntryType从FAST消息里读出对应数值。注意一个细节:同一档价位和数量在FAST编码里可能是分开的条目,不要假设"五档报价"会以一个结构体整体出现,要根据MDEntryType逐条还原买卖盘口。交易阶段代码TradingPhaseCode的第0位有特殊约定:A表示盘后定价交易,V表示波动性中断。判断一只证券当前能不能成交,先看这个位,再决定快照数据怎么用。

证券实时状态消息(MsgType=f)走1号频道,里面用SecuritySwitchType开关类别描述证券状态。开关类别从早期的基础开关一路加到36-债券回售转售,中间还经历过删除,比如2015年删除转股撤单、回售撤单,2020年删除实时分拆、实时合并,新增34-回售撤销、35-转融通出借。对接时不要把这些开关写死在业务代码里,状态开关是增删最频繁的字段,按字典维护比按硬编码分支维护省心得多。

3.3 逐笔行情:记录号、发送模式与港股开盘前扩展

逐笔行情分逐笔委托(UA201)和逐笔成交(UA202)两条消息,另外还有逐笔占位消息,用于在逐笔数据流中占位,保证记录号连续。逐笔行情的核心机制是记录号:每个频道内消息记录号从1开始顺序递增,如果出现跳号,说明消息丢了,要通过重传服务补。收到记录号小于等于本频道已收最大记录号的消息,说明重复,直接忽略;收到记录号大于已收最大记录号加1,说明中间有缺口,触发重传请求。

逐笔行情支持重传,这是它和快照最重要的区别。快照定时发布、不能重传;逐笔要保证链路完整,所以每个逐笔行情消息都带所属频道代码和记录号,补数的最小单位就是一条消息记录。2020年8月的版本还增加了"逐笔行情支持两种不同的发送模式",对接生产环境前必须先确认当前网关用的是哪种模式,否则解析出来的委托和成交消息可能对不上。

港股开市前时段的逐笔和快照在V1.11里都有专门扩展,估值时段的买卖盘价格用xr、xs、xt、xu四个条目类别区分上下限价,和连续竞价时段的常规档位是两套语义。解析港股行情时,要先判断当前处于开市前还是连续竞价阶段,再选择对应的MDEntryType解释方法,比如把xr当成买一价去挂单,实际它是开市前买盘上限价,逻辑完全不同,混用会把价格方向搞反。

4. 接入实战:登录建连、消息恢复与兼容性边界

看完消息结构,下一步是把流程串起来。V1.11版接入的完整闭环包含三件事:登录建立会话、用记录号做消息恢复、以及按兼容性要求保证升级不动现有系统。

4.1 登录与通信版本号:DefaultApplVerID别填错

每个VSS与行情网关之间最多建立两个TCP连接,IP和端口由网关分配。登录时消息头里的DefaultApplVerID要填当前规范对应的通信版本号1.02。这个字段对应的是会话层规范,不是文档本身的V1.11版本号,版本号填错是最常见的登录被拒原因,接的时候特别容易把两者搞混。

登录成功后会进入正常的数据接收状态。实时会话和重传会话都要维护心跳,心跳间隔3秒,超过两倍心跳间隔没收到任何数据包,基本可以判断网关侧已经出问题,VSS应该主动断开并尝试重连。这里"任何数据包"指的不是业务消息,而是包括心跳在内的所有消息,有的实现只统计业务消息,会把正常空闲频道的连接误判成断线。

4.2 应用层消息恢复:记录号比对与重传触发

规范里有一句话值得反复读:会话层的恢复机制仅仅是为了与标准FIX会话协议兼容,不能作为真正的消息恢复机制使用。也就是说TCP层重连之后,缺失的行情数据要靠应用层重传协议自己补,不能指望会话层。快照数据本身不能重传,需要重传的是逐笔行情和公告文件。

逐笔行情的补数逻辑是:维护每个频道已收最大记录号,新消息到达时比对。如果收到的记录号小于等于已收最大值,说明重复,丢弃;如果记录号大于已收最大值加1,比如最大值是10、新来的记录号是12,说明11丢了,立刻通过重传会话发起请求。注意重传请求要带具体的频道代码和记录号范围,不能简单地说"把丢的补给我"。

公告文件的恢复路径不同。公告是文件粒度,每个公告文件有唯一ID,登录后建议立即申请重传公告概要,拿到概要后和本地比对,发现有缺失或变化的公告再逐个按NewsID申请重传。公告概要本身也是通过重传消息返回的,操作时先申请一次概要看看到底缺什么。

def on_order_msg(channel_no: int, seq_no: int, raw: bytes): max_seq = channel_state[channel_no].max_seq if seq_no <= max_seq: return # 重复记录号,直接忽略 if seq_no > max_seq + 1: # 记录号跳变,说明有消息丢失,转重传会话补数 resend(channel_no, beg_seq=max_seq + 1, end_seq=0) return channel_state[channel_no].max_seq = seq_no handle_order(raw)

这段逻辑把两种情况分开了:重复的丢弃,跳号的进补数流程。实现上要注意channel_state是按频道隔离的,不要用一个全局最大值,否则不同频道之间的记录号会互相干扰。这里的handle_order在真实项目里会再根据消息类型拆成UA201逐笔委托和UA202逐笔成交两套解析,记录号判断逻辑是共用的。补数期间实时链路继续推新数据,重传链路只回指定范围的历史数据,两边汇合后统一走记录号去重,业务侧才不会算重。

4.3 兼容性五条规则:新字段自动忽略的边界

V1.11在兼容性上给VSS划了五条强制约定:新增行情频道可以先不处理;MDGW新增应用层消息类型可以先不处理;快照或逐笔新增行情类别(MDStreamID)可以先不处理;快照新增行情条目类别(MDEntryType)可以先不处理;证券实时状态新增开关类别(SecuritySwitchType)可以先不处理。

这五条合起来其实是同一句话:行情数据的演进是"增量兼容"的,老VSS不做任何改造,也应该能继续稳定接收旧数据。要做到这一点,解析器必须对未知的东西采取"跳过"策略而不是"报错"策略。未知模板ID返回None(如2.3节的骨架所示),未知MDEntryType不进盘口组装,未知开关类别不进状态判断,这样规范升级到V1.12时,旧代码可以直接跑。

反过来也要理解边界:能忽略的是"新增",不是"修改"。修订历史上出现过字段替换和删除,比如SecurityPreName换成FinancialStatus、逐笔消息删除TimeInForce三个字段、快照条目删除"结算价"类别,这类变化会对解析字段的全量逻辑产生影响,不能靠自动忽略规避,需要跟着版本同步改造。实际落地时,模板文件、字段字典、状态开关表要分别独立存放,这样升级时能单独替换一个配置而不影响其他模块。

5. 避坑指南:从V1.11修订史里挖出的五个故障

把V1.11从头翻到尾,修订历史本身就是一份踩坑记录。规范每修订一次,就有一批对接方的解析器要跟着调整,最容易翻车的地方也就集中在会话、记录号和FAST字典这些环节。我把实际接入中最高频的五个问题按现象、原因、解决的顺序列出来。

5.1 连接稳定运行几分钟后突然被网关断开

现象:实时连接建立后,盘中消息一多,TCP连接就被网关主动断开,程序陷入"重连-再被断开"的死循环。

原因:流量控制机制生效了。VSS消费RawData的速度跟不上网关发送速度,网关侧缓冲积累超过阈值后立即断开,它不会提前告警,只有断开这一下才意识到问题。

解决:从两个方向入手。一是提升消费效率,FAST解码不要放在业务处理线程里,接收、解码、策略处理三级流水线;二是加背压保护,接收队列有上限时暂停消费而不是无限堆积。网关断开是保护机制,重连逻辑要带退避,避免刚重连又被压垮。

5.2 重连成功后发现数据少了一段,会话层"恢复"并没有补回来

现象:TCP重连成功后,记录号出现缺口,用会话层机制做恢复却什么也没补到。

原因:规范写得很明确,会话层恢复机制仅仅是为了与标准FIX协议兼容,不能作为真正的消息恢复机制。很多人被"恢复"两个字误导,以为重连自动补数,实际上补数完全靠应用层重传消息。

解决:把重传逻辑做成应用层标配。重连成功后先做一次逐笔频道记录号检查,有缺口就走UA002请求补数;公告信息则在登录后主动申请一次公告概要。实时链路和重传链路各司其职:实时链路继续推最新数据,重传链路只返回指定范围的历史数据,两边不要混在一起处理。

5.3 记录号相同和跳号同时出现,补数补出重复数据

现象:补数请求发出去,数据回来了,却发现业务侧重复计算了成交。

原因:记录号小于等于已收最大值的消息是重复消息,应该丢弃。补数期间正常实时链路还在推新消息,补回的历史消息和实时消息会交错到达,如果只判断"大于最大记录号就处理",就会把补回的消息当成新消息重复处理。

解决:逐笔消息处理统一按记录号去重,不管消息来自实时会话还是重传会话,同一个频道的记录号是同一把尺子,先比对、再去重、后进业务。补回的消息记录号依然落在已收最大值到当前接收值之间,去重逻辑完全一样,不需要为补数走特殊分支。

5.4 隔一段时间解出来的价格突然跳变

现象:解码器偶尔解出异常价格,重启后恢复正常,过一段时间又复现,看起来毫无规律。

原因:FAST的增量编码依赖前值,同一个解码器如果跨频道复用,或者没有在每个RawData前重置字典前值,前面的字段值会被错误地带到后面消息里,解出来的数字自然就"串味"了。

解决:每个频道独立解码器实例,每个RawData前调用reset。这个动作成本很低,但漏掉就是最难查的脏数据问题,二进制协议里这类问题尤其难排查。建议在处理前把有异常的字段值和解码前值一起打进日志,比对正常消息和异常消息的前值差异,能很快定位是哪个字段串了。

5.5 按老版本解析V1.11的逐笔消息,字段对不上

现象:升级规范版本后,逐笔和证券状态消息解析错位,甚至出现告警刷屏。

原因:修订过程中有字段删除和替换。比如2015年逐笔消息删除TimeInForce、MaxPriceLevels、MinQty三个字段,证券实时状态消息里SecurityPreName被替换成FinancialStatus,快照条目也删除过"结算价"类别。对着老模板解新消息,字段位置自然全错。

解决:模板定义随版本走,升级时重新生成FAST模板并做一遍全量回放验证。新增字段可以靠兼容性规则自动忽略,删除和替换字段必须人工核对修订说明,把受影响的字段在解析器里同步调整。

6. 二次开发:解析器增量扩展与多频道配置

最后一层落到改造。行情接入项目最典型的二次开发需求有两个:一是新版本加了行情条目类别,解析器要平滑支持;二是多个频道要按业务开关配置订阅。两个需求都能用"字典驱动"的方式低成本解决。

6.1 用模板字典驱动新增条目类别

解析器不要写死字段分支,把MDEntryType和字段解析逻辑做成字典。新版本加一个条目类别,只需要往字典里注册新的解析函数,业务层代码完全不动:

handlers = { "0": parse_bid_price, "1": parse_offer_price, "9": parse_vwap, "xh": parse_nominal_price, } def on_snapshot(md_stream_id: str, entries: list): for entry in entries: h = handlers.get(entry.type) if h is None: continue # 新条目类别自动忽略 h(entry)

handler的注册逻辑要和FAST模板生成放一起,模板变、字典跟着变。验证方法很简单:用一段V1.10的抓包数据回放,确认老条目无回归;再对V1.11的新增条目做单测。从那以后我每次改解析器,都强制走一遍全量回放加条目单测,绝不只靠盘中的一层快照验证,希望帮到你。

6.2 频道订阅配置化与升级路径

频道订阅用配置驱动而不是硬编码。配置里写明频道代码、解码模板、是否启用,启动时按配置创建解码器实例。这样新增一个业务频道不需要改代码,改配置就能上线,2020年债券现券行情上线时,就是靠这类配置化改造快速接入了107x和207x频道。

还有一个习惯值得养成:把修订历史当成升级清单。每版V1.x更新,先过一遍"新增了什么、删除了什么、替换了什么",新增项检查兼容性规则是否覆盖,删除项检查现有解析器是否引用,再动代码。这样从V1.10升到V1.11,真正要动的地方往往只有三五处,而不会在盘中才发现字段解析错位。

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

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

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

立即咨询