惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
人人都是产品经理
人人都是产品经理
IT之家
IT之家
T
The Blog of Author Tim Ferriss
V
V2EX
博客园 - 聂微东
The Cloudflare Blog
Blog — PlanetScale
Blog — PlanetScale
A
About on SuperTechFans
U
Unit 42
Vercel News
Vercel News
L
LangChain Blog
博客园 - 司徒正美
H
Help Net Security
Recent Announcements
Recent Announcements
Recorded Future
Recorded Future
V
Visual Studio Blog
Jina AI
Jina AI
Microsoft Azure Blog
Microsoft Azure Blog
GbyAI
GbyAI
Y
Y Combinator Blog
C
Check Point Blog
博客园 - 三生石上(FineUI控件)
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
J
Java Code Geeks
The Register - Security
The Register - Security
The GitHub Blog
The GitHub Blog
B
Blog RSS Feed
F
Fortinet All Blogs
B
Blog
G
Google Developers Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
云风的 BLOG
云风的 BLOG
爱范儿
爱范儿
MongoDB | Blog
MongoDB | Blog
F
Full Disclosure
有赞技术团队
有赞技术团队
罗磊的独立博客
博客园_首页
MyScale Blog
MyScale Blog
aimingoo的专栏
aimingoo的专栏
Google DeepMind News
Google DeepMind News
M
MIT News - Artificial intelligence
N
Netflix TechBlog - Medium
Engineering at Meta
Engineering at Meta
量子位
I
InfoQ
小众软件
小众软件
P
Proofpoint News Feed

土法炼钢兴趣小组的算法知识备份

国密算法与国密 TLS 系列索引 【系统架构设计】架构质量属性:不只是"高可用高性能" 【系统架构设计百科】告警策略:如何避免"狼来了" 【系统架构设计】CQRS:读写分离的架构哲学 【系统架构设计】空间架构:极端扩展场景的解法 【系统架构设计】微服务架构深度审视:优势、代价与适用边界 【系统架构设计】扩展性原理:水平、垂直与对角扩展 【系统架构设计】无状态设计:扩展的第一步也是最难的一步 【系统架构设计】缓存架构:从本地到分布式的多级缓存体系 【系统架构设计】管道与过滤器:Unix 哲学的架构表达 【系统架构设计】复杂性管理:架构的核心战场 【系统架构设计】消息队列架构:异步解耦的设计与陷阱 【系统架构设计】CDN 架构:全球加速的设计原理 【系统架构设计】连接池设计:被忽视的性能杀手 【系统架构设计】弹性设计模式:熔断器、舱壁与超时 【系统架构设计】高可用设计模式:冗余、故障转移与仲裁 【系统架构设计】容量规划:从拍脑袋到数据驱动 【系统架构设计】数据库扩展:分库分表的工程实践与替代方案 【系统架构设计】SLO 工程:可靠性的量化管理 【系统架构设计】性能建模:用数学思维分析系统瓶颈 【系统架构设计】混沌工程:主动验证系统的韧性 【系统架构设计】零拷贝与内存映射:数据搬运的极致优化 【系统架构设计】线程模型:从 thread-per-request 到协程 【系统架构设计】容灾架构:多活与灾备设计 【系统架构设计】数据库性能模式:索引、查询与连接管理 【系统架构设计】数据建模:从关系范式到文档模型的真实权衡 【系统架构设计】吞吐量优化:批处理、流水线与并发模型 【系统架构设计】流处理架构:从批处理到实时的范式迁移 【系统架构设计】搜索引擎架构:倒排索引之上的系统设计 【系统架构设计】时序数据架构:监控与 IoT 的存储设计 【系统架构设计】数据迁移与版本化:在线不停机的数据演进 【系统架构设计】数据湖与数据仓库:分析架构的演进路线 【系统架构设计】API 网关设计:入口层的职责边界 【系统架构设计】应用层数据一致性模式:在正确性与性能之间走钢丝 【系统架构设计】多模数据库选型:Polyglot Persistence 的工程实践 【系统架构设计】服务发现与注册:动态拓扑的基础设施 【系统架构设计】配置管理架构:从配置文件到配置中心 【系统架构设计】全链路压测:大规模系统的性能验证 【系统架构设计】幂等性设计:分布式环境下的安全重试 【系统架构设计】契约测试与 Schema 演进:服务间的信任协议 【系统架构设计】长连接与推送架构:WebSocket、SSE 与 MQTT 【系统架构设计】延迟分析:从 P50 到 P999 的全链路追踪 【系统架构设计百科】DDD 战术模式:聚合、实体与值对象 【系统架构设计百科】防腐层与开放主机服务:系统集成的 DDD 方案 【系统架构设计百科】领域事件与事件风暴:从业务到架构的桥梁 【系统架构设计百科】CQRS + Event Sourcing 完整实战:从领域建模到部署 【系统架构设计百科】DDD 与微服务:用领域模型划分服务边界 【系统架构设计】DDD 战略设计:限界上下文与上下文映射 【系统架构设计百科】认证架构:从 Session 到 JWT 到 OIDC 【系统架构设计】API 设计哲学:REST vs GraphQL vs gRPC 的真实权衡 排序算法专题:从 TimSort 到并行排序 【密码学百科】国密算法体系:SM2/SM3/SM4/SM9 全景解读 【密码学百科】承诺方案:Pedersen 承诺、向量承诺与多项式承诺 【密码学百科】不经意传输与隐私信息检索:OT、OT 扩展与 PIR 【密码学百科】门限密码学:门限签名、门限解密与分布式密钥生成 完美哈希:从理论到 gperf 实践 【密码学百科】安全多方计算:从 Yao 的混淆电路到实用 MPC 【密码学百科】同态加密:从 Paillier 到全同态加密(FHE) 【密码学百科】零知识证明系统:zk-SNARKs、zk-STARKs 与 Bulletproofs 【密码学百科】概率论与密码分析:生日攻击、差分分析与线性分析 【密码学百科】计算复杂性与归约:密码安全性证明的基石 【密码学百科】秘密共享:Shamir 方案、VSS 与安全多方计算入口 【密码学百科】椭圆曲线代数:Weierstrass 方程、点群运算与曲线选择 【密码学百科】离散对数与配对密码学:从 DLP 到 BLS 签名 【密码学百科】格密码数学基础:SVP、LWE 与格基约化 【密码学百科】抽象代数:群、环、域的密码学视角 【密码学百科】有限域算术:GF(2^n) 运算与在 AES/ECC 中的应用 【密码学百科】数论进阶:二次剩余、椭圆曲线上的 Weil 配对 【密码学百科】密码学简史:从凯撒密码到量子时代 【密码学百科】威胁模型与安全目标:CIA 三要素之外 【密码学百科】Kerckhoffs 原则与现代密码设计哲学 【密码学百科】随机性:密码学的基石 【密码学百科】信息论入门:熵、完美保密与 Shannon 定理 【密码学百科】分组密码原理:Feistel 网络与 SPN 结构 【密码学百科】AES 逐步拆解:SubBytes 到 MixColumns 的数学 【密码学百科】分组密码工作模式全览:ECB/CBC/CTR/OFB/CFB 【密码学百科】流密码:RC4 的兴衰与 ChaCha20 的崛起 【密码学百科】密码学哈希函数:MD5→SHA-2→SHA-3 的进化之路 【密码学百科】MAC 与 HMAC:消息认证的正确姿势 【密码学百科】认证加密(AEAD):GCM、ChaCha20-Poly1305 与 OCB 【密码学百科】密钥派生函数:HKDF、PBKDF2、Argon2 与密码存储 【密码学百科】公钥密码的数论基础:模运算、群、原根 【密码学百科】RSA 从原理到攻击:教科书 RSA 为什么不安全 【密码学百科】Diffie-Hellman 密钥交换与离散对数问题 【密码学百科】椭圆曲线密码学(ECC):从几何直觉到点群运算 【密码学百科】数字签名:ECDSA、EdDSA 与 Schnorr 签名 【密码学百科】现代密钥交换:X25519、ECDHE 与前向保密 【密码学百科】混合加密与 KEM/DEM 范式:ECIES 与 HPKE 【密码学百科】填充方案:PKCS#1 v1.5、OAEP 与 PSS 【密码学百科】TLS 协议全解析:从握手到 0-RTT 【密码学百科】PKI 与数字证书:信任链的构建与崩塌 【密码学百科】密码认证协议:从 SRP 到 OPAQUE 【密码学百科】零知识证明入门:如何证明你知道而不泄露 【密码学百科】安全信道构造:Noise 协议框架与 Signal 协议 【密码学百科】密钥管理工程:HSM、KMS 与密钥生命周期 【密码学百科】侧信道攻击:从时序攻击到功耗分析 【密码学百科】密码学实现陷阱:三层漏洞分类、审计工具链与系统性预防 密码敏捷性:如何设计可升级的密码系统 【密码学百科】OpenSSL/BoringSSL 架构剖析:ENGINE、Provider 与 FIPS 模块 排序基准测试:用数据说话
【金融科技工程】行情分发:MBP/MBO、快照+增量、组播/TCP、FIX/ITCH
2026-04-22 · via 土法炼钢兴趣小组的算法知识备份

