☰
深交所Level2行情接口规范V1.11:会话机制与重传逻辑解析
2026/10/18 20:46:17 网站建设 项目流程

简介:深交所Level2行情数据接口规范V1.11是深圳证券交易所发布的官方技术标准,专为量化交易、高频交易和涨停板交易场景设计,系统规定了基于STEP协议的Level2行情数据格式、消息类型、字段定义及会话机制。该PDF文档共1个文件,大小约682KB,内容涵盖自2013年以来的全部修订记录,包括增加快照行情频道统计消息、业务拒绝消息、逐笔委托联系人与联系方式、债券现券交易竞买行情,以及港股通、期权备兑、盘后定价大宗交易等交易场景的开关类别和行情类别变化,并明确了接口兼容性要求。文档特别说明了用户行情系统如何自动忽略新增条目,保证系统在无需改造的情况下平滑升级。该规范适合负责行情接入的开发人员、量化策略研究员以及关注Level2数据的投资者使用,目前已有1992人学习下载,是理解深交所Level2接口演进和实际接入的重要参考。

1. 深交所Level2行情接口规范V1.11:先读懂这份规范再动手

量化团队天天讲Level2五档、逐笔委托、逐笔成交,但真正能把深交所Level2行情接到自研系统的团队,都是从一份接口规范开始的。深交所这份《STEP行情数据接口规范》V1.11,是2021年8月发布的版本,覆盖实时快照、逐笔委托、逐笔成交、公告、盘后定价交易以及新增的债券现券竞买行情,凡是做高频交易、量化回测、涨停板盘口监测的开发者,都需要把它当成地基图纸来读。它规定了TCP端口、心跳间隔、消息记录号、重传逻辑,还有一套“新增字段必须能自动忽略”的兼容性要求。这篇文章就按我拆这份规范的顺序,把会话机制、双层报文、重传逻辑和五个典型踩坑记录拆开讲清楚。

2. 会话机制:实时端口9129与重传端口9130怎么配合

2.1 两个端口,网络版才有后悔药

接入深交所Level2行情,第一步不是写解析代码,而是搞清楚你接通的是哪个版本的行情网关。规范明确把行情网关分成现场版和网络版两种:现场版走单向卫星线路,网络版走地面网络。端口方面,行情网关给接入用户提供两个服务端口,实时数据端口默认9129,重传服务端口默认9130。

端口用途现场版网络版
9129实时行情数据支持支持
9130缺失数据重传不支持支持

这里的含义很直接:现场版因为单向卫星线路,丢了数据没有后悔药;网络版因为走地面网络,才具备重传能力。更需要注意的是,每个端口只能建立一个TCP连接,也就是说VSS到行情网关之间最多两条连接,实时一条、重传一条。很多团队在联调环境里反复登录,同一个端口开了第二条连接,结果排在后面的登录直接被网关拒绝。

接入之前还要确认一件事:行情网关和用户行情系统必须处于同一个安全网络内。规范里写得很白,网关到VSS之间数据不加密,安全由接入网络负责。所以不要在公网上裸跑行情连接,一般做法是在行情网关所在网络内申请独立VLAN,再用访问控制列表把9129、9130两个端口限定到你的IP上。

2.2 登录:通信版本号填1.02而不是1.11

会话建立在登录消息上,规范全文遵循《轻量级STEP会话层接口规范》和FIX 5.0 SP2的体系。这里有个反直觉的点:你手里的规范叫V1.11,但登录消息(Logon)的DefaultApplVerID字段要填的通信版本号是1.02,不是1.11。规范的修订历史里,V1.11对应的通信版本号是“待定”,实际生效的协议版本仍然是1.02。很多团队第一次做对接时把版本号填进去,网关直接回了登出消息。

登录阶段VSS与网关最多能建立两个会话:实时数据会话和重传服务会话,分别对应9129和9130端口。会话建立过程不复杂,核心就是把DefaultApplVerID、SenderCompID、TargetCompID、HeartBtInt这些FIX标准字段填对。HeartBtInt建议设为30秒,这是标准FIX推荐的默认心跳间隔,如果网关侧没有下发特殊要求,用默认值最不容易出问题。另外要注意,用户信息报告消息UA003只在信息商VSS需要定时向网关上报时才发,券商自用的VSS不需要理会这个模板。

2.3 心跳与故障判定:3秒心跳,6秒判死

