引言:行情是撮合的”出口”,也是整个市场的”公共视野”
上一篇《撮合引擎实现》讲清了订单簿(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 毫秒。
行情系统真正的复杂度不在”推”,而在:
- 多份副本,多种协议,多个消费者,还得一致。一笔成交要同时以 L1(Level-1,顶档)、L2(Level-2,多档)、L3(Level-3,逐笔/逐单)形式发给散户行情、机构行情、内部风控、监管报送、历史库 Tick 存储,任何一路错位都会引发套利漏洞或监管追责。
- UDP 必然丢包,序列号必须严格单调,恢复必须在微秒级完成。组播快但不可靠,TCP 可靠但慢,生产系统要两者兼备。
- 历史行情既要完全真实,又要能”快进”回放用于回测。事件时间(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 在行情端几乎只剩两个位置还在用:
- 机构与经纪商之间的订单 + 低频行情:FIX Session 层提供 Heartbeat、Resend Request、Sequence Number 管理,是久经考验的双向连接协议;
- 加密市场的机构接入:不少 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 Order、Order Executed、Order Cancel、Trade、System Event等消息类型; - OUCH:下单(客户端 → 交易所),最短的新单消息只有 49 字节。
ITCH 的关键设计:
- 固定长度:比如
Add Order消息恒定 36 字节(ITCH 5.0),接收端按长度直接memcpy到结构体,不需要任何解析; - 大端字节序(Network Byte Order):跨平台一致;
- 价格用整数:比如 price × 10000 存成 Int32,避免浮点;
- 消息类型用单字节 ASCII:
A= 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)间冲突:多生产者未做全局编号。
因此生产端的要求:
- 全局单调:整个 feed(或分区后的每个 channel)序列号单调递增,即便进程重启;
- 落盘持久化:计数器必须写到持久存储(NVM / SSD),否则重启后必须跳过一个”安全间隔”或直接抛下一档序号让客户端走 snapshot;
- 严禁复用:同一序号的消息内容必须一致,否则下游幂等失败。
生产事故的典型教训: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 在驱动层截获包,开源免费替代 |
行情分发侧典型做法:
- 生产端用 DPDK / ef_vi 直接构造 UDP 包送到网卡,跳过内核;
- 接收端关键策略(做市、套利)用 OpenOnload / VMA,不改应用代码;
- 非关键消费者(合规、存档)用普通 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:30–10: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.ts6.3 Look-ahead Bias:回测最致命的陷阱
前视偏差是指策略在某时刻看到了该时刻之后才应该发生的信息。典型错误:
- 用某天 K 线的 close 价生成当天早盘信号;
- 行情 + 成交两条 feed 没按事件时间合并,策略看到了”先成交后挂单”;
- 回测时把交易成本按实时 BBO 算,但实际下单时盘口已经变了。
生产级回测框架必备:
- 单调推进的事件时钟:handler 之外没有任何数据来源能”看未来”;
- 撮合模拟器:策略下单后必须走模拟撮合,按当时 Order Book 真实可成交量给出成交价;
- 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 盘口是国内量化最常做的预处理:
- 维护一个
map<price, size>的买/卖侧; AddOrder(side, price, size):map[price] += size;Cancel(order_id, remain_size):map[price] -= remain_size,若为 0 删除;Trade(order_id, size):根据 order_id 查到价位,map[price] -= size。
生产级实现会额外维护每个价位的订单列表以支持从 MBP 反推 MBO。
八、代码示例:订阅 Binance 深度并本地维护订单簿
Binance 现货深度 WebSocket 是加密市场最常见的 Diff Depth 模式。官方文档给出的“How to manage a local order book correctly”流程是:
- 开启 WebSocket
@depth@100ms流(即bnbbtc@depth@100ms),把增量先缓冲; - 调 REST
GET /api/v3/depth?symbol=BNBBTC&limit=1000拿一次 snapshot,记下lastUpdateId; - 丢弃缓冲中
u < lastUpdateId + 1的事件; - 第一条应用的事件必须满足
U <= lastUpdateId + 1 <= u; - 此后每条事件必须满足
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 生产级细节
上面的示例代码只到”能跑”,离生产还差几步:
- gap 触发全量 resync:不要 panic,要像 CME 客户端一样退回 snapshot 状态机;
- Bid/Ask 用排序结构:
map取 BBO 是 O(N),生产用 B+树 / skip list / TreeMap; - Price 用整数:浮点比较最终会坑你,币安的价格精度是定义好的 tick size,用 Int64 × 10⁸;
- 时间戳用交易所
E字段:不要用本地time.Now()做策略时钟; - 断线自动重连 + 指数退避,并把 resync 计数作为监控指标;
- Checksum 校验(Kraken、OKX 提供):用收到的 checksum 对本地簿 top N 档哈希比对,不一致则重来。
九、行情分发链路与多路径重传(SVG)
图中实线代表主推流(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 项:
- 序列号全局单调,跨进程重启可恢复,NVM/SSD 持久化;
- 增量通道 A/B 双组播,默认开启去重;
- 快照通道周期 ≤ 5 秒,按 symbol 分片轮发;
- TCP 重传通道鉴权与流控,单客户端限速;
- 客户端 IGMP 退订逻辑经过故障演练;
- 端到端延迟(发布→订阅)监控 P50/P99/P999,告警阈值分档;
- 价格字段全部定点整数;
- 时间戳统一纳秒 + 单调时钟双轨;
- 服务端 PTP 同步,所有机器时钟偏差 < 1μs;
- 生产端丢包率监控,策略客户端 gap 次数告警;
- Conflation 与 Full Tick 两套频道,按订阅权限控制;
- 压力测试覆盖:日内峰值 ×3、长时间(4 小时)满载、突发 burst;
- 失败演练:交换机端口 down、冷启动 resync、时钟跳变、网卡丢包注入;
- 协议版本兼容性:老版本至少并行发布 6 个月;
- 合规存档:WORM 存储、保留年限符合监管要求;
- 监管报送链路独立部署,不影响主分发;
- 历史 tick 库按日分区,每日自动归档到冷存储;
- 行情停发检测 heartbeat + 自动冻结下游策略;
- 客户端 SDK 覆盖 Java / C++ / Go / Python,所有 SDK 共享同一套协议测试集;
- 文档(协议、订阅方法、示例代码、故障自诊断清单)对公,版本一致。
十三、真实事件与教训
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 目标。
参考资料
- FIX Trading Community:FIX 5.0 SP2 与 SBE Technical Standard,https://www.fixtrading.org/standards/
- Nasdaq:TotalView-ITCH 5.0 Specification,https://www.nasdaqtrader.com/content/technicalsupport/specifications/dataproducts/NQTVITCHSpecification.pdf
- CME Group:MDP 3.0 Market Data Platform,https://www.cmegroup.com/confluence/display/EPICSANDBOX/MDP+3.0+-+Market+Data
- SEC:Findings Regarding the Market Events of May 6, 2010(Flash Crash 报告),https://www.sec.gov/news/studies/2010/marketevents-report.pdf
- SEC:In the Matter of Knight Capital Americas LLC(骑士资本处罚书),https://www.sec.gov/litigation/admin/2013/34-70694.pdf
- Kx Systems:kdb+ 与 q 语言文档,https://code.kx.com/q/
- ClickHouse 官方文档:MergeTree 引擎与时序优化,https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/mergetree
- Binance:How to manage a local order book correctly,https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams
- Real Logic:Simple Binary Encoding,https://github.com/real-logic/simple-binary-encoding
- McKay Brothers:Microwave Network White Paper,https://www.mckay-brothers.com/
- 上海证券交易所:Level-2 行情数据接口规范(公开版),https://www.sse.com.cn/services/tradingservice/dataservice/
- 深圳证券交易所:数据接口规范(STEP / BIN),https://www.szse.cn/marketdata/
上一篇:《撮合引擎实现:撮合算法、价格优先时间优先、状态机、低延迟工程》
下一篇:《证券登记结算:中证登、DTCC、Euroclear、T+1、DvP》
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【金融科技工程】交易所核心系统架构:撮合、行情、做市、风控、清算
从订单网关到撮合引擎、从 L1/L2/L3 行情到清算与结算,系统梳理证券、期货、加密货币交易所的五大核心子系统;给出低延迟工程技术栈(Disruptor、Kernel Bypass、FPGA)、订单生命周期状态机、主流交易所(NYSE Pillar、Nasdaq INET、上交所新一代、CME Globex、Binance、dYdX v4)对比、以及 Flash Crash 与 Knight Capital 的工程教训。
【金融科技工程】金融科技工程全景:从支付到交易所的系统分类与读图
金融科技(FinTech)不是普通后端加一张账户表。钱的原子性、监管的硬边界、一个小数点的代价,把这个领域推进到工程强度最高的那一档。本文是【金融科技工程】25 篇的总目录与阅读地图:先交代为什么它比一般业务系统更难,再给出对账体、支付体、交易体、风控合规体四维分类,把后续 24 篇挂到骨架上,最后给出一份绿地项目的落地顺序建议。
【金融科技工程】钱的建模:金额精度、币种、会计单位、多语言金额
在代码里正确地表示"一笔钱"远比看起来难。本文系统梳理金额的数值建模(浮点、定点、Decimal、最小单位)、币种标准(ISO 4217)、本地化显示、汇率换算与数据库存储,并给出 Go、Python、Java、Rust 的工程化示例。
【金融科技工程】复式记账工程化:科目、分录、余额、对账
把 500 年历史的复式记账翻译成工程师可以落地的数据模型、SQL 表结构与余额计算策略,覆盖充值、下单、退款、分润、红包、多币种与冲销的真实场景,并对比 TigerBeetle、beancount、Ledger CLI、Square LedgerDB、Stripe Ledger 等开源与工业实现。