引言:行情是撮合的”出口”,也是整个市场的”公共视野”

上一篇《撮合引擎实现》讲清了订单簿(Order Book)内部如何以价格时间优先匹配买卖盘。但撮合引擎撮合出一笔成交,如果没人看见,它就只是一个私有状态的变更。只有当这个状态变更以某种协议、某种延迟、某种可靠性保证,推送给市场的每一个参与者时,这笔成交才真正成为”市场价格”。这个职责由行情系统(Market Data System)承担。

行情系统是交易所里最容易被低估的子系统。它看上去只是”把撮合结果广播出去”,但量化到数字上:

  • 纳斯达克(Nasdaq)的 TotalView ITCH 在美股正常交易日每秒产生数百万条消息,单日压缩后数十 GB;
  • 芝加哥商品交易所(CME)的 MDP 3.0(Market Data Platform 3.0)基于 SBE(Simple Binary Encoding)+ UDP 组播(UDP Multicast),端到端发布延迟稳定在个位数微秒
  • 上交所新一代行情系统 Level-2 在 2024 年大盘行情时单秒峰值 tick 量突破百万,券商前置机 UDP 丢包率必须压到 10⁻⁶ 以下;
  • 币安(Binance)现货 WebSocket @depth 频道全球订阅者数百万,单个交易对 @depth@100ms 每秒一条深度 diff,冷门币也要保证延迟不劣化;
  • 芝加哥—纽约间 1100 公里的微波(Microwave)链路比光纤快约 4.5 毫秒,HFT(High-Frequency Trading,高频交易)公司愿意付数千万美元购买一个微波塔的使用权,只为给行情快 4 毫秒

行情系统真正的复杂度不在”推”,而在:

  1. 多份副本,多种协议,多个消费者,还得一致。一笔成交要同时以 L1(Level-1,顶档)、L2(Level-2,多档)、L3(Level-3,逐笔/逐单)形式发给散户行情、机构行情、内部风控、监管报送、历史库 Tick 存储,任何一路错位都会引发套利漏洞或监管追责。
  2. UDP 必然丢包,序列号必须严格单调,恢复必须在微秒级完成。组播快但不可靠,TCP 可靠但慢,生产系统要两者兼备。
  3. 历史行情既要完全真实,又要能”快进”回放用于回测。事件时间(Event Time)与墙上时钟(Wall Clock)的分离是量化回测的核心抽象。

本篇的读者画像:

  • 交易所 / 券商 / Broker 后端工程师:需要理解行情生产端如何保证序列号、增量与快照一致;
  • 量化 / 做市工程师:需要在本地维护订单簿、回放 tick、计算 VWAP,理解 Conflation(聚合)与 Full Tick 的权衡;
  • 低延迟基础设施工程师:关心内核旁路(Kernel Bypass)、共址(Colocation)、微波传输这些物理层优化的边界。

本篇与前后三篇联动:第 15 篇《交易所架构》给出了五大子系统的分层图,行情系统位于撮合引擎下游;第 16 篇《撮合引擎》把 Order Book 内部结构讲清楚了,本篇接过撮合引擎吐出的事件流;下一篇第 18 篇《证券登记结算》则承接行情 + 成交进入登记结算机构的链路。


一、行情分级:L1、L2、L3 到底在看什么

所有后续协议、分发链路、存储方案的设计都围绕一个问题展开:订阅者要看什么? 不同客户承受的延迟、需要的细节、愿意付的费用差距极大,因此交易所历史上形成了分级订阅体系。

1.1 三级行情的本质区别

级别 英文 内容 粒度 典型订阅者 典型费用
Level-1 Top of Book / BBO 最优买价、最优卖价、最新成交价/量、日高低开 每 tick 一条 散户行情、App、资讯门户 免费/低价
Level-2 MBP(Market by Price) 买卖各 N 档(5/10/20/全档)价格、挂单量、订单数 每 tick 一条聚合 机构、做市商、算法策略 月费几千到几万人民币/几百美元
Level-3 MBO(Market by Order) 逐笔订单:每一个挂单的订单号、价格、剩余量、队列位置 每个订单事件一条 高频做市、HFT、撮合模拟器 昂贵,部分市场监管才能看

MBP 和 MBO 是行情从业者最容易混淆的两个词:

  • MBP(Market by Price,按价位聚合):盘口的每一档是”这个价格上总共挂了多少手、多少单”,一档 = 一条记录。N 档就是 2N 条。
  • MBO(Market by Order,按订单展开):盘口的每一档展开成若干条独立挂单,一个订单 = 一条记录,订阅者可以自己算队列位置(Queue Position)。

为什么有了 MBP 还要 MBO?因为 MBP 丢失了时间优先(Time Priority)信息。做市商关心自己的单在队列里排第几位,这决定了”如果行情不动,我大概还要等多少成交才能轮到我”,这个信息只有 MBO 才能还原。

1.2 谁在同一时刻看到什么

一个 tick 事件(比如一笔撤单)在行情系统内部分发时,会按不同级别被”降级”输出:

flowchart TB
    ME[撮合引擎 Event Stream] --> MBO[L3 MBO:逐单事件]
    MBO -->|按价位聚合| MBP[L2 MBP:多档盘口]
    MBP -->|取首档| L1[L1 BBO:最优买卖价]
    MBO --> Trade[成交 Trade Tape]
    Trade --> L1
    Trade --> Recorder[历史 Tick 存储]
    MBO --> Recorder

L1 是从 L2 顶档派生的,L2 是从 L3 聚合的,三者同源,但分发路径、压缩策略、延迟 SLA 都不同。这也是行情系统内部工程最繁琐的一部分——同一个事件要沿多条路径出去。

1.3 国际主流市场的分级对照

