简介:本资源是深圳证券交易所官方发布的《STEP行情数据接口规范V1.11》PDF文档,面向量化交易开发者、高频策略工程师及证券IT系统建设者,解决Level2行情数据接入、解析与兼容性适配等核心问题。文档全面覆盖快照行情、逐笔委托、逐笔成交、证券实时状态等消息格式,详述频道代码、MDStreamID、TradingPhaseCode、开关类别(如转融通出借、港股通买卖、波动性中断)及新增债券竞买行情等关键字段,特别强化了接口向后兼容设计——用户系统可自动忽略未识别的行情条目与开关,无需改造即可支持未来升级。资源为单个682KB PDF文件,结构清晰,含修订历史、名词释义、会话机制与完整协议字段定义,便于快速查阅与工程落地。目前已有1988人学习下载,是构建低延迟行情终端、开发涨停板策略或对接深交所Level2数据服务不可或缺的权威技术依据。
1. 深交所 Level2 行情接口 V1.11:不是“文档”,是量化系统上线前必须啃透的「数据契约」
你写完一个涨停板追单策略,回测年化 32%,实盘第一天就卡在委托延迟上——不是模型问题,是行情解析错了MDEntryType=xi(参考价)和xh(按盘价)的优先级;你搭好 FAST 解码器,跑通了股票快照,结果一接入债券现券频道107x就崩溃,因为没处理MDEntryType=xr/xs/xt/xu(港股开市前买/卖盘上下限价)这四个新增字段;你自以为重传逻辑万无一失,却在盘后定价大宗交易时段发现ApplEndSeqNum=0的语义被行情网关悄悄改成了「取当前内存最大值」,而你的重传请求里漏填了ApplBegSeqNum,直接触发了ResendStatus=4(数据不可用)……这些不是玄学,是《深交所 Level2 行情数据接口规范 V1.11》第 37 页白纸黑字写死的契约条款。它不是教你怎么写 Python,而是告诉你:当ChannelNo=2071(债券现券逐笔成交)的RawData流进来时,TemplateID必须是4071,MDEntryType可能出现2(最新成交)、4(买一)、5(卖一)、xr(买盘上限价)四种组合,且xr只在TradingPhaseCode=O(开市前时段)下有效——错一个字节,整条消息就进黑洞。这份规范面向的是已具备 TCP 连接管理、FAST 解码、心跳保活能力的量化系统工程师,目标明确:把深交所每毫秒吐出的二进制流,稳、准、快地喂进你的策略引擎。它不讲原理,只定义边界;不教编程,只划红线;不承诺兼容旧版,但强制要求「新增字段可自动忽略」——这才是 V1.11 最硬核的价值:让你的系统在深交所持续迭代中不翻车。
2. 协议栈拆解:STEP 会话层 + FAST 应用层 = 一条不能断、不能错、不能慢的数据生命线
2.1 为什么必须用 STEP 而非直连 TCP?会话层是行情系统的「呼吸节律」
深交所 Level2 数据不是裸 TCP 流,而是嵌套了两层协议:外层是 STEP(Securities Trading Exchange Protocol)会话层,内层是 FAST(FIX Adapted for STreaming)应用层。STEP 层负责「活着」,FAST 层负责「读懂」。很多团队初期试图跳过 STEP,自己拼接 TCP 包,结果在流量控制或断线重连时集体翻车——因为 STEP 会话层内置了深交所强约束的生存机制。关键点有三:
双端口绑定不可绕过:实时数据端口
9129(TCP)仅用于接收MsgType=UA001(心跳)、UA002(重传应答)、UA003(用户报告)及所有行情W/U/A消息;重传服务端口9130(TCP)仅用于发送UA002重传请求并接收重传响应。两个端口必须独立建立连接,且每个端口只允许一个 TCP 连接。现场版行情网关(卫星链路)不提供9130端口,网络版才支持——这意味着现场版用户必须接受「逐笔行情丢失即永久丢失」的事实,无法靠重传来兜底。DefaultApplVerID 是入场券,填错直接拒连:登录
Logon消息中DefaultApplVerID字段必须严格填写1.02(V1.11 对应的通信版本号),而非文档标题里的1.11。这是 STEP 会话层校验协议兼容性的第一道闸机。若填1.11,行情网关会在Logon响应中返回RejectReason=10(Unsupported 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推导得出。例如:
ChannelNo=1011(股票快照)→TemplateID=4011ChannelNo=2071(债券现券逐笔成交)→TemplateID=4071ChannelNo=5001(港股实时快照)→TemplateID=4101
这个映射关系在 V1.11 中未显式列出,但可通过ChannelNo末两位数字 +4000基础值推算(如1011→4000+11=4011)。若解码器硬编码TemplateID=4011去解析ChannelNo=2071的数据,因字段布局完全不同,会导致内存越界或解析出荒谬数值(如将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(f"Unknown ChannelNo: {channel_no}") # 示例:解析债券现券逐笔成交(ChannelNo=2071) 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:RawDataLength:FAST 消息体长度(字节),必须精确匹配96:RawData的实际长度。若解析时发现len(RawData) != RawDataLength,说明 TCP 层发生粘包或截断,需丢弃整条消息并告警。96:RawData:FAST 编码的二进制数据体,可能包含多条 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 # 示例:解析到 ChannelNo=1011 的消息 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 快照行情:定时广播的「市场切片」,五档盘口只是冰山一角
快照行情(MsgType=W)是深交所 Level2 的基础数据源,以固定频率(如股票快照每 500ms 一次)广播。V1.11 中快照消息的复杂性远超「买卖五档」认知——它是一个动态结构体,字段存在性由MDEntryType(行情条目类型)决定。以ChannelNo=1011(股票快照)为例,其TemplateID=4011定义了以下关键MDEntryType:
| MDEntryType | 含义 | 是否必填 | 业务意义 | V1.11 新增/变更 |
|---|---|---|---|---|
0 | 最新成交价 | Y | 当前最新一笔成交的价格 | — |
1 | 最新成交量 | Y | 最新一笔成交的数量 | — |
2 | 买一价 | Y | 当前最优买价 | — |
3 | 卖一价 | Y | 当前最优卖价 | — |
4 | 买一量 | Y | 买一价位上的委托总量 | — |
5 | 卖一量 | Y | 卖一价位上的委托总量 | — |
9 | 加权平均价 | N | 当日成交加权均价(高频策略关键指标) | V1.02 新增 |
xj | 加权平均价涨跌BP | N | 相比昨收的涨跌基点(量化择时信号) | V1.02 新增 |
xi | 参考价 | N | 开市前时段计算的理论开盘价 | V1.08/V1.07 新增 |
xr | 买盘上限价 | N | 港股开市前买盘最高申报价(仅TradingPhaseCode=O) | V1.07 新增 |
解析逻辑:FAST 解码器返回的parsed_msg是一个字典列表,每项代表一个MDEntryType对应的行情条目。你必须遍历所有条目,按MDEntryType分类聚合:
# 解析股票快照(ChannelNo=1011)示例 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仅在TradingPhaseCode='O'(开市前时段)下有效,其他时段该字段虽存在但无业务意义。V1.11 要求系统「能自动忽略不关心的字段」,因此你的聚合逻辑必须容忍xr_price为空。
3.2 逐笔行情:带序号的「市场脉搏」,委托与成交的原子事件流
逐笔行情(MsgType=UA201/UA202)是高频策略的生命线,V1.11 将其分为两类:逐笔委托(UA201)和逐笔成交(UA202)。二者均携带ApplSeqNum(消息记录号),这是重传机制的基石。
逐笔委托(UA201):记录每一笔进入订单簿的委托,含
OrderQty(委托数量)、Price(委托价格)、Side(买卖方向)、OrderID(委托编号)。V1.11 新增对债券竞买委托的支持(ChannelNo=401x),其MDEntryType可能为U(竞买预约)、V(竞买委托)。逐笔成交(UA202):记录每一笔实际成交,含
LastQty(成交数量)、LastPx(成交价格)、TradeID(成交编号)、MatchID(匹配编号)。债券现券ChannelNo=207x的成交消息中,MDEntryType为2(最新成交)。
重传逻辑的核心公式:
当收到ChannelNo=C的消息,其ApplSeqNum=N,本地已存最大序号为N_max:
- 若
N <= N_max→ 重复消息,丢弃; - 若
N == N_max + 1→ 正常接收,更新N_max = N; - 若
N > N_max + 1→ 消息丢失,需重传[N_max+1, 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 消息体:TemplateID=3002, ResendType=1, ChannelNo=channel_no, ApplBegSeqNum=beg_seq, ApplEndSeqNum=end_seq fast_body = build_fast_resend_req(3002, 1, channel_no, beg_seq, end_seq) # 发送至重传端口 9130 send_to_port(9130, fast_body) # 示例:收到 ChannelNo=2011 的 UA202 消息,ApplSeqNum=1005,本地 max=1002 # 触发重传请求:beg_seq=1003, end_seq=1004V1.11 特别强调:ApplEndSeqNum=0在重传请求中表示「取当前内存最大值」,但ApplBegSeqNum必须显式指定,否则行情网关返回ResendStatus=4(数据不可用)。这是实测中 80% 重传失败的根源。
3.3 公告消息:二进制文件的「数字信使」,从概要到全文的两级重传
公告消息(MsgType=B)是深交所向市场发布规则、停复牌、分红等信息的通道。V1.11 将其设计为「先概要、后全文」的两级结构,确保信息完整性:
- 公告概要(News Summary):
ChannelNo=2,TemplateID=4002,含NewsID(公告唯一 ID)、NewsCategory(类别)、NewsText(摘要)。这是重传的起点。 - 公告全文(Full News):
ChannelNo=2,TemplateID=4003,RawData字段为二进制文件(PDF/HTML),NewsID与概要一致。
重传流程:
- 登录成功后,立即发送
UA002请求ResendType=2(公告信息)、ChannelNo=2、NewsID=''(空字符串表示请求概要); - 收到概要后,比对本地
NewsID列表,找出缺失的NewsID; - 对每个缺失
NewsID,发送UA002请求ResendType=2、ChannelNo=2、NewsID=xxx,获取全文。
# 获取公告概要并比对 def fetch_news_summary(): # 构造重传请求:ResendType=2, ChannelNo=2, 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_id=news_id) send_to_port(9130, req_body) # 处理公告全文响应 def on_news_full(news_id: str, raw_data: bytes): # raw_data 是二进制文件,直接保存为文件 filename = f"news_{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 新增债券现券快照ChannelNo=107x,但TemplateID计算规则未覆盖107x。按4000 + (channel_no % 100)计算1071得4071,但实际应为4071(V1.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(买盘上限价)等字段在TradingPhaseCode='O'下有效,但其MDEntryPx数据类型为int64,深交所用特定编码表示「无报价」(如0x8000000000000000)。若解码器未按int64有符号解析,会得到极大正数。
解决:在解析MDEntryPx前,先检查TradingPhaseCode是否为'O',再对MDEntryPx做有符号转换:price = struct.unpack('>q', raw_bytes)[0](大端 int64)。
4.3 现象:重传请求ApplEndSeqNum=0后,行情网关返回ResendStatus=4
原因:V1.11 明确要求ApplBegSeqNum必须显式填写(表 4-5-2 注释:「当 ResendType=1 时生效」),但很多 SDK 默认不填。ApplBegSeqNum=0被网关解释为「从 0 开始」,而实际数据从1开始,导致无数据可返。
解决:重传请求中ApplBegSeqNum必须设为N_max + 1,ApplEndSeqNum设为0。示例:beg_seq=1003, end_seq=0。
4.4 现象:证券实时状态消息中SecuritySwitchType=36(债券回售转售)解析失败
原因:V1.11 新增开关类别36-债券回售转售(2021-7 修订),但部分旧版解码器的SecuritySwitchType字段仍按uint8解析,而36超出uint8范围(0-255),导致溢出为36或解析错误。
解决:确认SecuritySwitchType在 FAST 字典中定义为uint16(V1.11 第 32 页5.3 业务层域定义表中明确为16位),解析时用uint16类型。
4.5 现象:盘后定价大宗交易ChannelNo=300x快照中TradingPhaseCode='P',但策略误判为连续竞价
原因: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做实时「脉搏计数器」
频道心跳(UA001,TemplateID=3001)是唯一能实时反映行情网关健康状态的消息。V1.11 规定心跳间隔为 3 秒,且ApplLastSeqNum字段记录该频道最后一条行情消息的序号。这不是一个摆设数字,而是你的数据完整性探针:
- 心跳丢失检测:若连续 2 个心跳周期(6 秒)未收到
UA001,立即触发断连重连。不要等 TCP 超时(通常 30 秒以上),那已错过千笔行情。 - 序号跳跃预警:记录每个频道的
last_seq,每次收到心跳时对比ApplLastSeqNum。若current_seq - last_seq > 1000(股票快照约 500ms 一条,6 秒应增约 12 条),说明网关侧积压严重,需告警并检查 VSS 处理性能。 - 频道结束标志
EndOfChannel=1:当UA001中EndOfChannel=1,表示该频道数据流终止(如某只股票退市),应清理本地缓存并停止订阅。
# 心跳监控器(每频道独立) 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(f"Channel {self.channel_no} heartbeat timeout, reconnecting!") self._reconnect() # 检查序号跳跃 if current_seq > self.last_seq: gap = current_seq - self.last_seq if gap > 1000: # 警惕积压 alert(f"Channel {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 Side=1(买一价不高于买委托价);快照.AskPx1 <= 逐笔.UA201.Price where Side=2(卖一价不低于卖委托价)。
# 时间戳对齐验证(简化) 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(f"MS {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(f"MS <p> <a href="https://download.csdn.net/download/book_yxc/46329590" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>