ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

深交所Level2行情V1.11解析:从协议骨架到Tick复权与订单簿校验

深交所Level2行情V1.11解析:从协议骨架到Tick复权与订单簿校验 简介深交所Level2行情数据接口规范V1.11是官方发布的STEP行情数据接口标准面向高频交易、量化策略及涨停板交易等专业场景提供Level2五档行情、逐笔委托、快照消息等完整数据规则。资源共1个PDF文件约682KB内容涵盖会话机制、消息字段定义、行情类别与开关取值并梳理了2013年至2021年的历次修订包括盘后定价交易、港股通、期权备兑、债券现券等扩展功能。接口兼容性要求已明确说明用户系统可自动忽略新增条目便于现有系统平滑升级。文档目录结构清晰便于按章节直接查阅具体字段定义与示例。已有1990人学习下载适合券商、信息商、量化开发者及交易所接入用户作为接口开发与运维的权威参考。1. 深交所Level2行情V1.11快照秒级刷新盘口只能靠逐笔拼做量化的人第一次看深交所Level2行情接口规范V1.11多半被两个数字劝退快照每秒只有一两张逐笔委托和逐笔成交却是实时流。光靠快照你根本还原不出真实盘口中间那些撤单和成交撮合全部要用逐笔消息去补。这份规范解决的就是这件事——把登录鉴权、消息帧、快照、逐笔委托ORD、逐笔成交TRD的字段和时序约束定义清楚让行情开发商、私募和券商自研系统有同一份“翻译稿”。它的目标读者有两类负责接入深交所行情网关的工程师和做回测落地的量化研究员。前者关心协议骨架和鉴权流程后者关心字段含义和Tick复权。我的建议是先跑通二进制链路再研究STEP链路先解析消息头再去抠字段。下面按这个顺序带你走一遍最后把最容易翻车的几个坑单独拉出来讲。2. 先读消息头再谈业务V1.11的协议骨架与连接生命周期把V1.11当成一个黑匣子去调是最容易浪费一周时间的做法。协议栈的顺序是固定的TCP连接、登录鉴权、按帧收消息、按消息头分发、按消息体解析。任何一步错位后面都是乱码。所以这一章先把骨架立住你后面填业务字段时才不会迷路。2.1 两套应用层协议二进制Binary与STEPV1.11规范里同时存在两套应用层协议Binary定长加变长的紧凑二进制STEP基于文本标签的类FIX协议。生产环境几乎都用Binary带宽省、解析快STEP更多出现在跨部门联调和错误排查现场因为它每个字段都有英文标签抓包工具里一眼能看到“Price10.50”这种内容。常见做法是行情网关同时支持这两种编码。你选哪种取决于接入配置登录报文里通常有一个字段指定语言或编码类型或者你在网关的客户端配置里声明。我的习惯是先用STEP把业务跑通确认字段语义全对再切换Binary压性能和时延。反过来容易心态崩二进制解析错一位后面全部错位你很难判断是网关问题还是自己问题。对比项BinarySTEP编码风格紧凑二进制文本标签类FIX调试成本需要自写解析器可直接看报文文本带宽占用低高典型场景生产接收、压测联调排错、跨部门协作2.2 消息头24字节“解析错一位后面全错”的根因不管是快照还是逐笔每条业务消息前面都顶着一个定长消息头。公开资料里深交所Binary协议的消息头为24字节定长大端序。我用C结构体把它固定下来#pragma pack(push, 1) typedef struct { uint16_t msg_type; /* 业务消息类型快照、ORD、TRD、登录等 */ uint32_t body_len; /* 消息体长度不含消息头 */ uint16_t version; /* 协议版本V1.11 阶段固定版本号 */ uint32_t seq_no; /* 消息序号断线重连时用来对齐 */ uint64_t timestamp; /* 发送时间精度以规范定义为准 */ uint32_t source_id; /* 源标识多路网关时用来去重 */ uint16_t reserved; /* 保留字段 */ uint16_t checksum; /* 部分链路启用未启用时全 0 */ } szse_l2_header; #pragma pack(pop)这个结构体不建议手敲进生产代码而是用代码生成器从规范的机器可读描述里生成。手敲容易漏字节对齐尤其在Windows和Linux下默认对齐不一样一旦漏了或多了关键字解析出来的body_len就是错的整条消息全部偏移。消息头里的msg_type是分发键body_len告诉你这条消息的边界在哪seq_no做序列对齐timestamp是带了但注意它是网关发送时间不是业务发生时间。逐笔成交消息体里还有一个自己的时间戳那才是撮合时点。这两个时间戳混用是后面避坑章要讲的常见错误。2.3 登录、心跳与重连连接生命周期的三个动作V1.11的连接生命周期分三步TCP连上后先发登录报文网关回登录响应然后进入正常行情推送断线后重连再按最新序号补拉行情。登录报文里有两个关键点。一是用户名、密码的加密常见做法是MD5加盐后再做一层编码盐值拼接顺序在规范附录里二是登录响应里会带一个会话标识后续所有业务报文建议都带上它方便网关做会话关联。密码这里最容易踩的坑是字符编码中文用户名用GBK还是UTF-8转出来的字节不同MD5结果完全不同测试环境里八成鉴权失败都出在这个字符集上。心跳一般由客户端主动发间隔3秒或5秒网关连续几次没收到就断开连接。这是TCP层和应用层要同时做的TCP层靠系统keepalive兜底应用层靠你定时发心跳报文。重连之后先别急着收行情拿本地最大的seq_no和网关对一次差多少补多少。补拉回来的消息先落盘再回放避免一边收实时流一边补历史流把处理顺序搞乱。这个习惯能省掉后面无数笔对账的麻烦。3. 用Python把快照和逐笔成交拆成Tick字段映射与解析要点协议骨架立住之后就该碰业务字段了。生产环境用C或Java但原型验证阶段我强烈建议用Python——struct.unpack一行就是一个字段调试迭代速度快一个量级。这一章给你能直接跑的最小解析代码字段偏移以你手上的V1.11文档为准不同版本会有微调。3.1 快照消息MKT先看十档再看统计量深交所Level2快照消息里最值钱的字段不是十档盘口而是成交笔数和总买总卖。十档盘口每个行情源都能给但成交笔数是逐笔成交累加的结果你本地累出来的数和快照里的数对得上就说明你的逐笔流没漏。快照常见布局是证券代码、时间戳、最新价、十档买卖价、十档买卖量、总买量、总卖量、成交笔数后面再跟统计扩展字段。import struct def parse_mkt(body: bytes) - dict: # 快照是定长块证券6 时间8 最新价4 十档买/卖 统计量 base 6s I I 10i 10I 10i 10I I I I base_size struct.calcsize(base) if len(body) base_size: raise ValueError(MKT body too short: %d % len(body)) vals struct.unpack_from(base, body) return { symbol: vals[0].decode(ascii), time: vals[1], last_px: vals[2], bid_px: vals[3:13], bid_vol: vals[13:23], ask_px: vals[23:33], ask_vol: vals[33:43], total_bid_vol: vals[43], total_ask_vol: vals[44], trade_count: vals[45], }这段代码的核心是定长字段全部走一个fmt字符串变长扩展字段单独处理。十档价格我直接用int数组读进来到位数转换放到解析后面统一做不在这一层处理。原因很简单有的源用定点数表示价格有的直接用浮点混在解析层会越改越乱价格精度问题应该由统一的转换函数负责。trade_count是对账锚点。你本地解析完逐笔成交后维护一个计数器定时和快照里的trade_count比对不一致就说明漏包或重复处理。把这条检查写成一个独立函数每个快照消息都跑一遍开销很低但能救你于联调末期——等你发现的时候通常已经漏了几千笔。3.2 逐笔委托ORD与逐笔成交TRD最小可用的拆包代码ORD消息体是委托维度TRD是成交维度。两者都带证券代码、方向、价格、数量差异在标识ORD有委托序号TRD有成交序号以及匹配的买卖订单号。深交所逐笔消息一个帧里可以包含多只股票的记录所以解析要按条目循环TRD_ITEM 6s I I I I I I I I B B I def parse_trd(body: bytes) - list: item_size struct.calcsize(TRD_ITEM) items [] for off in range(0, len(body) - item_size 1, item_size): v struct.unpack_from(TRD_ITEM, body, off) items.append({ symbol: v[0].decode(ascii), seq: v[1], trade_px: v[2], trade_qty: v[3], bid_order: v[4], ask_order: v[5], exec_type: v[6], side: v[7], # 主动方向买或卖 ts: v[8], }) return items注意每个条目开头都带证券代码这是深交所逐笔消息的特点你不必按股票分组才能解析直接一条条append到全局队列后续用symbol字段分桶即可。方向字段的枚举值V1.11用数字表示买卖和撤单我建议直接在解析层保留数字到因子计算时再用映射表翻译这样格式调整时只改映射表不用回头动解析层。3.3 用msgType做分发一个能撑住V1.11全部业务消息的骨架一个健壮的消息分发循环本质上只做三件事读头、按msg_type查表、把body交给对应的解析函数。我用字典做路由msg_type的具体数值以你手上的规范附录为准def dispatch(data: bytes) - dict: header, body split_header(data) handler HANDLERS.get(header[msg_type]) if handler is None: # 未知消息类型记录日志不要中断主循环 return {type: unknown, seq: header[seq_no]} return handler(body) HANDLERS { LOGIN_RESP: parse_login_resp, MKT: parse_mkt, ORD: parse_ord, TRD: parse_trd, }这里有个容易被忽略的点分发字典里必须包含对未知类型的处理。测试环境偶尔会出现规范之外的消息类型最常见的是网关升级期间临时推送的测试消息。你要是直接抛异常整个接收线程就挂了。正确做法是记一条warning日志记下seq_no和body_len然后继续读下一条。分发骨架写完后再逐条加字段校验价格不能为负、数量不能为零、时间戳不能倒退。校验放在解析之后而不是之前因为解析失败时你已经知道是哪条消息直接跳过比中断强。我还会给这个模块单独定一条代码规范所有解析函数只做字节到字段的转换不做任何业务判断业务判断全部放上层。这条规矩比检查代码规范里的缩进规则更能救你——解析层保持无状态出问题时才能快速隔离是网络层、解析层还是策略层的问题。4. Tick复权算法成交价量怎么才能不受除权日影响解析完Tick你以为数据能直接用了但除权除息日会给你上一课某只股票10转10的第二天前一天的收盘价和后一天的开盘价差出一倍直接在原始价格上算收益率回测信号全是假的。日线复权是复权OHLC四个价Tick复权要复权每一笔成交的价和量算法逻辑不一样工序也更细。4.1 复权为什么是Tick数据独有的坎除权除息日交易所会对股价做除权处理送股、转增、派息都会让股价跳空。日线数据一天只有一根K线复权只需调整几个关键价Tick数据一天有几十万笔成交前后两笔价格可能从10块跳到5块中间没有任何过渡。如果你不做复权订单簿阈值、收益率序列、流动性指标全部会被除权日污染而且污染是永久性的——你没法在策略层通过简单过滤把它去掉。常见做法是因子法把除权除息公告转换成每日价格因子再按时间累计对历史Tick逐笔调整。这里有个关键认知成交量必须跟着价格一起调整。价格打折到原来的一半同样成交笔数对应的成交额不变所以成交量要除以价格因子。只调价不调量回测里的流动性判断会失真这是Tick复权最容易漏的一步。4.2 用累计因子做前复权、后复权一段可跑的Python因子法的核心是构建“价格调整因子序列”然后把因子乘到对应日期之前的每一笔成交上。下面这个简化版本用的是Pandasimport pandas as pd def build_adj_factor(dividend_events: pd.DataFrame) - pd.Series: # dividend_events 列date、factor # factor 表示除权后的价格折扣0.5 表示价格打五折 s dividend_events.set_index(date)[factor] # 后复权因子按时间累计早于除权日的价格乘上累积值 s s.sort_index().cumprod() return s def adjust_ticks(tick_df: pd.DataFrame, adj: pd.Series) - pd.DataFrame: df tick_df.copy() df df.merge(adj.reset_index(), left_ondate, right_ondate, howleft) df[factor] df[factor].fillna(1.0).cumprod() df[px_adj] df[px] * df[factor] df[qty_adj] df[qty] / df[factor] return df这段代码的逻辑是先按日期把因子排序除权日越早因子累积越大然后把每个交易日对应的因子merge到Tick数据上向前填充再累乘得到每一笔成交当天的复权因子。价格乘以因子得到复权价数量除以因子保持成交额守恒。参数上factor的来源建议直接用行情源发布的除权除息公告不要自己从价格里反推反推出来的因子和交易所口径对不上回测和实盘会系统性偏离。4.3 参数与边界复权基准日、精度、新股和ST复权算法调通容易调准难。参数边界上我踩过几个值得说明的位置。复权基准日前复权和后复权的基准不同前复权以最新交易日为基准后复权以最早上市日为基准同一个Tick数据用两种口径跑出来的收益率序列一致但绝对价格不同做订单簿回测时一定要明确你用的是哪一种混用了你会看到同一策略在不同时段表现完全不一样。价格精度复权后价格是浮点乘法累计下来会出现1e-6级别的误差。做Tick回测建议把价格整数化用最小变动价位做单位或者统一用Decimal不要在float上直接比较两个价格。新股上市首日没有除权除息记录因子为1不参与累计ST股票的除权频率更高因子变化更密要单独建一张表维护避免漏掉因子导致后续所有日期全部错位。检查方法也简单把复权后的序列里单日价格跳变超过阈值比如30%的记录全部列出来人工核对当天是否有除权事件。没有事件的跳变就是数据质量问题。5. 接入联调的避坑清单从鉴权失败到序号断层接入阶段最容易出问题的地方不在协议正文而在测试联调规范没定清楚。下面五条是我在多个项目里反复踩过的坑现象、原因、解决方式一次讲完你照着排查能省掉大半联调时间。5.1 登录鉴权失败密文长度永远对不上现象登录报文发出去网关回“鉴权失败”日志里看不到任何具体原因你反复核对用户名密码却找不出问题。原因密码加密后字节长度与规范不符最常见两类——盐值拼接顺序写反了规范是先拼盐再拼密码你写成了密码在前或用户名里的中文字符用了平台默认编码GBK和UTF-8转出来的字节不同MD5结果完全不同。解决把加密前和加密后的字节各做一份十六进制dump与规范附录里的样例输入逐字节比对先拿纯英文用户名跑通链路再切换中文用户名。这一步能立刻定位是字符集问题还是盐序问题。5.2 快照的成交笔数与本地累计不一致现象解析一切正常但快照里的trade_count比本地逐笔累计的成交笔数多几十笔每天偏差还不固定。原因漏了集合竞价前后的特殊成交记录或者断线重连后补拉回来的消息没有走同一个计数入口直接插进了实时队列。解决把补拉消息和实时消息统一路由到同一个处理函数补拉数据先落盘再回放不允许绕过计数器直接入队列。每天收盘后跑一次对账脚本差值超过阈值就告警把问题暴露在交易结束后而不是第二天开盘前。5.3 序号断层重连之后SeqNo倒退了现象断线重连后收到的第一条消息seq_no比本地记录的最大值小几百你以为网关把历史重发了一遍其实没有。原因不同网关源之间的序号空间是独立的source_id不同seq_no不能混着比。你本地把多个源的序号存进了同一个变量重连后拿A源的seq_no去对比B源当然看起来倒退。解决按source_id分桶维护seq_no重连只对比同一source_id的最新值。跨源的序号只做消息去重不做连续性判断连续性判断必须限定在单源内。5.4 集合竞价那段快照开盘价会跳变现象9:15到9:25之间快照里的最新价来回跳拿这个区间算波动率、算开盘动量策略结果乱得没法看。原因集合竞价阶段撮合还没完成快照里的最新价是参考价而不是成交价字段含义和连续竞价阶段不一样。规范里这个阶段的消息会带有阶段标志位你没读它。解决在解析层就把阶段标志位提取出来集合竞价阶段的快照单独打标不进入策略计算管道。实在要用只取集合竞价的最终结果不要取中间过程的价量数据。5.5 STEP和Binary混用联调和生产两套口径现象STEP链路调通了切到Binary后部分字段对不上价格能对上、量对不上或者反过来。原因STEP和Binary的字段顺序不一样报文头的字段顺序也不一样你拿STEP的字段顺序套Binary解析结果必错。解决按规范分别维护两套解析头描述写一个自动对比脚本同一组输入分别走两种协议解析对每个字段做diff。联调时用STEP定位业务问题上线前用Binary做回归测试两套口径都过了再切生产。6. 用快照加逐笔组装订单簿验证解析正确性的一个具体技巧全部解析调通之后怎么证明你是对的我的做法是别急着全量组装订单簿先拿快照和逐笔成交做三个字段的互验快照买一价、逐笔主动方向、成交价。这三个字段如果自洽说明快照和逐笔数据在你手里是同一份真实盘的切片而不是两套各说各话的乱码。验证逻辑如下快照里买一价是当前最优买价如果接下来一笔逐笔成交的成交价低于或等于买一价这笔大概率是主动卖出会消耗买一档的量。于是你维护一个临时变量初始为快照买一量每来一笔主动卖出的成交就把它扣掉扣到0时下一张快照的买一价和买一量必须发生变化否则说明中间有成交没推给你或者方向判断错了。def check_snapshot_vs_tick(mkt: dict, tick_q: list) - bool: bid_px, bid_vol mkt[bid_px][0], mkt[bid_vol][0] while tick_q and tick_q[0][px] bid_px: t tick_q.pop(0) bid_vol - t[qty] if bid_vol 0: # 快照量被消耗完还没等到下一张快照大概率漏了成交或方向错 return False return True这段代码的关键参数是价格比较方向成交价小于等于买一价才算主动卖出大于买一价的部分属于主动买入不消耗买一量。实际操作时建议用连续竞价阶段的数据跑避开集合竞价从9:30开始取前1000笔做循环校验每遇到“buy一量被扣穿”就打印快照序号和成交序号人工去看这两条消息之间发生了什么。这个技巧不是全流程自动化但它能在你刚开始接深交所Level2行情时用最少的排查成本建立起“快照和逐笔是一致的”这个信心。我最早做这套系统时只信快照里的买一量结果某天下午盘中发现量对不上账最后一查是上午有一段逐笔成交因为序号断点被当成重复消息过滤掉了。从那以后每个交易日开盘前跑一遍这个对账脚本就成了固定动作算是我个人接入Level2最值得保留的一个习惯。希望帮到你。本文还有配套的精品资源点击获取
返回列表