市场 L1 产品 L2 产品 L3 产品 备注
Nasdaq(美股) Nasdaq Basic TotalView-ITCH 5 档 TotalView-ITCH 完整 MBO ITCH 协议同时承载 L2/L3
NYSE(美股) NYSE BBO NYSE Integrated NYSE OpenBook Pillar 平台
CME(期货) MDP 3.0 Top of Book MDP 3.0 Market by Price MDP 3.0 MBO 全基于 SBE + UDP 组播
上交所 Level-1 行情 Level-2 10 档 / 逐笔委托 无公开 L3 Level-2 本质是增强 L2 + 逐笔委托/成交
深交所 Level-1 Level-2 10 档 / 逐笔委托 无公开 L3 协议为 STEP(证券交易数据交换协议)
港交所 BMP(Basic Market Data) MMP(Market Data Plus) OMD(Orion Market Data)逐单 OMD-C/OMD-D
Binance @bookTicker @depth20@100ms @depth@100ms diff + REST 快照 WebSocket + REST

国内市场没有严格意义上的 L3,但”逐笔委托 + 逐笔成交”合起来等价于 L3,事后可以重建 MBO。这一点在国内量化做市、撤单率分析里被大量用到。


二、行情协议:从 FIX 到 ITCH 到 SBE

协议决定了行情系统的两个核心指标:序列化开销(编码/解码 CPU 时间)和传输带宽(每条消息字节数)。低延迟市场对两者都极端敏感。

2.1 FIX:行业通用语,但太”胖”

FIX(Financial Information eXchange)是 1992 年由 Fidelity 和所罗门兄弟发起的行业事实标准,核心是文本 Tag-Value 格式

8=FIX.4.4|9=178|35=W|34=2|49=NYSE|56=CLIENT|52=20260422-09:30:00.001|
55=AAPL|268=2|269=0|270=185.23|271=500|269=1|270=185.25|271=800|10=128|

每条消息用 |(实际是 SOH,0x01)分隔 Tag=Value 对,Tag 35=W 表示 Market Data Snapshot。优点是人类可读、版本容易演进、网关好调试;缺点显而易见:一条 10 档快照轻松上千字节,解析要做字符串扫描和整数转换,在纳秒级系统里不可接受。

FIX 在行情端几乎只剩两个位置还在用:

  1. 机构与经纪商之间的订单 + 低频行情:FIX Session 层提供 Heartbeat、Resend Request、Sequence Number 管理,是久经考验的双向连接协议;
  2. 加密市场的机构接入:不少 OTC 柜台和 Prime Broker 用 FIX 4.4 / 5.0 SP2 做订单 + 部分行情订阅。

2.2 FIX FAST:在 FIX 语义上做二进制压缩

FAST(FIX Adapted for STreaming)是 FIX 协议委员会 2005 年推出的压缩方案。核心思想:

  • 模板(Template)定义字段顺序、类型、默认值、是否可省;
  • 存在位图(Presence Map)标记本条消息哪些字段存在;
  • 增量编码(Delta / Copy / Increment)对连续消息的同一字段只编码差值;
  • 整数用 Stop-bit 可变长度编码(类似 VarInt)。

同样的 10 档快照,FAST 可以压缩到原 FIX 的 10–20%,但解码需要完整的模板文件,任何模板变更都要客户端同步升级。上交所 Level-2 的 FAST 协议、CME 早期 MDP 2.5 都曾是 FAST 用户;国内部分券商直连方案至今仍在维护 FAST 解析器。

2.3 ITCH / OUCH:Nasdaq 的定长二进制

Nasdaq 1990 年代末自研 INET 撮合平台时顺手做了两套协议:

  • ITCH:行情(交易所 → 客户端),包含 Add OrderOrder ExecutedOrder CancelTradeSystem Event 等消息类型;
  • OUCH:下单(客户端 → 交易所),最短的新单消息只有 49 字节。

ITCH 的关键设计:

  • 固定长度:比如 Add Order 消息恒定 36 字节(ITCH 5.0),接收端按长度直接 memcpy 到结构体,不需要任何解析;
  • 大端字节序(Network Byte Order):跨平台一致;
  • 价格用整数:比如 price × 10000 存成 Int32,避免浮点;
  • 消息类型用单字节 ASCIIA = Add Order,E = Executed,X = Cancel,D = Delete,U = Replace,P = Trade(Non-Cross)。

ITCH 之所以成为低延迟行情协议事实上的范式,是因为:整条消息放进一个 UDP datagram,接收端几乎零拷贝(Zero-Copy),用 C/C++ 可以 cast 成 struct。生产系统里完整解析一条 ITCH 消息在现代 CPU 上只需要几十纳秒。

2.4 SBE:CME 主导的新一代二进制

SBE(Simple Binary Encoding)是 Real Logic 公司(Martin Thompson 等 LMAX Disruptor 原班人马)主导,2014 年被 FIX Trading Community 标准化的二进制编码格式,现在是 CME MDP 3.0、IEX、Bloomberg B-PIPE、部分亚洲交易所的选择。SBE 的核心目标:

  • 零拷贝 + 零分配:消息缓冲区就是业务对象,getter 直接读字节偏移;
  • 对 CPU cache line 和 SIMD 友好:字段按 8 字节对齐;
  • 支持变长部分(比如重复组、可变字符串)但放在消息尾部;
  • XML Schema 驱动:用 sbe-tool 生成 Java / C++ / Go / Rust 解码器。

一个 SBE Message Header 恒定 8 字节:

┌──────────────┬──────────────┬──────────────┬──────────────┐
│ BlockLength  │ TemplateId   │ SchemaId     │ Version      │
│   2 bytes    │   2 bytes    │   2 bytes    │   2 bytes    │
└──────────────┴──────────────┴──────────────┴──────────────┘

后面紧跟固定长度主体 + 可重复组。CME 公开基准测试显示:同一条 MDP 增量消息,JSON ~400 字节、FIX ~200 字节、SBE ~40 字节,解码延迟 JSON ~3μs、FIX ~1μs、SBE ~50–100ns。

2.5 国内协议:STEP、FAST、二进制

上交所、深交所、期交所(上期所、大商所、郑商所、中金所)对外协议有较长的历史演进:

市场 行情协议 传输 备注
上交所 Level-1 STEP(FIX-like 文本) TCP 中国证券交易所数据交换协议
上交所 Level-2 FAST / 二进制 UDP 组播(内部)/ TCP(外发) 含逐笔委托、逐笔成交
深交所 BIN / STEP UDP 组播 + TCP 二进制协议由交易所定义
中金所 CTP + FEMAS 行情 TCP / UDP 组播 期货 CTP 协议广泛用于券商前置
上海国际能源中心(INE) CTP / Femas 同上 原油期货等

国内券商前置机典型部署:交易所机房的组播在券商核心机房被接收、落盘、解析、转成 CTP/STEP 格式再推给终端。每一跳都要小心序列号。

2.6 加密行情:WebSocket 为主,JSON / Protobuf 并存

加密货币交易所基本跳过了 FIX 和二进制组播,直接用 WebSocket + JSON:

交易所 订阅协议 格式 快照 增量
Binance WebSocket JSON REST /depth 快照 @depth@100ms diff
OKX WebSocket JSON 首包快照 后续增量,带 checksum
Coinbase Advanced WebSocket JSON level2 通道首条 snapshot 后续 update
Kraken WebSocket v2 JSON 订阅即推 snapshot 后续 update,带 checksum
Bybit WebSocket JSON 首包 snapshot delta 增量

少数交易所提供 Protobuf / FlatBuffers 版本(如 Binance Spot 的 market-data-only-stream 有实验性二进制),但主流仍是 JSON,原因是加密用户大多跑 Node.js / Python 脚本,JSON 足够。延迟敏感客户会付费走 Colocation + 专线 + 私有二进制通道。


