1. 行情数据:金融市场的脉搏与神经
在金融交易的世界里,行情数据就是市场的脉搏和神经。无论是股票、期货还是外汇,每一笔交易、每一次报价,都通过行情数据这个载体实时地传递到全球的投资者面前。对于交易所而言,如何高效、准确、可靠地将海量的市场数据分发给成千上万的参与者,是一项核心的基础设施工程。香港交易所作为亚洲最重要的金融市场枢纽之一,其行情数据协议的设计与实现,直接关系到全球投资者能否公平、及时地获取市场信息,进而做出投资决策。
我接触过不少交易所的数据协议,从国内的Level-2到欧美的ITCH/FIX/FAST,再到港交所的OG(Open Gateway)协议。每次对接一个新的行情源,都像是一次深入其技术腹地的探险。今天,我想和大家深入聊聊港交所的行情协议体系。这不仅仅是一份技术文档的解读,更是结合了我在实际对接、性能调优和故障排查中的一系列经验。你会发现,理解协议背后的设计哲学,远比记住几个字段定义重要得多。
港交所的行情协议,官方称之为“证券市场行情数据服务”,它是一套完整的、从交易所核心交易系统到外部用户终端的数据分发解决方案。这套协议不仅仅定义了数据的格式,更规定了连接方式、会话管理、重传机制等一系列确保数据完整性与实时性的规则。对于程序化交易者、量化研究员、券商系统开发者乃至金融IT运维人员来说,透彻理解这套协议,是构建稳定、低延迟数据通道的基石。
2. 港交所行情协议生态全景:从OG到二进制流
很多人一提到港交所行情,可能首先想到的是各种数据供应商提供的封装好的API,比如某些商业软件的插件。但如果我们想追求极致的延迟控制、完全自主的数据处理逻辑,或者需要定制化的数据清洗流程,那么直接对接港交所官方的原始协议就成为了必由之路。港交所的行情分发体系主要基于其OG(Open Gateway)架构。
2.1 核心协议栈:MMDP与OMP
港交所的行情数据主要通过两种主流通路分发,它们面向不同需求和规模的用户:
MMDP(Market Data Multicast Distribution Platform):这是港交所低延迟行情数据的核心。它采用组播(Multicast)技术进行数据分发。你可以把组播想象成一个电台广播:交易所作为广播塔,发送一份数据流,所有订阅了这个“频道”的接收者都能同时收到,而不是为每个接收者单独发送一份拷贝。这极大地节省了交易所出口带宽和核心系统的处理压力,是实现低延迟、高吞吐量分发的关键技术。
- 协议载体:MMDP数据流基于UDP(User Datagram Protocol)传输。UDP是无连接的,没有TCP那样的握手、确认、重传机制,因此开销极小,延迟极低。但这也意味着,网络丢包需要由应用层协议来处理。
- 数据格式:MMDP传输的是高度优化的二进制流。每个数据包(Packet)都经过精心设计,字段紧凑,没有冗余字符,以最大限度地减少传输数据量,提升解析速度。
OMP(Open Market Data Platform):可以理解为MMDP的“友好访问版”或“补充通道”。它主要面向那些无法直接接入组播网络的环境(例如某些云服务器或网络架构不支持组播),或者对延迟要求不是极端苛刻的用户。
- 协议载体:OMP通常基于TCP(Transmission Control Protocol)。TCP提供可靠的、有序的、错误校正的数据流,确保了数据的完整无误,但代价是比UDP有更高的延迟和开销。
- 访问方式:用户可以通过标准的TCP Socket连接到港交所指定的服务器地址和端口来获取数据。
注意:对于绝大多数追求性能的机构用户(如券商、高频交易公司),MMDP是首选。而OMP则常用于备份链路、测试环境或某些特定的应用场景。我们接下来的讨论将主要围绕MMDP展开,因为它是协议设计的精髓所在。
2.2 会话生命周期:从登录到心跳
对接行情协议不是简单的“打开一个端口收数据”。它是一系列有状态的交互,我们称之为一个“会话(Session)”。一个完整的会话通常包括以下几个阶段:
- 登录(Logon):客户端首先需要向行情网关发送一个登录请求报文。这个报文中包含了你的用户账号、密码(或令牌)、请求的行情频道等信息。网关验证通过后,会回复一个登录响应,标志着会话正式建立。
- 数据流传输:登录成功后,网关开始向你推送连续的行情数据流。这个流是混合的,里面包含了不同证券的买卖盘、成交、状态等信息。
- 心跳(Heartbeat)与序号检查:为了监测连接的健康状态,客户端和服务器会定期互相发送心跳报文。更重要的是,每个数据报文都带有一个序列号(Sequence Number)。客户端需要持续检查这个序列号是否连续。如果发现跳号,就意味着中间有数据包丢失了。
- 重传请求(Retransmission Request):当检测到丢包(序列号不连续)时,客户端必须立即向网关发送重传请求,指明丢失的序列号范围。网关会从缓存中重新发送这些丢失的数据包。这是MMDP/UDP方案下保证数据完整性的关键机制。
- 登出(Logout):当客户端需要断开连接时,应发送登出请求,进行优雅的会话终止。
这个生命周期管理,确保了即使在不可靠的UDP传输上,也能构建出一个可靠的数据服务。理解每个阶段的状态和可能发生的问题,是后续进行故障排查的基础。
3. 二进制报文拆解:读懂市场的语言
MMDP传输的二进制流是效率的体现,但也对解析程序提出了高要求。我们收到的不是一个一个的“消息”,而是一个个的“数据包(Packet)”。每个Packet有一个固定的包头(Packet Header),后面跟着一个或多个行情消息(Message)。
3.1 数据包结构:信封与信件
让我们类比一下:整个数据包就像一个快递信封,信封上有收件人需要的信息(包头),信封里装着的是一封或多封具体的信件(行情消息)。
Packet Header(包头):固定长度,通常是20字节左右。它包含以下关键信息:
- Packet Sequence Number(包序列号):这是整个数据包的全局唯一递增序号。用于检测丢包。
- Message Count(消息数量):指明这个Packet里面封装了多少条独立的行情消息。
- Send Time(发送时间戳):交易所发出这个Packet的精确时间(通常是纳秒级)。这是计算网络延迟和交易所内部处理延迟的黄金指标。
- Packet Length(包长度):整个Packet的总字节数。
Message(s)(消息体):在包头之后,紧跟着的就是一条或多条行情消息。每条消息也有自己的小头(Message Header)和消息体(Message Body)。
- Message Header(消息头):包含消息类型(Message Type)、消息长度、对应证券的代码(通常是一个数字形式的Instrument ID)等。
- Message Body(消息体):这是核心数据所在,其结构完全由Message Type决定。
3.2 核心消息类型解读
港交所定义了数十种消息类型,但最核心、出现频率最高的是以下几类:
- Incremental Packet(增量更新包):这不是一个具体的消息类型,而是一种数据组织方式。一个Packet里可以包含多条不同类型的增量更新消息,用于实时刷新市场状态。
- 交易状态消息(Trading Session Status):告诉你市场目前处于什么阶段,例如“开市前时段”、“持续交易时段”、“午间休市”、“收市”等。你的系统必须根据这个状态来决定如何处理后续的行情(比如,在非交易时段收到的订单簿更新可能只是指示性报价)。
- 证券静态信息消息(Security Definition):通常在每个交易日开始时发送。它定义了今天所有可交易证券的基本信息,包括数字ID与交易代码(如
00005.HK)的映射、买卖单位(Lot Size)、价格变动单位(Tick Size)、货币等。这是你建立本地代码映射表的依据,没有它,你收到一堆数字ID将无法识别是哪只股票。 - 订单簿增量更新消息(Order Book Update):这是流量最大的一类消息。它通常以“价格档次(Price Level)”为单位进行更新。例如,买一价的数量发生了变化,或者卖五价被撤销了。一条消息里会包含:
UpdateAction:是新增(New)、修改(Change)还是删除(Delete)这个价格档位?Side:是买盘(Bid)还是卖盘(Offer)?Price:价格。Size:在这个价格上的合计订单数量。Order Count(有时有):在这个价格上的订单数量(冰山订单下有用)。客户端需要根据这些增量消息,在内存中维护一个本地订单簿的镜像。
- 成交消息(Trade):报告一笔成交的发生。包含成交价格、成交量、成交时间、买卖方向(通常是主动成交的方向)等。注意,成交发生后,订单簿的相应数量会被扣除,这通常由后续的订单簿更新消息来反映。
- 快照消息(Snapshot):在某些情况下(如每日开盘前,或响应重传请求后),网关会发送完整的订单簿快照。它包含了当前所有价格档位的买卖盘信息。用于初始化或重建本地订单簿。
解析二进制流的过程,就是循环读取Packet Header,根据其中的Message Count,循环读取每条Message Header,再根据Message Type调用对应的解析函数来解读Message Body。这个过程必须高效且准确,通常会用C++、Rust或高性能的Java/C#来编写。
4. 实战对接:从零构建一个稳定的行情客户端
理解了协议原理,我们来谈谈实战。构建一个生产级别的港交所MMDP行情客户端,远不止写个解析器那么简单。它涉及网络、系统、内存、逻辑等方方面面。
4.1 环境准备与网络配置
这是第一步,也是坑最多的一步。
- 组播订阅:你的服务器必须接入支持组播的网络,并且正确配置了路由。你需要从港交所或你的线路供应商那里获取:
- 组播地址(Multicast Group IP)和端口(Port):不同的行情频道(如证券、衍生品)对应不同的组播地址。
- 源地址(Source IP):有时为了安全,会要求指定只接收来自特定源IP的组播流。
- 在你的接收程序(或操作系统)中,你需要加入(Join)这个组播组。在Linux下,这通常通过socket选项
IP_ADD_MEMBERSHIP来完成。
- 网络适配器与缓冲区:高速数据流对网卡和操作系统网络栈是巨大考验。
- 使用高性能网卡:考虑支持RSS(接收侧缩放)的万兆甚至更高速率网卡。
- 调整Socket缓冲区大小:默认的UDP接收缓冲区(
SO_RCVBUF)通常太小,在行情爆发时极易丢包。你需要将其设置为一个很大的值(例如64MB或更大)。并且,注意:在Linux上,你不仅要在代码中设置,还需要提高系统的net.core.rmem_max等内核参数上限,否则设置不生效。
# 示例:临时提高系统参数 sysctl -w net.core.rmem_max=134217728 # 128MB sysctl -w net.core.rmem_default=134217728
4.2 核心处理逻辑设计
你的客户端程序需要处理多条并行的逻辑线:
- 网络接收线程:这个线程的唯一任务就是以最高优先级从Socket读取数据包,放入一个无锁环形队列(Ring Buffer)。它的工作必须尽可能快,避免任何阻塞操作(如日志打印、业务处理)。
- 协议解析与订单簿维护线程:从环形队列中取出原始Packet,进行解析。根据消息类型:
- 如果是静态信息,更新本地代码表。
- 如果是订单簿更新,更新内存中的订单簿数据结构。这里的数据结构设计至关重要。通常使用
std::map或std::unordered_map(以证券ID为Key),其Value是一个包含买盘和卖盘std::map(以价格为Key,排序)的结构。更新时需要处理好线程安全。 - 如果是成交,更新本地成交记录,并可能触发策略逻辑。
- 持续检查包序列号,发现丢包立即构造并发送重传请求。
- 心跳与会话管理线程:定时(如每秒)发送心跳报文,并检查接收心跳响应是否超时。管理登录、登出等会话状态。
- 数据分发线程:将处理好的、结构化的行情数据(如订单簿快照、成交事件)分发给内部的其他策略或风控模块。
4.3 性能优化关键点
- 内存管理:避免在高速处理路径上动态分配内存(
new/delete,malloc/free)。应使用内存池预分配所有需要的缓冲区。 - CPU亲和性与NUMA:将关键的接收线程和解析线程绑定到特定的CPU核心上,减少上下文切换和缓存失效。如果使用多路CPU(NUMA架构),确保线程和其使用的内存位于同一个NUMA节点。
- 时间戳:在Packet进入网卡驱动层(甚至使用支持硬件时间戳的网卡)、进入用户程序、开始解析等关键节点打上时间戳。这能帮你精确衡量每个环节的延迟,定位瓶颈。
- 解析优化:二进制解析避免使用高层抽象。直接使用
memcpy到结构体(注意字节序对齐和转换),或者使用指针偏移直接读取。对于频繁调用的价格、数量转换函数(如将整数表示的“价格*10000”转换为浮点数),确保其被内联或高度优化。
5. 避坑指南:那些年我踩过的雷
对接港交所行情,光看文档是远远不够的。下面分享几个典型的“坑”,希望能帮你少走弯路。
5.1 序列号不连续不等于丢包
这是最容易让人紧张的问题。你发现Packet Sequence Number从 1000 直接跳到了 1003,第一反应就是“丢了2个包!快重传!”。但等等,先检查一下Message Type。
港交所的协议中,有一种特殊的“心跳包”或“空数据包”。它可能只包含包头和极少的心跳信息,其序列号是递增的,但Message Count为 0。也就是说,序列号1001和1002的包可能是这种不包含实际行情数据的“空包”。协议规范里会明确说明这种包的序列号行为。
正确的做法:实现一个“有效数据包序列号”的检查逻辑,只对那些Message Count > 0的包进行连续性校验。对于空包,记录其序列号用于监控连接活性即可,不触发重传。
5.2 订单簿重建的时机与一致性
你的本地订单簿是基于持续的增量更新维护的。但在以下情况,它可能和交易所的权威状态不一致:
- 程序刚启动。
- 网络中断后重连。
- 重传逻辑出现异常。
此时,你需要一个快照(Snapshot)来重建一个正确的起点。港交所通常在每日开盘前、以及响应某些特定重传请求时,会发送完整快照。
关键经验:不要假设任何时候都能收到快照。你的程序应该设计成两种模式:
- 增量模式:正常运行时,信任并应用每一条增量更新。
- 快照模式:在检测到状态异常(如序列号缺口太大且无法通过重传弥补)或收到快照消息时,清空本地订单簿,用快照数据完全重建,然后切换回增量模式。
更棘手的是“增量与快照的交叉”:你可能正在处理增量流,突然插进来一个快照包。如果处理顺序不当,会导致订单簿混乱。必须严格按包序列号顺序处理所有Packet,并在处理快照消息时,原子性地切换订单簿状态。
5.3 价格与数量的表示与计算
行情数据中的价格和数量通常不是浮点数或整数,而是经过缩放(Scaled)的整数。
- 价格:可能是
Price * 10000或Price * 100000000(取决于产品)后的整数值。解析后必须除以相应的缩放因子。务必查阅规格书,确认每只证券的缩放因子,因为不同产品(如股票、涡轮、牛熊证)可能不同。静态信息消息(Security Definition)里通常会包含这个PriceDisplayFormat或MinPriceIncrement信息。 - 数量:通常是股份数量(股数),对于期货则是合约张数。但要注意买卖盘更新中的
Size,它代表的是该价格档位的总订单数量,单位是“手(Lot)”。你需要用静态信息中的Lot Size(每手股数)乘以这个Size,才能得到总股数。- 例如,买一价
Size = 50,该股票的Lot Size = 500,则买一价的总订单股数为50 * 500 = 25,000股。
- 例如,买一价
忽略缩放因子和单位转换,会导致计算出的金额、市值等全部错误,这是非常低级的致命错误。
5.4 网络抖动与重传风暴
在UDP环境下,网络轻微抖动可能导致瞬间的连续丢包。如果你的重传策略过于激进,可能会发生:
- 发现丢失包1001-1005。
- 立刻发送对1001-1005的重传请求。
- 由于网络问题,这个重传请求也可能丢失或延迟。
- 你等不及,又发送了一次重传请求。
- 最终,网络恢复,网关收到了两个重复的重传请求,于是发送了两份1001-1005的数据。
- 你的客户端收到双份数据,订单簿被重复更新,导致数量虚高。
应对策略:
- 实现重传请求的退避(Backoff)机制:第一次重传等待时间短(如10ms),如果还没收到,第二次等待时间长一些(如50ms),以此类推。
- 记录已请求重传的序列号范围:避免在收到数据前重复请求同一段数据。
- 设置合理的重传超时和放弃阈值:对于太久远的数据,交易所的缓存可能已经清除,重传也无意义。此时应记录错误,并尝试寻找机会(如等待下一个快照)来恢复状态,而不是无限重试。
对接港交所行情协议,是一个将严谨的协议规范、高性能的系统编程和细致的金融业务知识相结合的过程。它没有太多黑科技,更多的是对细节的掌控和对异常情况的周全考虑。从网络报文的抓取解析,到内存订单簿的毫秒级更新,再到面对网络波动时的从容恢复,每一个环节都考验着开发者的功底。当你亲手构建的系统能够稳定地吐出精准的市场数据,并支撑起交易决策时,那种成就感是巨大的。希望这篇结合了原理与实战的分享,能为你深入金融市场数据底层架构提供一张有用的地图。