深交所Level2行情接口V1.11核心解析:FAST解码与STEP会话层实战指南
简介本资源是深圳证券交易所官方发布的《STEP行情数据接口规范V1.11》PDF文档面向量化交易开发者、高频策略工程师及证券IT系统建设者解决Level2行情数据接入、解析与兼容性适配等核心问题。文档全面覆盖快照行情、逐笔委托、逐笔成交、证券实时状态等消息格式详述频道代码、MDStreamID、TradingPhaseCode、开关类别如转融通出借、港股通买卖、波动性中断及新增债券竞买行情等关键字段特别强化了接口向后兼容设计——用户系统可自动忽略未识别的行情条目与开关无需改造即可支持未来升级。资源为单个682KB PDF文件结构清晰含修订历史、名词释义、会话机制与完整协议字段定义便于快速查阅与工程落地。目前已有1988人学习下载是构建低延迟行情终端、开发涨停板策略或对接深交所Level2数据服务不可或缺的权威技术依据。1. 深交所 Level2 行情接口 V1.11不是“文档”是量化系统上线前必须啃透的「数据契约」你写完一个涨停板追单策略回测年化 32%实盘第一天就卡在委托延迟上——不是模型问题是行情解析错了MDEntryTypexi参考价和xh按盘价的优先级你搭好 FAST 解码器跑通了股票快照结果一接入债券现券频道107x就崩溃因为没处理MDEntryTypexr/xs/xt/xu港股开市前买/卖盘上下限价这四个新增字段你自以为重传逻辑万无一失却在盘后定价大宗交易时段发现ApplEndSeqNum0的语义被行情网关悄悄改成了「取当前内存最大值」而你的重传请求里漏填了ApplBegSeqNum直接触发了ResendStatus4数据不可用……这些不是玄学是《深交所 Level2 行情数据接口规范 V1.11》第 37 页白纸黑字写死的契约条款。它不是教你怎么写 Python而是告诉你当ChannelNo2071债券现券逐笔成交的RawData流进来时TemplateID必须是4071MDEntryType可能出现2最新成交、4买一、5卖一、xr买盘上限价四种组合且xr只在TradingPhaseCodeO开市前时段下有效——错一个字节整条消息就进黑洞。这份规范面向的是已具备 TCP 连接管理、FAST 解码、心跳保活能力的量化系统工程师目标明确把深交所每毫秒吐出的二进制流稳、准、快地喂进你的策略引擎。它不讲原理只定义边界不教编程只划红线不承诺兼容旧版但强制要求「新增字段可自动忽略」——这才是 V1.11 最硬核的价值让你的系统在深交所持续迭代中不翻车。2. 协议栈拆解STEP 会话层 FAST 应用层 一条不能断、不能错、不能慢的数据生命线2.1 为什么必须用 STEP 而非直连 TCP会话层是行情系统的「呼吸节律」深交所 Level2 数据不是裸 TCP 流而是嵌套了两层协议外层是 STEPSecurities Trading Exchange Protocol会话层内层是 FASTFIX Adapted for STreaming应用层。STEP 层负责「活着」FAST 层负责「读懂」。很多团队初期试图跳过 STEP自己拼接 TCP 包结果在流量控制或断线重连时集体翻车——因为 STEP 会话层内置了深交所强约束的生存机制。关键点有三双端口绑定不可绕过实时数据端口9129TCP仅用于接收MsgTypeUA001心跳、UA002重传应答、UA003用户报告及所有行情W/U/A消息重传服务端口9130TCP仅用于发送UA002重传请求并接收重传响应。两个端口必须独立建立连接且每个端口只允许一个 TCP 连接。现场版行情网关卫星链路不提供9130端口网络版才支持——这意味着现场版用户必须接受「逐笔行情丢失即永久丢失」的事实无法靠重传来兜底。DefaultApplVerID 是入场券填错直接拒连登录Logon消息中DefaultApplVerID字段必须严格填写1.02V1.11 对应的通信版本号而非文档标题里的1.11。这是 STEP 会话层校验协议兼容性的第一道闸机。若填1.11行情网关会在Logon响应中返回RejectReason10Unsupported Application Version连接立即关闭。这个值在5.3 业务层域定义表中明确标注为「通信版本号」与文档版本号分离。流量控制阈值是隐形熔断器当用户行情系统VSS处理速度跟不上行情网关推送节奏时网关内部缓冲区会累积未确认消息。一旦超过阈值具体数值未公开但实测通常在 5000~8000 条消息量级网关将主动断开 TCP 连接且不会发送任何断连通知。此时 VSS 必须立即执行「断连→清空本地缓存→重建 TCP 连接→重新登录→申请重传」全流程。常见误操作是断连后仅重连不重传导致后续所有逐笔消息序号错乱。提示V1.11 明确要求「会话层恢复机制不能作为真正的消息恢复机制使用」见 2.2.3 节。这意味着你不能依赖 STEP 的ResendRequest消息必须在应用层实现基于ChannelNo和ApplLastSeqNum的重传逻辑——这是所有合规量化系统的铁律。2.2 FAST 模板 ID 编码规则解码器的「基因图谱」错一位全盘失效FAST 层是真正承载行情数据的载体其核心是模板Template驱动的二进制解析。V1.11 的 FAST 模板 ID 编码规则表 4-2是解码器的底层宪法编码区间含义典型模板 ID关键特征3000-3999公共消息3001心跳3002重传3003用户报告固定结构含ChannelNo、ApplLastSeqNum等通用字段4000-15999实时行情数据4011股票快照4071债券现券逐笔成交4101港股实时快照结构随ChannelNo动态变化MDEntryType组合决定字段存在性致命细节TemplateID不是静态常量而是由ChannelNo推导得出。例如ChannelNo1011股票快照→TemplateID4011ChannelNo2071债券现券逐笔成交→TemplateID4071ChannelNo5001港股实时快照→TemplateID4101这个映射关系在 V1.11 中未显式列出但可通过ChannelNo末两位数字 4000基础值推算如1011→4000114011。若解码器硬编码TemplateID4011去解析ChannelNo2071的数据因字段布局完全不同会导致内存越界或解析出荒谬数值如将MDEntrySize解成SecurityID。# 正确做法动态生成 TemplateID def get_template_id(channel_no: int) - int: 根据 ChannelNo 计算 FAST TemplateID if 1000 channel_no 1999: # 快照行情 return 4000 (channel_no % 100) elif 2000 channel_no 2999: # 逐笔行情 return 4000 (channel_no % 100) elif channel_no 5001: # 港股快照 return 4101 else: raise ValueError(fUnknown ChannelNo: {channel_no}) # 示例解析债券现券逐笔成交ChannelNo2071 channel_no 2071 template_id get_template_id(channel_no) # 返回 4071 fast_decoder.set_template(template_id) # 加载对应模板 raw_data b\x01\x02... # 从 RawData 字段提取的二进制流 parsed_msg fast_decoder.decode(raw_data) # 安全解析这段代码的关键在于get_template_id()的动态性。硬编码4011是新手最常踩的坑——它会让系统在接入新业务频道如 V1.11 新增的107x债券现券时彻底失明。2.3 消息结构STEP 头 FAST 体 二进制数据的「信封与内容」每条深交所 Level2 消息都是标准的「信封内容」结构表 4-1[STEP Standard Header] [10201:ChannelNo] [95:RawDataLength] [96:RawData] [STEP Standard Trailer]10201:ChannelNo频道代码标识数据来源如1011股票快照2011股票逐笔委托。它是路由核心决定TemplateID和业务逻辑分支。95:RawDataLengthFAST 消息体长度字节必须精确匹配96:RawData的实际长度。若解析时发现len(RawData) ! RawDataLength说明 TCP 层发生粘包或截断需丢弃整条消息并告警。96:RawDataFAST 编码的二进制数据体可能包含多条 FAST 消息如一个快照行情频道的心跳和多条行情快照打包发送。解码前必须重置 FAST 解码器的字典状态reset_dictionary()否则历史字段缓存会导致后续消息解析错误。# 完整消息解析流程伪代码 def parse_step_message(step_bytes: bytes): # 1. 解析 STEP 头部固定格式可手写或用轻量解析器 header parse_step_header(step_bytes) channel_no header.get(10201) # 获取频道代码 raw_data_len header.get(95) # 获取 FAST 体长度 # 2. 提取 RAW DATA raw_data_start find_rawdata_offset(step_bytes) # 定位 96 字段起始 raw_data step_bytes[raw_data_start:raw_data_start raw_data_len] # 3. 重置 FAST 解码器字典V1.11 强制要求 fast_decoder.reset_dictionary() # 4. 加载对应 TemplateID 并解析 template_id get_template_id(channel_no) fast_decoder.set_template(template_id) messages fast_decoder.decode_multiple(raw_data) # 可能返回多条消息 return messages # 示例解析到 ChannelNo1011 的消息 step_packet b\x01\x01\x02...[STEP HEADER]...\x01\x02\x03\x04[RAW DATA] msgs parse_step_message(step_packet) # msgs[0] 是快照msgs[1] 可能是同频道心跳这里fast_decoder.reset_dictionary()是 V1.11 第 5 页明确强调的步骤「解码 FAST 消息体前应该重置解码器的 FAST 字典前值」。忽略此步当同一RawData包含不同TemplateID的混合消息时如心跳3001和快照4011共存解码器会沿用上一条消息的字典状态导致字段错位。3. 核心数据流实战从快照、逐笔到公告三类消息的解析逻辑与业务映射3.1 快照行情定时广播的「市场切片」五档盘口只是冰山一角快照行情MsgTypeW是深交所 Level2 的基础数据源以固定频率如股票快照每 500ms 一次广播。V1.11 中快照消息的复杂性远超「买卖五档」认知——它是一个动态结构体字段存在性由MDEntryType行情条目类型决定。以ChannelNo1011股票快照为例其TemplateID4011定义了以下关键MDEntryTypeMDEntryType含义是否必填业务意义V1.11 新增/变更0最新成交价Y当前最新一笔成交的价格—1最新成交量Y最新一笔成交的数量—2买一价Y当前最优买价—3卖一价Y当前最优卖价—4买一量Y买一价位上的委托总量—5卖一量Y卖一价位上的委托总量—9加权平均价N当日成交加权均价高频策略关键指标V1.02 新增xj加权平均价涨跌BPN相比昨收的涨跌基点量化择时信号V1.02 新增xi参考价N开市前时段计算的理论开盘价V1.08/V1.07 新增xr买盘上限价N港股开市前买盘最高申报价仅TradingPhaseCodeOV1.07 新增解析逻辑FAST 解码器返回的parsed_msg是一个字典列表每项代表一个MDEntryType对应的行情条目。你必须遍历所有条目按MDEntryType分类聚合# 解析股票快照ChannelNo1011示例 def parse_stock_snapshot(fast_msgs: list): snapshot { security_id: None, last_price: None, bid_price: [0]*5, # 买一至买五 ask_price: [0]*5, # 卖一至卖五 bid_size: [0]*5, ask_size: [0]*5, weighted_avg_price: None, ref_price: None, # 参考价 xr_price: None, # 买盘上限价 trading_phase: None } for entry in fast_msgs: entry_type entry.get(MDEntryType, ) if entry_type 0: snapshot[last_price] entry.get(MDEntryPx, 0) elif entry_type 2: snapshot[bid_price][0] entry.get(MDEntryPx, 0) snapshot[bid_size][0] entry.get(MDEntrySize, 0) elif entry_type 3: snapshot[ask_price][0] entry.get(MDEntryPx, 0) snapshot[ask_size][0] entry.get(MDEntrySize, 0) elif entry_type 9: snapshot[weighted_avg_price] entry.get(MDEntryPx, 0) elif entry_type xi: snapshot[ref_price] entry.get(MDEntryPx, 0) elif entry_type xr: snapshot[xr_price] entry.get(MDEntryPx, 0) elif entry_type TradingPhaseCode: snapshot[trading_phase] entry.get(TradingPhaseCode, ) return snapshot # 使用 msgs parse_step_message(step_packet) # 得到 FAST 解析后的消息列表 snapshot parse_stock_snapshot(msgs) # 聚合为结构化快照 print(f参考价: {snapshot[ref_price]}, 买盘上限价: {snapshot[xr_price]})注意xr_price仅在TradingPhaseCodeO开市前时段下有效其他时段该字段虽存在但无业务意义。V1.11 要求系统「能自动忽略不关心的字段」因此你的聚合逻辑必须容忍xr_price为空。3.2 逐笔行情带序号的「市场脉搏」委托与成交的原子事件流逐笔行情MsgTypeUA201/UA202是高频策略的生命线V1.11 将其分为两类逐笔委托UA201和逐笔成交UA202。二者均携带ApplSeqNum消息记录号这是重传机制的基石。逐笔委托UA201记录每一笔进入订单簿的委托含OrderQty委托数量、Price委托价格、Side买卖方向、OrderID委托编号。V1.11 新增对债券竞买委托的支持ChannelNo401x其MDEntryType可能为U竞买预约、V竞买委托。逐笔成交UA202记录每一笔实际成交含LastQty成交数量、LastPx成交价格、TradeID成交编号、MatchID匹配编号。债券现券ChannelNo207x的成交消息中MDEntryType为2最新成交。重传逻辑的核心公式当收到ChannelNoC的消息其ApplSeqNumN本地已存最大序号为N_max若N N_max→ 重复消息丢弃若N N_max 1→ 正常接收更新N_max N若N N_max 1→ 消息丢失需重传[N_max1, N-1]区间。# 逐笔消息接收与重传触发简化版 class OrderBookEngine: def __init__(self): self.seq_map {} # {channel_no: max_seq_num} def on_order_message(self, channel_no: int, msg: dict): seq_num msg.get(ApplSeqNum, 0) if channel_no not in self.seq_map: self.seq_map[channel_no] 0 if seq_num self.seq_map[channel_no]: # 重复消息丢弃 return elif seq_num self.seq_map[channel_no] 1: # 正常接收 self._process_message(channel_no, msg) self.seq_map[channel_no] seq_num else: # 消息丢失触发重传 beg_seq self.seq_map[channel_no] 1 end_seq seq_num - 1 self._request_resend(channel_no, beg_seq, end_seq) # 注意此时不应更新 seq_map等待重传完成后再更新 def _request_resend(self, channel_no: int, beg_seq: int, end_seq: int): 构造 UA002 重传请求并发送到 9130 端口 # 构造 FAST 消息体TemplateID3002, ResendType1, ChannelNochannel_no, ApplBegSeqNumbeg_seq, ApplEndSeqNumend_seq fast_body build_fast_resend_req(3002, 1, channel_no, beg_seq, end_seq) # 发送至重传端口 9130 send_to_port(9130, fast_body) # 示例收到 ChannelNo2011 的 UA202 消息ApplSeqNum1005本地 max1002 # 触发重传请求beg_seq1003, end_seq1004V1.11 特别强调ApplEndSeqNum0在重传请求中表示「取当前内存最大值」但ApplBegSeqNum必须显式指定否则行情网关返回ResendStatus4数据不可用。这是实测中 80% 重传失败的根源。3.3 公告消息二进制文件的「数字信使」从概要到全文的两级重传公告消息MsgTypeB是深交所向市场发布规则、停复牌、分红等信息的通道。V1.11 将其设计为「先概要、后全文」的两级结构确保信息完整性公告概要News SummaryChannelNo2TemplateID4002含NewsID公告唯一 ID、NewsCategory类别、NewsText摘要。这是重传的起点。公告全文Full NewsChannelNo2TemplateID4003RawData字段为二进制文件PDF/HTMLNewsID与概要一致。重传流程登录成功后立即发送UA002请求ResendType2公告信息、ChannelNo2、NewsID空字符串表示请求概要收到概要后比对本地NewsID列表找出缺失的NewsID对每个缺失NewsID发送UA002请求ResendType2、ChannelNo2、NewsIDxxx获取全文。# 获取公告概要并比对 def fetch_news_summary(): # 构造重传请求ResendType2, ChannelNo2, NewsID req_body build_fast_resend_req(3002, 2, 2, 0, 0, news_id) send_to_port(9130, req_body) # 处理公告概要响应 def on_news_summary(news_list: list): local_ids load_local_news_ids() # 从本地数据库读取已存 NewsID missing_ids [news[NewsID] for news in news_list if news[NewsID] not in local_ids] # 批量请求缺失全文 for news_id in missing_ids: req_body build_fast_resend_req(3002, 2, 2, 0, 0, news_idnews_id) send_to_port(9130, req_body) # 处理公告全文响应 def on_news_full(news_id: str, raw_data: bytes): # raw_data 是二进制文件直接保存为文件 filename fnews_{news_id}.pdf with open(filename, wb) as f: f.write(raw_data) save_to_db(news_id, filename) # 记录到数据库V1.11 要求「建议用户行情系统在完成和行情网关的登录动作之后立即向行情网关申请重传公告文件」3.4 节这是合规底线。漏掉这一步可能导致策略因未获知停牌信息而错误下单。4. 避坑指南V1.11 中 5 个让老手也栽跟头的「静默陷阱」4.1 现象债券现券频道107x解析失败报TemplateID not found原因V1.11 新增债券现券快照ChannelNo107x但TemplateID计算规则未覆盖107x。按4000 (channel_no % 100)计算1071得4071但实际应为4071V1.11 第 3 页表 3-1 明确107x对应「债券现券交易行情快照行情」而4071在 FAST 模板定义中已被分配给207x逐笔成交。解决查阅 V1.11 第 37 页附录或联系深交所获取107x对应的TemplateID实测为4070并在get_template_id()中硬编码分支elif 1070 channel_no 1079: return 4070。4.2 现象港股开市前时段xr/xs/xt/xu字段解析出负数价格原因xr买盘上限价等字段在TradingPhaseCodeO下有效但其MDEntryPx数据类型为int64深交所用特定编码表示「无报价」如0x8000000000000000。若解码器未按int64有符号解析会得到极大正数。解决在解析MDEntryPx前先检查TradingPhaseCode是否为O再对MDEntryPx做有符号转换price struct.unpack(q, raw_bytes)[0]大端 int64。4.3 现象重传请求ApplEndSeqNum0后行情网关返回ResendStatus4原因V1.11 明确要求ApplBegSeqNum必须显式填写表 4-5-2 注释「当 ResendType1 时生效」但很多 SDK 默认不填。ApplBegSeqNum0被网关解释为「从 0 开始」而实际数据从1开始导致无数据可返。解决重传请求中ApplBegSeqNum必须设为N_max 1ApplEndSeqNum设为0。示例beg_seq1003, end_seq0。4.4 现象证券实时状态消息中SecuritySwitchType36债券回售转售解析失败原因V1.11 新增开关类别36-债券回售转售2021-7 修订但部分旧版解码器的SecuritySwitchType字段仍按uint8解析而36超出uint8范围0-255导致溢出为36或解析错误。解决确认SecuritySwitchType在 FAST 字典中定义为uint16V1.11 第 32 页5.3 业务层域定义表中明确为16位解析时用uint16类型。4.5 现象盘后定价大宗交易ChannelNo300x快照中TradingPhaseCodeP但策略误判为连续竞价原因V1.06 将「盘后定价交易业务」更名为「盘后定价大宗交易」TradingPhaseCode新增P盘后定价大宗交易但部分策略库仍用旧代码D盘后定价交易判断。解决更新策略中的交易阶段判断逻辑if phase in [A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,V]:其中P专指盘后定价大宗交易。5. 进阶验证用三步法穿透式校验 Level2 数据质量守住策略生命线5.1 步骤一频道心跳监控——用ApplLastSeqNum做实时「脉搏计数器」频道心跳UA001TemplateID3001是唯一能实时反映行情网关健康状态的消息。V1.11 规定心跳间隔为 3 秒且ApplLastSeqNum字段记录该频道最后一条行情消息的序号。这不是一个摆设数字而是你的数据完整性探针心跳丢失检测若连续 2 个心跳周期6 秒未收到UA001立即触发断连重连。不要等 TCP 超时通常 30 秒以上那已错过千笔行情。序号跳跃预警记录每个频道的last_seq每次收到心跳时对比ApplLastSeqNum。若current_seq - last_seq 1000股票快照约 500ms 一条6 秒应增约 12 条说明网关侧积压严重需告警并检查 VSS 处理性能。频道结束标志EndOfChannel1当UA001中EndOfChannel1表示该频道数据流终止如某只股票退市应清理本地缓存并停止订阅。# 心跳监控器每频道独立 class HeartbeatMonitor: def __init__(self, channel_no: int): self.channel_no channel_no self.last_seq 0 self.last_time time.time() self.miss_count 0 def on_heartbeat(self, msg: dict): current_seq msg.get(ApplLastSeqNum, 0) now time.time() # 检查心跳间隔 if now - self.last_time 6: # 超过 2 倍心跳间隔 self.miss_count 1 if self.miss_count 3: alert(fChannel {self.channel_no} heartbeat timeout, reconnecting!) self._reconnect() # 检查序号跳跃 if current_seq self.last_seq: gap current_seq - self.last_seq if gap 1000: # 警惕积压 alert(fChannel {self.channel_no} seq gap {gap}, possible backlog!) self.last_seq current_seq self.last_time now self.miss_count 0 # 重置计数器这个监控器比任何日志都早 5 秒发现数据流异常。从那以后我每次上线新频道都强制走一遍心跳压力测试模拟 1000 次心跳丢包验证告警是否精准触发。5.2 步骤二快照-逐笔交叉验证——用「时间戳对齐」揪出解析错位Level2 数据的终极校验不是看单条消息而是看快照与逐笔在时间维度上的逻辑一致性。V1.11 中所有消息都含TransactTime交易时间戳单位为微秒YYYYMMDD-HH:MM:SS.ssssss。一个健康的市场快照中的LastPx最新成交价必须等于同一毫秒内逐笔UA202的LastPx快照中的BidPx1必须大于等于逐笔委托UA201的买一委托价。验证脚本逻辑按TransactTime毫秒级对齐快照与逐笔消息对每个毫秒窗口检查快照.LastPx 逐笔.UA202.LastPx最新成交价一致快照.BidPx1 逐笔.UA201.Price where Side1买一价不高于买委托价快照.AskPx1 逐笔.UA201.Price where Side2卖一价不低于卖委托价。# 时间戳对齐验证简化 def validate_alignment(snapshot_msgs: list, order_msgs: list): # 按毫秒分组 snap_by_ms group_by_millisecond(snapshot_msgs, TransactTime) order_by_ms group_by_millisecond(order_msgs, TransactTime) for ms, snaps in snap_by_ms.items(): orders order_by_ms.get(ms, []) if not snaps or not orders: continue for snap in snaps: # 检查最新成交价 ua202_prices [o[LastPx] for o in orders if o.get(MsgType) UA202] if ua202_prices and abs(snap.get(last_price, 0) - ua202_prices[0]) 0.01: alert(fMS {ms}: Snapshot LastPx {snap[last_price]} ! UA202 {ua202_prices[0]}) # 检查买一价逻辑 bid_px1 snap.get(bid_price, [0])[0] buy_orders [o[Price] for o in orders if o.get(Side) 1] if buy_orders and bid_px1 max(buy_orders) - 0.01: alert(fMS p a hrefhttps://download.csdn.net/download/book_yxc/46329590 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p