三、UDP 组播 + 序列号 + 重传:交易所的经典分发模型

行情系统的分发从来不是一个简单的”广播”。工程上它必须同时满足三件事:低延迟、多订阅者、可恢复丢失。UDP 组播给出了前两件,序列号 + 重传通道补齐了第三件。

3.1 为什么是组播而不是单播

假设 CME 有 1000 个订阅者,每秒 100 万条 tick,每条 100 字节:

  • 单播(Unicast):交易所侧出口带宽 = 1000 × 100 万 × 100 B = 100 GB/s,明显不现实;
  • 组播(Multicast):交易所侧只发一份,由网络设备(路由器/交换机)在经过 IGMP/PIM 加入组的端口复制,出口带宽 = 1 × 100 万 × 100 B = 100 MB/s

组播的代价是需要底层网络支持,因此几乎只在交易所内部 LAN、机构 Colo 机房、运营商专线里可用。普通互联网路由器不转发组播,加密交易所对公网用户只能 TCP/WebSocket。

3.2 A/B 双线冗余:丢包第一道防线

UDP 不保证投递,中间交换机一次掉电、CPU 中断风暴都能丢包。主流交易所采用 A/B 双线(也叫 Line A / Line B):

  • 同一份消息通过两个独立组播组、两条物理路径同时发送;
  • 接收端按序列号去重,优先用先到的
  • 理论上两条线同时丢同一条的概率 = 单线丢包率²,从 10⁻⁴ 降到 10⁻⁸。

CME MDP 3.0、Nasdaq ITCH 都提供 A/B 双组播。接收端典型实现:

on_packet(pkt):
    if pkt.seq > expected:
        enqueue_gap(expected, pkt.seq)    # 记录缺口
        request_retransmit(expected, pkt.seq)  # 触发 TCP 恢复通道
    if pkt.seq == expected:
        deliver(pkt)
        expected = pkt.seq + 1
    # pkt.seq < expected:来自另一条线的重复,丢弃

3.3 Snapshot + Incremental Recovery

双线丢包仍不足以满足监管要求(任何参与者都必须能重建完整订单簿)。行情系统因此普遍采用快照 + 增量恢复

  • 增量通道(Incremental Feed):UDP 组播,每条消息带严格单调的 MsgSeqNum
  • 快照通道(Snapshot Feed):UDP 组播,周期性(秒级)重复发送当前订单簿全量 + 当时的 LastMsgSeqNumProcessed
  • 重传通道(Retransmission / Replay):TCP 单播,客户端凭缺口序列号区间 pull 历史消息。

客户端启动或大段丢包后的恢复流程:

sequenceDiagram
    participant C as 客户端
    participant I as Incremental UDP
    participant S as Snapshot UDP
    participant R as Retransmission TCP

    C->>I: 加入组播,开始缓冲增量
    C->>S: 加入组播,等一个完整快照
    S-->>C: Snapshot (seq=N, book=...)
    C->>C: 用快照初始化本地簿
    C->>C: 应用缓冲区里 seq > N 的增量
    Note right of C: 若缓冲里出现缺口 (N, M)
    C->>R: TCP Replay Request (N..M)
    R-->>C: 补齐丢失消息
    C->>C: 按序列号重新应用,本地簿与交易所一致

三个通道的职责严格单一:Incremental 只管推、不管补;Snapshot 只管”全量快照”、不管增量;Retransmission 只管按序列号范围回放。这种分离设计源于 FIX Trading Community 的 “Market Data - Incremental Refresh + Snapshot” 模型,如今在 CME MDP、Nasdaq ITCH、上交所 Level-2 里都能看到。

3.4 序列号的”单调性神圣”

行情系统里最容易出事的是序列号回退或跳跃。只要客户端观察到:

  • seq_new < seq_last:生产端重启后没持久化计数器 → 客户端无法判定数据新鲜度,只能拒绝;
  • seq_new - seq_last > 阈值:大段丢包 → 必须走快照恢复;
  • seq 在两个分片(Shard)间冲突:多生产者未做全局编号。

因此生产端的要求:

  1. 全局单调:整个 feed(或分区后的每个 channel)序列号单调递增,即便进程重启;
  2. 落盘持久化:计数器必须写到持久存储(NVM / SSD),否则重启后必须跳过一个”安全间隔”或直接抛下一档序号让客户端走 snapshot;
  3. 严禁复用:同一序号的消息内容必须一致,否则下游幂等失败。

生产事故的典型教训:2015 年 8 月纽交所因行情系统配置错误停摆 3.5 小时,根因之一就是升级后 feed 序列号管理与客户端预期不一致,大量消费者触发 resync 风暴,拖垮快照通道。

3.5 Conflation vs Full Tick

当订阅者能力有限(比如 Web App、手机端、海外跨洋链路),行情系统提供聚合(Conflation)模式:

  • Full Tick:每一个事件都发,客户端自己处理;
  • Conflation:每 N 毫秒只发最新状态(比如 100ms 一条顶档)。

Binance 的 @depth20@100ms 就是典型 Conflation:100ms 内的多笔深度变化合并成一条 20 档快照。代价是中间状态消失了,比如一个 1 手挂单 ms 级出现又被吃掉,Conflation 模式下订阅者完全看不到。

选型原则:

  • 做市商、HFT:Full Tick + MBO,因为需要队列位置;
  • 普通算法策略:Full Tick + MBP
  • 散户 App、资讯门户:Conflation + L1/少档 L2
  • 远程跨洋用户:Conflation + Full Snapshot,牺牲细节换传输量。

四、物理层:把延迟压到物理极限

在撮合引擎之外,行情分发是交易所内部对延迟最敏感的一层。所有”快一毫秒”的优化都会落到这里。

4.1 内核旁路(Kernel Bypass)

传统 Linux 网络栈从网卡到应用态要经过:中断 → softirq → skb → socket buffer → recvmsg 系统调用 → 用户态。即便打开 NAPI、调优 NIC offload,一个收包路径也要几微秒。

内核旁路直接绕过内核:

方案 厂商 / 来源 特点
Solarflare OpenOnload AMD Xilinx(原 Solarflare) LD_PRELOAD 劫持 socket API,用户态协议栈,延迟 ~1μs
Mellanox VMA / libvma NVIDIA(原 Mellanox) 同理,Infiniband / RoCE 场景常用
DPDK Intel / 开源 Poll Mode Driver,100% CPU 轮询,纯用户态
ef_vi Solarflare 原生 API 比 OpenOnload 更底层,几百纳秒
XDP / AF_XDP Linux 内核 eBPF 在驱动层截获包,开源免费替代

行情分发侧典型做法:

  1. 生产端用 DPDK / ef_vi 直接构造 UDP 包送到网卡,跳过内核;
  2. 接收端关键策略(做市、套利)用 OpenOnload / VMA,不改应用代码;
  3. 非关键消费者(合规、存档)用普通 Linux 网络栈,降低维护成本。

4.2 Colocation(共址)

交易所对外提供”机柜租赁”服务,让客户的服务器摆在撮合引擎”隔壁”:

  • NYSE:Mahwah, NJ 数据中心,Colo 租金 + 带宽 + 交叉连接,月租数千美元起;
  • Nasdaq:Carteret, NJ;
  • CME:Aurora, IL;
  • LSE:Slough, UK;
  • 上交所 / 深交所:托管机房,券商与交易所同机房,跨机房约 2–3ms;
  • Binance Futures:AWS Tokyo ap-northeast-1 上部署,客户可以在同 AZ 部署拿到次毫秒延迟。