连接建立之后,真正的行情数据开始流动。每个行情数据频道在空闲时会独立发送心跳消息,格式是公共消息里的UA001,模板ID为3001。规范写得很清楚:频道心跳发送间隔为3秒。这不是FIX会话层的HeartBtInt,而是FAST消息体里的频道心跳,两者要区分开。

对VSS来说,这个3秒心跳是判断链路是否存活的重要依据。规范给出的判死标准是:用户行情系统没有接收到数据包的时间超过两倍的心跳间隔,也就是约6秒,就认为行情网关可能故障,应当断开连接并尝试重连。注意这里说的是“没有接收到数据包”,不管是行情数据还是心跳消息,只要是同一个频道的数据包就行。我一般会在VSS里做一层频道路由,定时器按频道单独维护,而不是全局一个心跳计时器,否则某个低频频道没数据时会把其他正常频道误判成故障。

import time class ChannelMonitor: def __init__(self, channel_no, heartbeat_interval=3.0): self.channel_no = channel_no self.last_packet_time = time.monotonic() self.timeout_threshold = heartbeat_interval * 2 def on_packet(self): self.last_packet_time = time.monotonic() def is_timeout(self): return time.monotonic() - self.last_packet_time > self.timeout_threshold

这段代码逻辑很简单,on_packet在收到该频道任何数据包时调用更新最后活跃时间,is_timeout负责判断是否超过6秒没数据。参数上的关键点是timeout_threshold取心跳间隔的两倍,规范原文就是这个标准,不要自己放宽到10秒或15秒,否则网关重新连上之前,你的VSS还站在原地等数据。

2.4 流量控制:处理慢了会被直接踢下线

流量控制是会话层最容易被忽略的部分。规范原文是:如果用户行情系统不能及时处理行情网关发送的数据,导致网关累积的待发送消息超过设定阈值,网关会立即断开连接。这里没有任何预警机制,没有告警消息,没有先减帧再断开的过渡,直接就是TCP断开。对做Level2行情的团队来说,这是最恐怖的坑之一。

我在做某跨平台行情接收系统时,最初把行情解析和业务入库放在同一个线程里,行情峰值一来,数据库写入变慢,网关侧积压消息数瞬间超过阈值,连接被踢掉,然后又要走重连加重传流程,循环往复。后来改成IO线程只管收包和解析,把结果丢进有界队列,业务线程独立消费,问题才消失。建议在VSS里把网络接收和业务处理彻底分离,队列满时宁可丢业务数据也不要阻塞IO读取,因为一旦阻塞,远端网关可不会等你。

提示:流量控制触发后,网关不会发送任何形式的预告。连接被断开的唯一征兆是TCP连接异常关闭,所以VSS的断线重连逻辑必须做得足够快,重连成功后还要主动走应用层重传。

2.5 会话恢复:别指望会话层重放

最后说恢复。规范里专门有一句提醒:会话层恢复机制仅仅是为了与标准FIX会话协议兼容,不能作为真正的消息恢复机制使用。也就是说,断线重连后,TCP层面以前收过的消息不会自动补给你,缺失的数据要靠应用层的重传消息机制来补,具体逻辑在第四章详细讲。VSS重启后也是一样,建议在完成登录动作之后,立即向网关申请重传公告概要,把可能漏掉的公告文件先补回来,再继续处理实时行情。

这个顺序很重要,公告文件涉及停牌、复牌、除权除息这些影响证券状态的事件,如果登录后先消费快照而忘记申请公告,盘中突然收到一笔影响持仓的公告时,你的风控模型可能已经晚了半拍。

3. 消息结构:STEP与FAST双层报文解析方案

3.1 STEP消息层:ChannelNo、RawDataLength、RawData三件套

从TCP端口拿到的数据不是一根裸的FAST流,而是STEP消息层包着FAST消息体的结构。每个应用层消息由STEP消息层和FAST消息层两级构成,STEP层类似信封,FAST层才是正文。STEP消息层的核心字段有三个:10201 ChannelNo频道代码、95 RawDataLength表示FAST消息部分的数据长度、96 RawData保存FAST消息体。

解码顺序上有个必须记住的规矩:RawData中可能包含本频道的多条FAST编码消息,在解码每条FAST消息体之前要重置解码器的FAST字段前值。这个“字段前值”是FAST协议用来压缩数据用的,类似增量编码里的上次值,如果不重置,第二条消息会拿着第一条消息的前值继续解码,轻则解析出离谱的价格,重则直接报错。

