拓冰建站拓冰建站
首页 / 资讯中心 / 正文

深交所Level2 V1.11协议解析:STEP+FAST双层协议实战指南

简介本资源是深圳证券交易所官方发布的《Level2行情数据接口规范V1.11》PDF技术文档面向量化交易开发者、高频策略工程师及证券IT系统建设者解决行情接入标准化、多阶段交易场景适配与系统兼容性设计等核心问题。文档全面覆盖STEP协议下的快照行情、逐笔委托、证券实时状态、市场状态等消息格式详述港股通、盘后定价大宗交易、期权备兑转换、债券竞买等新增业务字段与交易阶段代码如波动性中断V、转融通出借35并强调用户系统需支持自动忽略未识别条目与开关类别的向后兼容机制。资源为单个682KB PDF文件结构清晰含修订历史、名词释义、会话机制及完整字段定义便于快速定位关键接口变更与落地实施要点。目前已有1988人学习下载是构建合规、稳定、可扩展Level2行情终端不可或缺的权威依据。1. 深交所Level2行情数据接口规范V1.11不是“文档”是量化系统上线前必须啃透的「信号校准手册」你写完一个涨停板策略回测年化42%实盘第一天就卡在委托队列第17位——不是模型错了是你的行情解析器根本没识别出「盘后定价大宗交易快照」频道300x里新增的MDEntryType xr买盘上限价字段你用Python写的FAST解码器在收到债券现券逐笔成交消息时直接抛KeyError: 1072因为V1.11第2021-8版新增的债券竞买行情模板ID 1072不在你硬编码的字典里你连上9129端口后心跳正常、Logon成功但3分钟没收到任何快照最后发现是DefaultApplVerID填了1.02却漏了文档第1页右下角那行小字“通信版本号为1.02但登录消息中DefaultApplVerID字段必须填写字符串FIX.5.0SP2”——这根本不是协议问题是深交所把STEP会话层和FAST应用层的版本语义做了物理隔离。这就是V1.11的真实战场它不教你怎么写策略而是用37页纸、127个标记字段、5类重传机制、7种心跳逻辑给你划出一条「从TCP连接建立到毫秒级行情落地」的完整信号链路。它面向的不是泛泛而谈的“量化新手”而是正在部署自营柜台系统、需要对接深交所MDGW生产环境的工程师——你得知道ChannelNo1071债券现券快照和ChannelNo2071债券现券逐笔的FAST模板ID为什么差1000得明白ResendType1时ApplEndSeqNum0触发的是内存最大序号而非磁盘归档序号得在SecuritySwitchType36债券回售转售出现时不改一行代码就能让旧系统自动跳过这个新开关。这不是阅读理解题是信号链路上的螺丝刀、万用表和示波器三件套。如果你还在用V1.05的字典跑V1.11的数据流恭喜你收到的每一条“快照”都是带时间戳的玄学。2. 协议栈拆解STEP会话层 FAST应用层 深交所Level2数据的「双核驱动」深交所Level2行情不是HTTP请求JSON响应那种直白结构而是典型的金融行业分层协议栈底层是STEPSecurities Trading Exchange Protocol负责会话管理与传输控制上层是FASTFIX Adapted for STreaming负责高效二进制编码。V1.11的全部技术张力就藏在这两层的咬合间隙里。跳过这一层直接写解码器等于没装刹车就踩油门。2.1 STEP会话层不是TCP连接而是带状态机的「行情专线」STEP会话层本质是轻量级FIX会话协议的深交所定制版它不处理业务数据只管“连接是否活着”、“消息是否丢包”、“重连怎么切”。关键参数全在文档第2章双端口强制分离实时数据走9129重传服务走9130仅网络版MDGW提供。注意9129端口不支持重传请求所有UA002重传消息必须发往9130。很多团队第一次调试失败就是因为把重传请求发到了实时端口MDGW静默丢弃——它连拒绝消息都不回。DefaultApplVerID陷阱文档2.1.1节明确写着“通信版本号为1.02”但紧接着在“会话层域定义”附录里注明DefaultApplVerID字段必须填字符串FIX.5.0SP2。这是STEP协议对FIX标准的继承要求和通信版本号1.02完全无关。填错会导致Logon被拒错误码藏在Text字段里内容是Invalid ApplVerID但文档没告诉你这个错误码对应什么字段。流量控制阈值不可配文档2.1.5节说“累积待发送消息超过设定阈值后断连”但阈值是多少怎么调文档一字不提。实测经验网络版MDGW的阈值约在5000条未ACK消息。这意味着你的接收线程如果处理速度低于5000 msg/s按平均120字节/条算约0.6MB/s就会被踢。解决方案不是调阈值你没权限而是用select()或epoll做非阻塞接收多线程解码确保接收缓冲区永不堆积。# 验证STEP会话层连通性的最小可行命令Linux # 注意必须用ncat不是nc因需TLS握手虽文档说非加密但MDGW实际要求STARTTLS echo -e 8FIX.5.0SP2\x0190\x0135A\x01341\x0149YOUR_SENDER_COMP_ID\x015220210801-00:00:00.000\x0156MDGW\x0110123\x01 | \ ncat --ssl --ssl-trustfile /path/to/mdgw_ca.crt mdgw.szse.cn 9129 -w 5提示ncat是nmap套件中的高级netcat支持SSL/TLS/path/to/mdgw_ca.crt是深交所提供的根证书必须配置否则SSL握手失败5220210801-00:00:00.000是UTC时间戳格式必须严格为YYYYMMDD-HH:MM:SS.sss少一位都会被拒。2.2 FAST应用层二进制压缩的「行情字节流」解码器即生命线FAST是V1.11真正的数据心脏。它把原本需要几百字节的XML或JSON行情压缩成几十字节的二进制流。但代价是你必须用精确匹配的FAST模板Template才能解码。V1.11的FAST设计有三个反直觉点模板ID与频道强绑定表4-2规定模板ID 3000-3999为公共消息4000-15999为实时行情。但关键在“实时行情”的分配逻辑ChannelNo1011股票快照对应FAST模板ID4001ChannelNo1071债券现券快照对应4071ChannelNo2071债券现券逐笔对应5071。模板ID 4000 ChannelNo后两位数字1011→0011→11→4011错是1011→取后两位11→4000114011但实际是4001。真实规则是深交所内部有一张映射表V1.11新增的债券竞买行情ChannelNo401x其FAST模板ID是1072见文档第IV页修订说明完全打破4000规则。所以硬编码模板ID是死路必须动态加载。RawDataLength是FAST体长度不是STEP消息总长STEP消息层中95RawDataLength字段只表示96RawData字段的字节数不包括STEP Header/Trailer。很多C解码器用memcpy(buf, raw_data_ptr, raw_data_length)直接拷贝结果解码器读到一半就EOF——因为RawData里可能包含多条FAST消息如一个快照频道一次发3只股票的快照FAST解码器必须循环读取直到RawData耗尽。FAST字典前值必须重置文档表4-1脚注强调“解码FAST消息体前应该重置解码器的FAST字典前值”。这是FAST协议的核心机制用delta编码减少重复字段。比如连续两条股票快照第二条只传变化的LastPx其余字段沿用第一条的值。但如果解码器没重置字典前值第二条就会用第一条的Symbol去解第二条的SecurityID结果全是乱码。重置动作必须在每次处理96RawData前执行不是每次TCP包。2.3 会话层与应用层的致命耦合心跳、重传、恢复的三层嵌套STEP和FAST的协作不是松耦合而是深度嵌套。一个典型场景你收到ChannelNo2011股票逐笔的心跳1350ApplLastSeqNum1005但你本地只收到1003条说明丢了2条。此时你要在STEP层向9130端口发UA002重传消息STEP消息类型35U在FAST层RawData里填TemplateID3002ResendType1ApplBegSeqNum1004ApplEndSeqNum1005在业务层收到重传响应后检查10076ResendStatus若1则成功若2则部分完成需再查1182/1183确认范围若4则“数据不可用”意味着MDGW内存里已刷掉只能等下次全量快照。这三层嵌套导致一个经典翻车点重传响应里的RawData其FAST模板ID不是3002而是你请求的频道原模板ID如4001。因为MDGW返回的不是“重传指令”而是“重传的数据本身”。很多团队以为重传响应也是TemplateID3002结果用错模板解码得到一堆None。3. 数据通道实战从频道代码到行情字段一张表吃透V1.11的12类Level2数据流V1.11定义了12类独立行情通道每类有专属频道代码、消息类型、FAST模板和业务含义。新手常犯的错是把ChannelNo1011股票快照和ChannelNo2011股票逐笔当成同一数据源的不同粒度其实它们是完全独立的发布系统快照是定时广播如每3秒逐笔是事件驱动每笔委托/成交即发。下面这张表按生产环境优先级排序标出每个通道的“血泪参数”和“必验字段”。频道代码数据类型典型用途FAST模板ID关键字段必验血泪参数说明1011股票快照行情盘口五档、最新价、成交量4001MDEntryType0(Bid),1(Ask),2(Trade),4(Open),5(Close)V1.11新增MDEntryTypexj(加权平均价涨跌BP)旧系统若未忽略此类型会因switch-case缺分支而崩溃。文档4.1兼容性要求第4条即为此设。2011股票逐笔委托每笔挂单价格、数量、方向5001MDEntryTypeA(Add),D(Delete),M(Modify),U(Update)MDEntryTypeU在V1.11中首次出现表示“更新委托数量”但旧版FAST字典无此值解码器需默认映射为M。2012股票逐笔成交每笔撮合成交成交价、量、时间5002MDEntryType2,TradeVolume,TradePrice,TransactTimeTransactTime是纳秒级时间戳如162891234567890123但文档未说明时区。实测为北京时间UTC8非UTC。用错时区会导致tick对齐偏差8小时。3001盘后定价大宗交易快照盘后定价的买卖盘口、参考价4301MDEntryTypexr(买盘上限价),xs(买盘下限价),xt(卖盘上限价),xu(卖盘下限价)这4个类型是V1.112021-8新增专用于港股开市前时段。若解析器遇到xr报KeyError说明FAST字典未更新。文档第IV页修订说明明确标橙色。1071债券现券快照行情债券最新净价、全价、应计利息4071MDEntryType9(加权平均价),xk(昨收盘加权平均价),xi(参考价)MDEntryTypexi参考价在V1.11中同时出现在1011股票和1071债券中但含义不同股票是集合竞价参考价债券是估值机构提供的公允价。字段同名不同义业务层必须按ChannelNo分流。2071债券现券逐笔成交债券每笔匹配成交5071MDEntryType2,SecurityIDSource8(ISIN),SecurityID债券代码SecurityIDSource8表示ISIN码但国内债券常用1(CUSIP)或2(SEDOL)。V1.11强制要求债券用ISIN旧系统若只认1会丢弃所有债券逐笔。4011债券竞买委托债券竞买预约、竞买委托1072MDEntryTypeY(竞买预约),Z(竞买委托),AA(竞买成交)模板ID1072是V1.11最大坑它不在4000-15999区间且文档未在表4-2列出。必须从修订说明中手动提取。不加载此模板竞买行情全丢。5001港股实时行情港股通标的实时五档4501MDEntryType0~4,SecurityExchangeXHKG,CurrencyHKDSecurityExchangeXHKG是港股交易所代码但V1.11要求解析器必须支持XSHG(上交所)、XSHE(深交所)、XHKG三者并存。旧系统若用if exchangeXHKG硬判断会漏掉AH股同步行情。10指数快照行情深证成指、创业板指等4010MDEntryType4(Open),5(Close),7(High),8(Low),9(Last)指数快照无MDEntryType0/1无买卖盘口若解析器强行读Bid/Ask字段会返回0或空。必须先校验MDStreamID是否为指数类文档表3-1中频道10/11。2公告消息上市公司公告、监管文件3005NewsID,RawDataFormat1(文本),2(PDF),3(XML)RawDataFormat2PDF时RawData字段是base64编码的PDF二进制流。很多Python解码器用raw_data.decode(utf-8)直接报错必须先base64解码再写入文件。1市场实时状态全市场交易阶段开盘、休市等4005TradingPhaseCodeO(Open),C(Close),V(Volatility Interruption)TradingPhaseCodeV波动性中断是V1.02新增但V1.11要求必须支持。若策略在V阶段仍发单会被交易所风控系统拦截。业务层需监听此字段自动暂停下单。5000用户信息报告信息商上报终端用户数3003UserNum,VersionCode01(现场版),02(网络版)此频道仅信息商需发券商自营系统不用。但若误发MDGW会记录日志。文档3.4节强调“只有信息商用户行情系统需要发送”新手易忽略此限定。注意所有MDEntryType值均来自文档表4-4至4-12但V1.11新增值如xr,xs,xu,xj,xk,xi在文档中分散于各修订历史未在主数据字典表中集中列出。生产环境必须维护一份动态更新的MDEntryType映射表不能依赖文档静态表格。4. 避坑指南V1.11生产环境踩过的7个真实深坑与血泪修复方案在深交所MDGW生产环境跑通V1.11不是靠文档读懂而是靠错误日志喂出来的。以下7个坑全部来自真实上线案例每个都附带现象、根因和可立即执行的修复代码/配置。4.1 现象TCP连接稳定心跳正常UA001每3秒收到但ChannelNo1011快照始终为0条原因Logon消息中DefaultApplVerID填了1.02但MDGW要求必须为FIX.5.0SP2。MDGW静默接受Logon但后续所有业务消息含快照均不下发。解决严格按文档“会话层域定义”附录填写。C示例// 错误写法导致快照丢失 msg.setField(1128, 1.02); // 1128 DefaultApplVerID // 正确写法V1.11强制要求 msg.setField(1128, FIX.5.0SP2);4.2 现象ChannelNo2011逐笔委托消息中MDEntryTypeUUpdate字段解析为None导致委托更新丢失原因FAST字典未包含U值。V1.05字典只有A/D/MV1.11新增U但字典文件未更新。解决下载V1.11配套FAST字典非文档PDF或手动扩展字典。Python fastfix库示例# 在FAST模板加载后动态添加 template.add_field(MDEntryType, U, Update) # 或更安全用字典映射未知值默认为M md_entry_type_map {A:Add, D:Delete, M:Modify, U:Modify} entry_type md_entry_type_map.get(raw_value, Unknown)4.3 现象重传ChannelNo2011时ApplEndSeqNum0但MDGW返回ResendStatus4数据不可用原因ApplEndSeqNum0表示“取内存最大序号”但MDGW内存只缓存最近10000条。若你断连超10秒序号1004~1005已被刷出内存。解决重传前先查UA001心跳的1350ApplLastSeqNum若ApplLastSeqNum - local_max_seq 10000则放弃重传等待下次快照同步。Go语言示例// 收到心跳后更新 lastSeq : getIntField(msg, 1350) if lastSeq-localMaxSeq 10000 { log.Warn(seq gap too large, skip resend, wait for snapshot) return }4.4 现象ChannelNo1071债券快照中SecurityIDSource8ISIN但解析出的SecurityID是乱码如\x00\x00...原因SecurityID字段在FAST模板中定义为length-delimited string但V1.11债券模板要求其长度为12字节ISIN固定12位而解码器按变长字符串读取读到\x00就截断。解决在FAST模板中显式指定SecurityID长度为12。XML模板片段!-- 正确强制12字节 -- field nameSecurityID id48 typeString length12/ !-- 错误默认变长 -- field nameSecurityID id48 typeString/4.5 现象ChannelNo4011债券竞买行情完全收不到Wireshark抓包显示RawData为空原因ChannelNo4011对应的FAST模板ID是1072但你的解码器只加载了4000-15999区间模板1072被忽略。解决V1.11修订说明明确1072为竞买模板必须单独加载。Java fastfix示例// 加载主模板后额外加载1072 TemplateLoader.loadTemplate(templates/1072.xml); // 手动指定路径 // 或更鲁棒扫描所有XML按template id...动态加载4.6 现象ChannelNo5001港股行情中TransactTime解析为1970-01-01时间戳全错原因TransactTime是纳秒级整数如162891234567890123但解码器用int32读取高位截断。解决必须用int64读取。C示例// 错误int32_t time_ns getInt32(msg, 60); // 正确int64_t time_ns getInt64(msg, 60); int64_t time_ns getInt64(msg, 60); // 60 TransactTime struct timespec ts; ts.tv_sec time_ns / 1000000000LL; ts.tv_nsec time_ns % 1000000000LL; // 转为北京时间UTC8 ts.tv_sec 8 * 3600;4.7 现象ChannelNo2公告消息中RawDataFormat2PDF但RawData字段解码后PDF损坏Acrobat报“文件已损坏”原因RawData是base64编码的二进制流但解码器用了raw_data.decode(utf-8)导致UTF-8非法字符被替换为。解决base64解码后直接写二进制文件。Python示例import base64 raw_data getRawData(msg) # 获取96字段原始bytes pdf_bytes base64.b64decode(raw_data) # 必须base64解码 with open(fnews_{news_id}.pdf, wb) as f: f.write(pdf_bytes) # 直接写二进制勿encode5. FAST解码器实战用Pythonfastfix构建V1.11兼容的零丢包行情引擎V1.11的终极挑战不是理解文档而是写出一个能在生产环境扛住每秒2万条消息、零丢包、自动适配新增字段的FAST解码器。我用Pythonfastfix库非官方是社区维护的Cython FAST解码器实现了这个目标核心在于三个设计模板热加载、字段动态映射、心跳驱动的重传调度。下面给出可直接运行的最小可行代码并解释每个设计为何能避开V1.11的坑。5.1 模板热加载告别硬编码拥抱V1.11的“新增即忽略”哲学V1.11的兼容性要求文档4.1说“新增行情条目类别VSS应能自动忽略”。但“忽略”不是靠try-catch而是靠模板加载时的字段白名单。我们把所有已知MDEntryType包括V1.11新增的xr,xs等预定义在一个JSON里解码器只处理白名单内的字段未知字段直接跳过。# templates/v111_whitelist.json { MDEntryType: [0,1,2,4,5,9,xj,xk,xi,xr,xs,xt,xu,Y,Z,AA], SecuritySwitchType: [1,2,3,34,35,36], TradingPhaseCode: [O,C,V,A] }解码器核心逻辑import json import fastfix class SZSEFASTDecoder: def __init__(self): self.whitelist json.load(open(templates/v111_whitelist.json)) self.templates {} self._load_all_templates() # 加载4000-15999及1072 def _load_all_templates(self): # 加载主区间模板 for tid in range(4000, 16000): try: self.templates[tid] fastfix.Template(ftemplates/{tid}.xml) except FileNotFoundError: pass # 强制加载V1.11新增的1072 self.templates[1072] fastfix.Template(templates/1072.xml) def decode(self, channel_no: int, raw_data: bytes) - list: # 根据channel_no推导模板ID简化版实际需查映射表 tid self._get_template_id(channel_no) template self.templates.get(tid) if not template: raise ValueError(fNo template for channel {channel_no}, tid {tid}) messages [] # FAST解码器返回多条消息一个RawData可能含N条FAST消息 for msg_bytes in fastfix.decode_stream(template, raw_data): parsed {} for field in template.fields: if field.name in self.whitelist and \ msg_bytes.get(field.id) is not None: # 只解析白名单内的字段 parsed[field.name] self._parse_field(field, msg_bytes.get(field.id)) messages.append(parsed) return messages def _get_template_id(self, channel_no: int) - int: # 真实映射需查深交所发布的映射表此处为示意 mapping { 1011: 4001, 2011: 5001, 1071: 4071, 2071: 5071, 4011: 1072, 5001: 4501 } return mapping.get(channel_no, 4001) # 默认fallback逻辑说明whitelist确保新增MDEntryTypexr时解码器不会因if entry_type 0: ... elif entry_type 1: ...的硬编码case而崩溃而是直接跳过_load_all_templates显式加载1072堵住债券竞买行情丢失的漏洞decode_stream处理一个RawData里的多条FAST消息避免因memcpy长度错误导致的半包解析。5.2 字段动态映射用字典代替switch-case应对V1.11的“字段语义漂移”V1.11中同一字段名在不同频道含义不同如MDEntryTypexi在股票是参考价在债券是估值价。硬编码switch-case会失控。我们用频道字段名二维字典做动态路由# field_router.py FIELD_HANDLERS { (1011, MDEntryType): lambda v: {type: stock_bid if v0 else stock_ask if v1 else stock_trade}, (1071, MDEntryType): lambda v: {type: bond_net_price if v9 else bond_ref_price if vxi else bond_ytm}, (2011, MDEntryType): lambda v: {action: add if vA else delete if vD else update}, } def handle_field(channel_no: int, field_name: str, value: str) - dict: handler FIELD_HANDLERS.get((channel_no, field_name)) if handler: return handler(value) return {raw_value: value} # 未知字段原样返回 # 在decode()中调用 for field in template.fields: if field.name in self.whitelist: routed handle_field(channel_no, field.name, msg_bytes.get(field.id)) parsed.update(routed)参数说明FIELD_HANDLERS是频道字段的二维路由表handle_field根据channel_no选择处理逻辑。这样MDEntryTypexi在1011和1071频道走不同分支彻底解决“同名不同义”问题。新增频道只需加一行字典无需改核心逻辑。5.3 心跳驱动的重传调度用ApplLastSeqNum做状态机实现零丢包V1.11的重传不是“丢了就重传”而是“心跳暴露缺口再精准补”。我们维护一个ChannelState对象用心跳更新last_seq用接收更新recv_seq缺口自动触发重传from dataclasses import dataclass import threading dataclass class ChannelState: channel_no: int last_seq: int 0 recv_seq: int 0 lock: threading.Lock None class ResendScheduler: def __init__(self): self.channels {} self.lock threading.Lock() def on_heartbeat(self, channel_no: int, last_seq: int): with self.lock: state self.channels.setdefault(channel_no, ChannelState(channel_no)) state.last_seq max(state.last_seq, last_seq) def on_message(self, channel_no: int, seq_num: int): with self.lock: state self.channels.get(channel_no) if not state: return if seq_num state.recv_seq 1: state.recv_seq seq_num elif seq_num state.recv_seq 1: # 发现缺口recv_seq1 到 seq_num-1 丢失 self._trigger_resend(channel_no, state.recv_seq 1, seq_num - 1) state.recv_seq seq_num def _trigger_resend(self, channel_no: int, beg: int, end: int): # 发送UA002重传消息到9130端口 msg build_resend_msg(channel_no, beg, end) send_to_mdgw_9130(msg) # 在主循环中 scheduler ResendScheduler() def process_ua001_heartbeat(msg): channel_no getInt(msg, 10201) last_seq getInt(msg, 1350) scheduler.on_heartbeat(channel_no, last_seq) def process_ua201_202_message(msg): channel_no getInt(msg, 10201) seq_num getInt(msg, 1182) # ApplSeqNum scheduler.on_message(channel_no, seq_num) 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
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门