公平接入原则要求所有 Colo 客户到撮合引擎的物理线缆长度完全一致(到米级甚至厘米级),因此交易所会刻意”盘线”让每个机柜绕等长路径。

4.3 微波 / 激光 / 毫米波:城际级物理优化

芝加哥(CME)到纽约(NYSE/Nasdaq)约 1200 公里。光纤在玻璃里的折射率约 1.47,光速 20.4 万公里/秒,双向单程约 6ms,加上交换延迟约 8ms。微波在大气中接近真空光速,同距离约 3.9ms,节省 4ms

链路 媒介 单程延迟(芝加哥↔︎纽约)
金融专线光纤(最短路径) 单模光纤 ~8 ms
Spread Networks(2010,专修短路径光纤) 单模光纤 ~6.65 ms
McKay Brothers / Tradeworx 微波 11 GHz / 18 GHz 微波 ~4.1 ms
Jump Trading 毫米波 / 激光 更高频段 ~3.95 ms

微波的问题是雨衰:一场暴雨能让链路丢包到不可用。HFT 公司因此同时运营微波 + 光纤备份,恶劣天气自动切换。这些优化单独看只值几毫秒,但在跨所套利(NYSE 和 Nasdaq 之间、CME 和 NYSE 之间)里,快几毫秒就意味着每次套利多赚一整轮

4.4 时钟同步:PTP 而不是 NTP

行情消息必须带交易所时间戳,且精度要高到可以判断 tick 先后。NTP(网络时间协议)精度在毫秒级,对行情远远不够。交易所和券商普遍用 PTP(Precision Time Protocol,IEEE 1588v2) + GPS 钟源:

  • PTP 在硬件时钟同步下精度到亚微秒
  • GPS 天线接入交易所屋顶做主时钟(Grandmaster);
  • 所有网卡支持 PTP 硬件时间戳(HW Timestamp),在 NIC 入口打 64-bit 纳秒戳;
  • MiFID II 在欧盟要求所有可报告事件时间戳精度到 100μs,HFT 要求 1μs。

五、Tick 数据存储:一天 TB 级的流水账

行情系统不仅要发,还要存。一家中等规模交易所的完整 tick 一天能写 500 GB – 5 TB 原始数据,业界的存储方案围绕”高写入 + 范围扫描 + 时间维度聚合”展开。

5.1 kdb+:Wall Street 的历史默认

kdb+ 是 Kx Systems 1993 年开始开发的列式时序库,搭配 q 语言。在顶级投行(Morgan Stanley、GS、JPM、Citadel、Two Sigma)、主流交易所(LSE、ICE)几乎无处不在。关键特性:

  • 按日分区(Date-Partitioned Splayed Table):每天一个目录,每列一个文件;
  • 内存映射(mmap):列数据直接映射,q 的 select 是对内存数组的向量操作;
  • 原生时间类型timestamp 精度到纳秒;
  • 典型查询