import socket import struct buf = b"" while True: data = sock.recv(65535) if not data: break buf += data # 每个STEP消息以标准头开始,按标签长度解析 while len(buf) >= 16: channel_no, raw_len = parse_step_header(buf) if len(buf) < 16 + raw_len: break raw_body = buf[16:16 + raw_len] buf = buf[16 + raw_len:] # 关键:解码FAST前先重置解码器字段前值 fast_decoder.reset_state() for fast_msg in fast_decoder.decode(raw_body): handle_market_data(channel_no, fast_msg)

这里parse_step_header是把FAST编码的头部解析成频道代码和数据长度,fast_decoder.decode是按FAST模板逐个解码消息。留意调用顺序,reset_state必须在decode之前执行,而且每个RawData都执行一次。参数上,raw_len来自95号域,它只代表FAST部分长度,不含STEP头本身。

3.2 FAST模板ID:3000-3999公共消息,4000-15999实时行情

FAST消息体里第一个要解的字段是模板ID,规范给模板ID划了两个区间:3000-3999是公共消息,4000-15999是实时行情数据。公共消息和实时行情分开编址这件事,对VSS的代码结构影响很大,因为两类消息的生命周期和处理路径完全不同。

常见模板对应关系:

模板ID消息类型用途
3001UA001频道心跳
3002UA002重传请求(VSS发往网关)
3003UA003用户信息报告(信息商VSS定期上报)
4000+UA2xx / W实时行情消息

模板ID的解析决定了后续所有字段的映射。我在实际项目中的做法是先用模板ID做一次分派,公共消息走独立处理函数,行情消息再按频道代码和行情类别二次分派。这样加新模板时,只需要在分派表里注册,不需要改主循环。

3.3 快照行情W消息:盘口价格和参考价都在这

快照行情是VSS最常打交道的部分。快照的本质是定时发布的场景状态切片,每个快照行情频道可能包含多个类别的行情,每类行情都有自己的发布频率。这和逐笔不一样,快照没有记录号,也不支持重传,丢了就等下一帧快照。

频道方面,快照覆盖了指数、股票、基金、可转债、权证、期权、盘后定价大宗交易、债券分销、港股实时行情等类别。具体频道代码区间在第四章的频道表里统一列。做涨停板盘口监测的团队,重点盯集中竞价交易快照行情,股票在101x频道,基金102x,可转债103x,权证104x,期权105x。盘中某个类别长时间不发快照是正常的,因为各类别行情有自己的发布节奏,不能误判为链路故障。

快照消息里值得注意的行情条目类别包括:MDEntryType为9的加权平均价、xi参考价、xh按盘价、xk昨收盘加权平均价,这些类别是量化回测中计算均价和涨跌停参考的重要字段。如果行情条目里有不认识的MDEntryType,规范要求VSS应该自动忽略它,而不是报错。我在代码里会把未知类别打印到日志但继续处理,这样升级规范后不会因为新增条目类别而导致整个解析线程崩溃。

3.4 逐笔行情UA201与UA202:委托和成交是两条消息

逐笔行情分为逐笔委托(UA201)和逐笔成交(UA202)两类消息,同一频道里两类消息交替出现。委托消息描述订单进入订单簿,成交消息描述订单簿上发生的撮合结果。做逐笔还原的团队,要把两类消息按证券代码和时间戳拼接,才能重建出完整的订单簿变化过程。

逐笔行情支持重传,这是它和快照最大的区别。每个逐笔行情消息都带有所属频道代码和消息记录号,记录号在频道内从1开始顺序递增。行情网关在某个频道的逐笔消息发送完毕后,会发送一笔频道结束消息,标记该频道的本轮数据发送完了。心跳消息没有记录号,所以频道结束消息和心跳消息都不能参与记录号连续性判断。

1.07版本之后,逐笔行情支持两种不同的发送模式。规范只提到“支持两种不同的发送模式”,没有在正文展开细节,所以我对接前一般会主动向行情网关提供方确认当前生产环境用的是哪种模式,再决定VSS里的组装逻辑。这个问题不提前确认,联调时会出现明明记录号连续、但行情序列就是对不上的现象。

4. 重传机制与频道订阅:数据补全的关键逻辑

4.1 记录号跳变检测:丢失的逐笔用序号抓出来

逐笔行情的数据补全完全依赖记录号。规范给的判断规则非常明确:当收到的消息记录号小于等于本频道已经收到的最大消息记录号时,说明这条消息之前已经收过,应忽略;当收到的消息记录号大于最大记录号加1时,说明中间有丢消息,需要走重传。

max_seq = {} # 频道 -> 最大记录号 def on_tick_message(channel_no, seq): cur_max = max_seq.get(channel_no, 0) if seq <= cur_max: # 重复消息,直接丢弃 return if seq > cur_max + 1: # 发现缺口,发起重传 request_retransmit(channel_no, cur_max + 1, seq - 1) max_seq[channel_no] = seq

这段代码的逻辑顺序很关键,先判断重复,再判断缺口,最后更新最大值。如果先把通道的最大值往上更新再发重传请求,重传回来的消息会因为记录号小于等于新最大值而被当成重复消息丢掉。这是第五章要展开的经典翻车现场。参数上,request_retransmit的起止序号取cur_max+1到seq-1,覆盖的正是缺口区间。

4.2 重传请求UA002:参数怎么填才不会白请求

重传消息由VSS发给行情网关,消息类型为UA002,模板ID为3002。请求里最重要的两个参数是ResendType和ApplBegSeqNum/ApplEndSeqNum。ResendType=1表示逐笔行情数据,ResendType=2表示公告信息。请求逐笔时,ChannelNo填目标频道,ApplBegSeqNum填起始序号,ApplEndSeqNum填结束序号。

有一个参数细节值得注意:ApplEndSeqNum填0时,网关会把结束序号自动设置为收到请求时该频道数据在内存中的最大值。这个设计很实用,当你知道丢了多少但不知道能补到哪时,直接填0让网关决定边界。重传请求参数表:

字段取值说明
ResendType1 / 21=逐笔行情,2=公告信息
ApplBegSeqNum整数起始序号,ResendType=1时生效
ApplEndSeqNum整数或0结束序号,0表示取内存最大值
NewsID字符公告ID,ResendType=2时生效,空表示申请公告概要

网关以“请求—应答”方式处理重传,多个重传请求按到达顺序排队。所以不要并发发送多个重传请求再期待乱序返回,VSS里要做请求串行化。网关返回的重传结果由ResendStatus标记:1完成,2部分完成,3无权限,4数据不可用。部分完成说明有部分请求的数据没有返回,常见原因是请求序号范围超出了网关内存中保留的数据范围,这时需要接受数据断层,从最新数据重新开始。

4.3 公告重传:先拉概要,再逐条补文件

公告文件和逐笔行情共用UA002重传通道,但处理逻辑不同。每个公告文件在网关侧都有唯一ID,即NewsID。行情网关对每个新到达的公告文件,都会通过公告消息主动推给VSS。如果VSS在公告发布时处于断线状态,这笔公告就丢了。

补公告的标准流程是两步:先发一条NewsID为空的UA002重传请求,申请公告概要,公告概要里包含所有已发布公告文件的ID列表;VSS拿到概要后,对照本地已有的公告ID,找出缺失或变化的文件,再逐个申请重传。规范明确建议VSS在完成与网关的登录动作之后,立即申请重传公告概要。这条建议要严格执行,因为盘中停牌公告直接影响证券实时状态和交易逻辑,晚一分钟补上就有可能在风控层面慢半拍。

4.4 频道代码表与订阅策略:别把所有频道都接进来

行情频道的完整清单在规范里。每个行情网关可以配置只接收某些频道的交易行情数据,从业务上把频道分成几大块:

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

订阅策略我一般按业务切割:只做A股股票量化的,集中竞价快照订阅101x、逐笔订阅201x就足够;涉及可转债的策略加103x和203x;港股通相关策略再补5001。不要为了省事把全部频道接进来,频道越多,FAST解码压力越大,流量控制被断连的风险越高。规范在兼容性要求里也明确,VSS如果不使用新增频道,应可以不作变更就自动忽略这些新增频道中的消息,所以订阅时可以保守一点。

5. 避坑记录:拆解V1.11时踩过的五个坑

5.1 登录消息填1.11被网关秒拒

现象:VSS发送Logon消息后,网关立即返回Logout,连接被关闭,抓包看只有一次正常的TCP握手。 原因:把文档版本号V1.11直接填进了DefaultApplVerID字段。规范里通信版本号是独立的,当前为1.02,1.11是文档修订版本,两者不是一回事。 解决:登录消息的DefaultApplVerID固定填1.02。把版本号配置放到单独配置文件里,文档版本和通信版本分开管理,升级规范时不要顺手改登录参数。