/ 取 AAPL 当天 09:3010:00 所有 tick 的 VWAP
select vwap:size wavg price from trade where date=.z.d, sym=`AAPL,
  time within 09:30:00.000 10:00:00.000
  • 缺点:商业许可昂贵(单 core 年费数万美元)、q 语言陡峭、容错靠外部工程师扛。

5.2 ClickHouse:近几年最流行的开源替代

ClickHouse 从 Yandex 内部搜索日志库演化而来,2016 开源。在金融 tick 存储场景被大量采用(Cloudflare、Bloomberg 内部工具、许多加密量化基金):

  • MergeTree 引擎按主键(通常 (symbol, ts))排序,范围扫描极快;
  • 列压缩(LZ4 / ZSTD)把原始 tick 压到 1/10;
  • 原生支持 AggregatingMergeTree 直接维护 K 线;
  • SQL 友好,生态成熟。

典型 tick 表:

CREATE TABLE market.trade
(
    symbol       LowCardinality(String),
    ts           DateTime64(9, 'UTC'),     -- 纳秒时间戳
    price        Decimal(18, 8),
    size         Decimal(18, 8),
    side         Enum8('B'=1,'S'=2),
    trade_id     UInt64,
    seq          UInt64
)
ENGINE = MergeTree
PARTITION BY toYYYYMMDD(ts)
ORDER BY (symbol, ts)
SETTINGS index_granularity = 8192;
-- 1 分钟 K 线(即席查询,不建物化视图)
SELECT
    symbol,
    toStartOfMinute(ts) AS bar_ts,
    argMin(price, ts)   AS open,
    max(price)          AS high,
    min(price)          AS low,
    argMax(price, ts)   AS close,
    sum(size)           AS volume,
    sum(price * size) / sum(size) AS vwap
FROM market.trade
WHERE symbol = 'BTCUSDT'
  AND ts >= '2026-04-22 00:00:00'
  AND ts <  '2026-04-23 00:00:00'
GROUP BY symbol, bar_ts
ORDER BY bar_ts;

5.3 QuestDB / TimescaleDB / InfluxDB

  • QuestDB:专做金融 tick,SQL + Java 内核,支持 SAMPLE BY 直接出 K 线,内存映射文件存储,开源;
  • TimescaleDB:Postgres 扩展,兼容性最好但写入吞吐较低,适合中频场景;
  • InfluxDB:IoT 时序为主,金融用较少,主要因为 Tag 基数(Cardinality)爆炸会出问题。

5.4 Parquet + Arrow:长期归档与回测

实时热库用 kdb+/ClickHouse,历史冷数据普遍落到 Parquet + S3 / OSS

  • date=2026-04-22/symbol=AAPL/part-000.parquet 分区;
  • 列压缩 + 字典编码,AAPL 一天的 tick 压到几十 MB;
  • 用 Apache Arrow / DuckDB / Polars / Spark 做回测分析,零序列化;
  • 回测团队拉一个本地 Parquet 子集就能跑策略,不用打扰生产库。

六、行情回放与回测:事件时间才是”真时间”

量化策略研发离不开回测(Backtest),回测的第一原则是重放事件时间而不是墙上时钟

6.1 两类时间戳的分离

时间 含义 来源
事件时间(Event Time) 行情本身发生/交易所发出的时间戳 交易所 feed 内嵌的 TransactTime
处理时间(Processing Time) 回放系统当前挂钟时间 time.Now()
发送时间(Send Time) 生产端发包时间 Wire Header
接收时间(Receive Time) 消费端网卡 HW 时间戳 NIC timestamp

回测必须按事件时间排序、按事件时间触发策略计算。用挂钟一步步等会导致:

  • 重放一天行情要等整整一天(经典菜鸟错误);
  • 跨品种事件没法按真实顺序交织。

6.2 基本回放引擎伪代码

def replay(events, handler, speed=0.0):
    """
    events: 按 event_time 升序的迭代器
    speed:  0 表示尽快重放;1.0 表示实时;0.5 表示慢速
    """
    wall_start = time.time()
    event_start = None
    for ev in events:
        if event_start is None:
            event_start = ev.ts
        if speed > 0:
            elapsed_event = (ev.ts - event_start) / speed
            elapsed_wall = time.time() - wall_start
            sleep_for = elapsed_event - elapsed_wall
            if sleep_for > 0:
                time.sleep(sleep_for)
        handler.on_event(ev)  # 策略看到的是 ev.ts

6.3 Look-ahead Bias:回测最致命的陷阱

前视偏差是指策略在某时刻看到了该时刻之后才应该发生的信息。典型错误:

  • 用某天 K 线的 close 价生成当天早盘信号;
  • 行情 + 成交两条 feed 没按事件时间合并,策略看到了”先成交后挂单”;
  • 回测时把交易成本按实时 BBO 算,但实际下单时盘口已经变了。

生产级回测框架必备:

  1. 单调推进的事件时钟:handler 之外没有任何数据来源能”看未来”;
  2. 撮合模拟器:策略下单后必须走模拟撮合,按当时 Order Book 真实可成交量给出成交价;
  3. Latency Model:下单、撤单必须加一个可配置的延迟(几十 μs 到几毫秒),模拟真实到达交易所的延迟。

七、从逐笔到盘口:K 线、VWAP、TWAP 的合成

L3 事件流是所有衍生行情指标的”原子数据”。常见的合成:

7.1 K 线(OHLCV Bar)

K 线(Candlestick Bar)的定义:在一个时间窗口 [t, t+Δt) 内:

Open   = 窗口内第一笔成交价
High   = 窗口内最高成交价
Low    = 窗口内最低成交价
Close  = 窗口内最后一笔成交价
Volume = 窗口内成交量之和

对 1 分钟 K 线,标准实现是一个增量聚合器:

type BarAgg struct {
    Open, High, Low, Close float64
    Volume                 float64
    Start                  time.Time
    HasData                bool
}

func (b *BarAgg) OnTrade(ts time.Time, price, size float64) {
    if !b.HasData {
        b.Start = ts.Truncate(time.Minute)
        b.Open, b.High, b.Low, b.Close = price, price, price, price
        b.Volume = size
        b.HasData = true
        return
    }
    if price > b.High { b.High = price }
    if price < b.Low  { b.Low  = price }
    b.Close = price
    b.Volume += size
}

边界处理是坑点:一根分钟 K 线在当分钟没有一笔成交时应当沿用上一根 Close 形成十字线,还是跳空?不同数据源约定不一,量化回测里要统一。

7.2 VWAP(成交量加权平均价)

VWAP(t) = Σ(price_i × size_i) / Σ(size_i),i 为 [0, t] 所有成交

VWAP 两种用法:

  • 日内 VWAP:算法单执行的基准价,券商承诺客户”当日成交均价不劣于 VWAP”;
  • 滚动 VWAP:策略信号平滑。

7.3 TWAP(时间加权平均价)

TWAP(t) = ∫(price ds) / (t - t₀),在无连续价的离散场景下常用分钟 close 等间隔采样

TWAP 在非连续交易(场外、低流动性)或滑点控制里比 VWAP 更合理,因为它不被大单拖偏。

7.4 MBP 盘口合成:从 L3 到 L2

从逐笔委托 + 逐笔成交(国内 Level-2 的原料)重建 MBP 盘口是国内量化最常做的预处理:

  1. 维护一个 map<price, size> 的买/卖侧;
  2. AddOrder(side, price, size)map[price] += size
  3. Cancel(order_id, remain_size)map[price] -= remain_size,若为 0 删除;
  4. Trade(order_id, size):根据 order_id 查到价位,map[price] -= size

生产级实现会额外维护每个价位的订单列表以支持从 MBP 反推 MBO。


八、代码示例:订阅 Binance 深度并本地维护订单簿

Binance 现货深度 WebSocket 是加密市场最常见的 Diff Depth 模式。官方文档给出的“How to manage a local order book correctly”流程是:

  1. 开启 WebSocket @depth@100ms 流(即 bnbbtc@depth@100ms),把增量先缓冲
  2. 调 REST GET /api/v3/depth?symbol=BNBBTC&limit=1000 拿一次 snapshot,记下 lastUpdateId;
  3. 丢弃缓冲中 u < lastUpdateId + 1 的事件;
  4. 第一条应用的事件必须满足 U <= lastUpdateId + 1 <= u
  5. 此后每条事件必须满足 U == prev_u + 1,否则重来第 2 步。

8.1 Python 实现

import asyncio
import json
import heapq
import httpx
import websockets
from collections import OrderedDict

SYMBOL = "bnbbtc"
WS_URL = f"wss://stream.binance.com:9443/ws/{SYMBOL}@depth@100ms"
REST_URL = f"https://api.binance.com/api/v3/depth?symbol={SYMBOL.upper()}&limit=1000"

class OrderBook:
    def __init__(self):
        self.bids: dict[float, float] = {}
        self.asks: dict[float, float] = {}
        self.last_update_id: int = 0

    def apply(self, side: dict[float, float], updates: list[list[str]]):
        for price_s, qty_s in updates:
            price, qty = float(price_s), float(qty_s)
            if qty == 0.0:
                side.pop(price, None)
            else:
                side[price] = qty

    def best(self):
        bb = max(self.bids) if self.bids else None
        ba = min(self.asks) if self.asks else None
        return bb, ba

async def run():
    async with websockets.connect(WS_URL) as ws:
        buffer = []
        # 1) 先缓冲增量
        async def buffer_task():
            async for raw in ws:
                buffer.append(json.loads(raw))
        buf = asyncio.create_task(buffer_task())
        await asyncio.sleep(0.5)  # 给 WS 一点时间积累事件

        # 2) 拉快照
        async with httpx.AsyncClient() as c:
            snap = (await c.get(REST_URL)).json()
        book = OrderBook()
        book.last_update_id = snap["lastUpdateId"]
        book.apply(book.bids, snap["bids"])
        book.apply(book.asks, snap["asks"])

        # 3) 丢弃旧事件
        fresh = [e for e in buffer if e["u"] > book.last_update_id]
        if not fresh or not (fresh[0]["U"] <= book.last_update_id + 1 <= fresh[0]["u"]):
            raise RuntimeError("快照与增量不连续,重来")

        prev_u = book.last_update_id
        for ev in fresh:
            if ev["U"] != prev_u + 1:
                raise RuntimeError("增量序列号断裂,重来")
            book.apply(book.bids, ev["b"])
            book.apply(book.asks, ev["a"])
            prev_u = ev["u"]
        buffer.clear()

        # 4) 进入稳态循环
        async for raw in ws:
            ev = json.loads(raw)
            if ev["U"] != prev_u + 1:
                # 真实实现:重启整个流程
                buf.cancel()
                return await run()
            book.apply(book.bids, ev["b"])
            book.apply(book.asks, ev["a"])
            prev_u = ev["u"]
            bb, ba = book.best()
            print(f"BBO bid={bb} ask={ba} spread={ba-bb:.8f}")

asyncio.run(run())

8.2 Go 实现(核心片段)

type Book struct {
    Bids, Asks   map[float64]float64
    LastUpdateID int64
}

func (b *Book) apply(side map[float64]float64, updates [][]string) {
    for _, pq := range updates {
        price, _ := strconv.ParseFloat(pq[0], 64)
        qty, _ := strconv.ParseFloat(pq[1], 64)
        if qty == 0 {
            delete(side, price)
        } else {
            side[price] = qty
        }
    }
}

type DepthEvent struct {
    E int64      `json:"E"`
    U int64      `json:"U"`   // first update id
    Uu int64     `json:"u"`   // last update id
    B [][]string `json:"b"`
    A [][]string `json:"a"`
}

func maintainBook(ctx context.Context) error {
    ws, _, err := websocket.DefaultDialer.DialContext(ctx,
        "wss://stream.binance.com:9443/ws/bnbbtc@depth@100ms", nil)
    if err != nil { return err }
    defer ws.Close()

    buf := make([]DepthEvent, 0, 1024)
    bufDone := make(chan struct{})
    go func() {
        for {
            var ev DepthEvent
            if err := ws.ReadJSON(&ev); err != nil { close(bufDone); return }
            buf = append(buf, ev)
        }
    }()
    time.Sleep(500 * time.Millisecond)

    // 拉快照
    resp, err := http.Get("https://api.binance.com/api/v3/depth?symbol=BNBBTC&limit=1000")
    if err != nil { return err }
    defer resp.Body.Close()
    var snap struct {
        LastUpdateID int64      `json:"lastUpdateId"`
        Bids         [][]string `json:"bids"`
        Asks         [][]string `json:"asks"`
    }
    json.NewDecoder(resp.Body).Decode(&snap)

    book := &Book{Bids: map[float64]float64{}, Asks: map[float64]float64{}}
    book.LastUpdateID = snap.LastUpdateID
    book.apply(book.Bids, snap.Bids)
    book.apply(book.Asks, snap.Asks)

    // 应用缓冲(跳过过期事件、校验首条跨接)
    prevU := book.LastUpdateID
    started := false
    for _, ev := range buf {
        if ev.Uu <= prevU { continue }
        if !started {
            if !(ev.U <= prevU+1 && prevU+1 <= ev.Uu) {
                return fmt.Errorf("snapshot/incremental gap")
            }
            started = true
        } else if ev.U != prevU+1 {
            return fmt.Errorf("gap in buffered incrementals")
        }
        book.apply(book.Bids, ev.B)
        book.apply(book.Asks, ev.A)
        prevU = ev.Uu
    }

    // 稳态
    for {
        var ev DepthEvent
        if err := ws.ReadJSON(&ev); err != nil { return err }
        if ev.U != prevU+1 { return fmt.Errorf("gap, need resync") }
        book.apply(book.Bids, ev.B)
        book.apply(book.Asks, ev.A)
        prevU = ev.Uu
    }
}

8.3 生产级细节

上面的示例代码只到”能跑”,离生产还差几步:

  1. gap 触发全量 resync:不要 panic,要像 CME 客户端一样退回 snapshot 状态机;
  2. Bid/Ask 用排序结构map 取 BBO 是 O(N),生产用 B+树 / skip list / TreeMap;
  3. Price 用整数:浮点比较最终会坑你,币安的价格精度是定义好的 tick size,用 Int64 × 10⁸;
  4. 时间戳用交易所 E 字段:不要用本地 time.Now() 做策略时钟;
  5. 断线自动重连 + 指数退避,并把 resync 计数作为监控指标;
  6. Checksum 校验(Kraken、OKX 提供):用收到的 checksum 对本地簿 top N 档哈希比对,不一致则重来。

九、行情分发链路与多路径重传(SVG)

撮合引擎 Matching Engine 事件流 / 序列号生成

行情生产端 Feed Publisher MBO→MBP→L1 · 序列号 · 编码 (ITCH/SBE)

快照生成 Snapshot Publisher 周期全量盘口 · 带 LastMsgSeqNumProcessed

增量组播 A UDP Multicast 主路径 · 低延迟 · 可能丢包

增量组播 B UDP Multicast 冗余路径 · 不同交换机 · 去重合并

快照组播 Snapshot UDP 周期重播全量簿 · 新客户端/大段丢包恢复

重传通道 Retransmission TCP 按序列号区间 pull · 补小段缺口

做市 / HFT Kernel Bypass 本地 MBO 簿 队列位置估算 μs 级决策

算法策略 MBP 10 档 VWAP / TWAP Signal 生成 ms 级反应

风控 / 清算 最新成交 保证金重算 强平触发 100ms 窗口

Tick 存储 / 监管 kdb+ / ClickHouse Parquet 归档 监管报送 CAT/TR 回测数据源

图中实线代表主推流(UDP 组播),橙色代表快照恢复路径,红色虚线代表按需 TCP 重传。做市商和算法同时订阅 A、B 两条增量组播并做去重,风控与存档由于对延迟不敏感可以共用快照通道。


十、消费者:同一份行情,五种不同的用法

行情数据在交易所下游至少有五类消费者,对应五种 SLA 与五种失败模式

10.1 交易策略(Strategy)

  • 需求:最低延迟、Full Tick、MBO;
  • 失败模式:延迟跳变(Latency Spike)→ 信号基于过期价 → 亏损;
  • 工程策略:Colocation + Kernel Bypass + 热备策略自动切换。

10.2 风控(Risk)

  • 需求:最新成交价用于 PnL 与保证金;时序上允许 10–100ms 延迟;
  • 失败模式:行情中断但仍放新单 → 风险敞口失控(骑士资本类事故);
  • 工程策略:Stale Price 检测,行情停滞超阈值自动进入只减仓(Reduce-Only)模式。

10.3 清算(Clearing)

  • 需求:日终结算价(Settlement Price),一般是收盘集合竞价均价或 VWAP;
  • 工程策略:事后批处理计算,容忍几分钟延迟但要求可审计。

10.4 监管报送

  • 美国:CAT(Consolidated Audit Trail)要求所有订单与成交上报 SRO,时钟同步 100μs;
  • 欧盟:MiFID II RTS 25 时钟同步、RTS 22 交易报告;
  • 中国:沪深交易所要求券商异常交易、频繁报撤单监控并上报证监会;
  • 加密:多数管辖区要求合规 VASP 上报可疑交易(SAR / STR)。

这条路径的关键不是延迟,而是完整性与不可篡改。行情存档必须是 WORM(Write-Once-Read-Many)介质或经审计的云 Object Lock。

10.5 普通用户 / 资讯

  • App、K 线图、门户;
  • Conflation + Full Snapshot + CDN;
  • 最大容忍延迟:秒级。

十一、工程坑点

11.1 序列号不是业务 ID。业务上的 trade_id、order_id 由撮合引擎分配;feed 的 MsgSeqNum 由行情生产端分配。两个命名空间独立,重启/切换时一定要分开持久化。

11.2 组播 IGMP 泄漏。客户端没有 IGMP leave 就崩溃,交换机仍会向该端口投递流量,下一个新程序上来可能收到陈旧副本。生产端要设置 Source-Specific Multicast(SSM)+ 客户端显式 IP_DROP_MEMBERSHIP

11.3 快照”太小”或”太稀”都出问题。快照频率太低,新客户端启动时需要等下一个周期,冷启动慢;太高则吃网络带宽。典型做法是分片:整个订单簿按 symbol 切成 N 片,每片 1–5 秒轮发一次。

11.4 盘口深度用定点数而不是浮点price = 12.30 在 IEEE 754 里不是精确的 12.3,累加几万次会漂。所有价格按 tick size 换成 Int64。

11.5 WebSocket 背压(Back-pressure)。订阅端消费慢时服务端缓冲积累,典型故障:Binance 在极端行情下会对慢客户端主动断开。客户端要统计 WS 读取到处理的时延,超过阈值主动降级(减少订阅对或开多进程分担)。

11.6 时钟回跳。NTP 小步校时可能让进程级 time.Now() 回跳几毫秒,导致”新消息时间早于旧消息”。用 单调时钟(Monotonic Clock)做内部排序,UTC 时钟只做展示和报送。

11.7 跨集群序列号合并。为吞吐分片后,每个 shard 有自己的序列号。下游全局存储时要用 (shard_id, seq) 复合键,或在 Kafka/消息总线侧重新分配全局 offset。

11.8 回测”完美”陷阱。历史 tick 重放时策略的成交假设往往过于乐观——“挂单马上在最优价成交”——实际在真实盘口里同价位前面可能排着几百手队列。合理的成交模拟要结合 MBO 的 Queue Position。

11.9 行情停发的优雅处理。交易所技术故障会停发行情(2015 NYSE、2020 TSE),下游不能假设”没有消息 = 价格不动”,要做 heartbeat 超时告警并冻结策略。

11.10 协议版本迁移。FAST / SBE 模板升级是典型的”鸡生蛋”问题:客户端升级慢,交易所升级必须保留老版本并行发布 6–12 个月。上线前必须提供模板差异工具和对比测试集。


十二、选型建议与落地清单

12.1 选型建议

场景 建议
证券 / 期货交易所自研行情 ITCH 风格定长二进制 或 SBE;A/B 双 UDP 组播;独立 snapshot 与 TCP replay
券商前置 / 行情中台 接入交易所原生协议 + 对外统一归一化(FIX/WebSocket),按业务分层转发
加密现货 / 衍生品交易所 WebSocket JSON 主通道 + REST snapshot + 机构 FIX 通道,checksum 必备
量化策略消费端 本地维护 MBP + MBO;序列号严格校验;gap → 全量 resync,不容忍”容忍”
历史 tick 存储 热:kdb+(预算充足)或 ClickHouse(开源优先);冷:Parquet + S3/OSS
回测平台 事件时间驱动 + 撮合模拟 + Latency Model,三者缺一不可
跨地域分发 核心市场 Colo 接入 + 微波/光纤跨城 + CDN 分发给终端

12.2 落地清单

生产环境上线行情系统前,至少核对以下 20 项:

  1. 序列号全局单调,跨进程重启可恢复,NVM/SSD 持久化;
  2. 增量通道 A/B 双组播,默认开启去重;
  3. 快照通道周期 ≤ 5 秒,按 symbol 分片轮发;
  4. TCP 重传通道鉴权与流控,单客户端限速;
  5. 客户端 IGMP 退订逻辑经过故障演练;
  6. 端到端延迟(发布→订阅)监控 P50/P99/P999,告警阈值分档;
  7. 价格字段全部定点整数;
  8. 时间戳统一纳秒 + 单调时钟双轨;
  9. 服务端 PTP 同步,所有机器时钟偏差 < 1μs;
  10. 生产端丢包率监控,策略客户端 gap 次数告警;
  11. Conflation 与 Full Tick 两套频道,按订阅权限控制;
  12. 压力测试覆盖:日内峰值 ×3、长时间(4 小时)满载、突发 burst;
  13. 失败演练:交换机端口 down、冷启动 resync、时钟跳变、网卡丢包注入;
  14. 协议版本兼容性:老版本至少并行发布 6 个月;
  15. 合规存档:WORM 存储、保留年限符合监管要求;
  16. 监管报送链路独立部署,不影响主分发;
  17. 历史 tick 库按日分区,每日自动归档到冷存储;
  18. 行情停发检测 heartbeat + 自动冻结下游策略;
  19. 客户端 SDK 覆盖 Java / C++ / Go / Python,所有 SDK 共享同一套协议测试集;
  20. 文档(协议、订阅方法、示例代码、故障自诊断清单)对公,版本一致。

十三、真实事件与教训

13.1 2015-07-08 NYSE 停摆 3.5 小时

纽交所在当天上午因前一晚发布的行情软件升级配置与客户端协议不一致,导致大面积断连。Pillar 平台上线后的消费者反复触发 snapshot resync 使压力级联到内部总线,最终交易所宣布停止全部股票交易接近 3.5 小时。教训:协议升级必须有”开关”可以回退到上一版本;客户端必须限制 resync 频率防止风暴。

13.2 2020-10-01 东京证交所(TSE)全天停牌

原因是共享硬盘某个存储板的自动切换失败,行情系统启动时无法拉到前一天收盘状态,所有上市股票交易中断整整一天。这是日本自 1999 年全电子化以来最严重事故。教训:存储冗余的failover 本身必须演练,行情生产端的”冷启动对初态的依赖”是隐藏风险源。

13.3 国内 Level-2 “逐笔委托泄漏”争议

2020–2021 年间国内多家券商因 Level-2 数据重分发权限模糊,出现 “散户用户看到专业级 MBO”引发的合规核查。核心问题是行情权限的技术层与合规层没有对齐:协议上能分发,许可上不允许。教训:行情系统必须在生产端(而不是客户端)根据订阅合同裁剪字段,客户端不可被信任。

13.4 2021-05-19 Binance 极端行情 WS 断流

加密市场当日多头大规模爆仓,Binance 现货 / 合约 WebSocket 因后端 Kafka 队列堆积对大量慢消费者主动断开,部分做市商被迫只能 REST 轮询 /depth 导致滑点放大。教训:公开 API 的背压策略必须写进 SLA 文档;机构级客户应走独立专线而非公共 WebSocket。

13.5 2010-05-06 美股 Flash Crash

虽然根因在大单 + 算法 + 流动性撤退的共振,但 SEC 调查报告指出,多个做市商在事故期间停订阅主行情转用备用慢行情,进一步抽走了流动性。教训:行情系统的可用性和资本市场稳定性直接挂钩,不是单个交易所的内部问题。


十四、与本系列其他文章的交叉引用

  • 第 15 篇《交易所核心系统架构》:行情系统在五大子系统中的位置,以及与订单网关、清算的边界;
  • 第 16 篇《撮合引擎实现》:行情的源头是撮合引擎发出的事件流,理解 MBO 必须先理解 Order Book 内部;
  • 第 18 篇《证券登记结算》:行情里的成交最终进入登记结算机构完成 DvP;
  • 第 19 篇《实时风控引擎》:风控对行情的消费模式和 stale price 处理;
  • 第 24 篇《金融级可靠性》:行情系统的两地三中心、切换演练与 RPO/RTO 目标。

参考资料


上一篇《撮合引擎实现:撮合算法、价格优先时间优先、状态机、低延迟工程》

下一篇《证券登记结算:中证登、DTCC、Euroclear、T+1、DvP》

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-22 · architecture / fintech

【金融科技工程】交易所核心系统架构:撮合、行情、做市、风控、清算

从订单网关到撮合引擎、从 L1/L2/L3 行情到清算与结算,系统梳理证券、期货、加密货币交易所的五大核心子系统;给出低延迟工程技术栈(Disruptor、Kernel Bypass、FPGA)、订单生命周期状态机、主流交易所(NYSE Pillar、Nasdaq INET、上交所新一代、CME Globex、Binance、dYdX v4)对比、以及 Flash Crash 与 Knight Capital 的工程教训。

2026-04-22 · architecture / fintech

【金融科技工程】金融科技工程全景:从支付到交易所的系统分类与读图

金融科技(FinTech)不是普通后端加一张账户表。钱的原子性、监管的硬边界、一个小数点的代价,把这个领域推进到工程强度最高的那一档。本文是【金融科技工程】25 篇的总目录与阅读地图:先交代为什么它比一般业务系统更难,再给出对账体、支付体、交易体、风控合规体四维分类,把后续 24 篇挂到骨架上,最后给出一份绿地项目的落地顺序建议。

2026-04-22 · architecture / fintech

【金融科技工程】复式记账工程化:科目、分录、余额、对账

把 500 年历史的复式记账翻译成工程师可以落地的数据模型、SQL 表结构与余额计算策略,覆盖充值、下单、退款、分润、红包、多币种与冲销的真实场景,并对比 TigerBeetle、beancount、Ledger CLI、Square LedgerDB、Stripe Ledger 等开源与工业实现。