5.2 快照丢了数据却发重传,返回数据不可用

现象:盘中快照偶尔丢失一个周期,VSS按逐笔的逻辑发UA002重传请求,网关返回ResendStatus=4,数据不可用。 原因:快照行情定时发布,规范明确不能重传。逐笔有记录号可以精确补,快照是切片状态,旧帧没有保存价值。 解决:快照丢失不做重传,等待下一周期快照覆盖即可。把快照和逐笔的重传开关分开配置,避免通用重传模块把快照也带进去。

5.3 一个RawData里多条FAST消息,解码没重置前值

现象:联调时单条消息解析正常,但同一RawData内第二条消息后的价格、数量偶尔出现夸张的异常值,且错误位置不确定。 原因:RawData可以包含本频道的多条FAST编码消息,FAST字段前值只有在单条FAST流内才有效。没有在解码前重置解码器前值,第二条消息就会继承第一条的字典状态。 解决:在decode每个RawData前强制reset_state()。在解析框架里把这个重置逻辑放在RawData级别,而不是消息级别,并且代码评审时把它列为必查项。

5.4 处理速度跟不上,网关直接断链

现象:行情峰值期TCP连接被网关主动断开,重启VSS后很快又被断开,日志里没有任何业务错误。 原因:流量控制机制生效。网关待发送消息数超过阈值后立即断开连接,断开前没有发送任何预告。 解决:把网络接收线程与业务处理线程完全分离,中间用有界队列缓冲。队列满时优先丢弃非关键业务数据,也不要让IO阻塞;同时监控队列积压量,接近阈值时预警。对高频交易场景,这个指标要进监控大屏。

5.5 重传回来的数据被当成重复消息丢弃

现象:检测到记录号跳变发起重传,请求全部返回完成,但补回来的消息数量对不上,部分订单明细始终缺失。 原因:判断逻辑先更新了最大值再过滤重复。重传消息的记录号落回缺口区间时,被“seq <= max”的重复判断拦掉了。 解决:严格按照规范的判断顺序:先检查重复、再检查缺口、最后更新最大值。给这条逻辑加单元测试,构造“收到12但最大是10,重传回11、12再来一条13”的用例,确保重传消息能正常落库。

6. 兼容性条款的设计落地:让VSS扛住下一次版本升级

6.1 把“必须自动忽略”写进解析器

V1.11的兼容性要求一共五条,核心意思是:新增频道、新增消息类型、新增行情类别、新增行情条目类别、新增开关类别,VSS如果不关心就应该能自动忽略,而不是改造代码。这五条约束其实是给VSS开发者设计解析器的指导意见——解析器必须做成“识别并跳过”的模式。

我在实现解析框架时,专门维护了一个模板ID分发表:已知模板走对应的解析函数,未知模板ID打进调试日志但继续解码下一条,不中断频道处理。消息字段层同理,遇到不认识的MDEntryType或SecuritySwitchType,只记录出现次数,不进业务库。这样下次交易所升级规范时,我只需要对照修订历史确认新增了哪些字段,判断是否要发展为业务字段,而不需要紧急发版才能保住行情不断流。

def dispatch_fast_message(template_id, fields): if template_id in TEMPLATE_HANDLERS: return TEMPLATE_HANDLERS[template_id](fields) # 未知模板:记录并跳过,保持频道存活 logger.warning("unknown template", template_id=template_id, fields=fields) return None

这段分派代码的价值不在技术难度,而在于“未知不等于错误”的态度。新模板能够即时跳过,整个频道就不会因为一个陌生模板ID而崩溃。

6.2 升级规范时的检查习惯

每次拿到新版规范,我一般会先干一件事:对比修订历史。V1.11的修订历史从2013年的0.10版一路走到2021年8月,每一版都标注了新增了哪些开关类别、行情类别、频道代码。把新增项列成一张“影响面清单”,再对照自己的VSS确认:新增频道我是否要订阅,新增MDEntryType是否需要入库,新增证券实时状态开关类别是否影响风控逻辑。确认完之后,才动解析代码。

这个习惯救过我一次。某次升级时新增了一个干扰观测的行情条目,我没有先读修订历史就急着改解析器,结果把原有快照的字段映射弄错,盘口报价出现了毛刺。从那以后我拿到新版规范,都强制走一遍“修订历史→影响面清单→模板分发表→功能开关”这个流程,把未知字段当常规输入对待。希望帮到你。

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

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

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

立即咨询