03 · 行情系统:交易软件的「眼睛」
行情的价值不在「收到」,而在「完整、有序、及时」。对一个对接项目来说,行情模块的坑几乎全部集中在三个问题上:数据从哪来、断线了怎么办、延迟有多高。
本篇讲行情源与行情级别、快照与增量、协议、时间与延迟、行情网关架构、断线重连与落库,最后给出延迟测量方法。
1. 行情源类型
| 行情源 | 层级 | 特点 | 典型用途 |
|---|---|---|---|
| 交易所原始行情 | 最高 | Level-2 逐笔委托/逐笔成交,延迟最低,但直连权限门槛高(国内通常不可得,加密交易所可直连) | 高频/做市策略 |
| 柜台行情 | 中间 | 国内期货主力路径(CTP 行情、恒生行情等),快照为主,延迟受柜台与专线影响 | 常规交易系统 |
| 第三方数据商 | 最低 | Wind、聚宽、天软、TickData、盘立方等,覆盖全、有历史库、有复权等加工数据 | 研究、回测、展示 |
工程原则:生产行情(实盘决策用的)必须来自柜台/交易所直连行情,不能来自第三方数据商——第三方数据的延迟以秒级计,且无法保证逐笔完整性。第三方数据只用于历史回测与展示。
⚠️ 生产行情必须来自柜台或交易所直连
生产行情(实盘决策用的)必须来自柜台/交易所直连行情,不能来自第三方数据商——第三方数据的延迟以秒级计,且无法保证逐笔完整性。行情系统最危险的事故不是「行情慢了」,而是「看起来正常其实数据是错的」——断线重连失败却没告警,策略按过期数据下了单。
2. 行情级别:Level-1 与 Level-2
| 级别 | 内容 | 国内典型 | 加密典型 |
|---|---|---|---|
| Level-1(快照) | 最新价、成交量、涨跌幅、若干档盘口(如 1 档/5 档)、买卖量 | 期货快照(如 CTP 深度行情)、A 股 5 档快照(3 秒刷新,以交易所规则为准) | 不需要「L1」概念,普通 depth 流 |
| Level-2(逐笔) | 逐笔委托(order by order)、逐笔成交(trade by trade)、完整盘口(10-20 档甚至全深度) | 上交所/深交所 Level-2 需授权,期货逐笔需单独权限 | 币安 depth(增量)、OKX 等全深度快照 |
- Level-1 快照是「打包的结果」:价格、量、盘口在某一个采样时刻的状态,中间发生了什么不可见。
- Level-2 是「过程的记录」:每一笔委托、每一笔成交都有独立事件,可以精确重建订单簿。
- 对回测的意义完全不同:用 L1 快照回测只能模拟到秒级/快照级精度,用 L2 逐笔才能精确模拟「如果我挂单,能不能成交、在哪**一档**成交」。
3. 快照行情与增量行情
3.1 快照(Snapshot)
- 柜台/交易所周期性推送的行情整体状态,如 CTP 的深度行情(
CThostFtdcDepthMarketData),加密交易所的 RESTdepth全量快照。 - 特点:自带全量状态,重启后订阅即可恢复,但无法反映两次快照之间的事件。
- 快照频率:国内期货快照约 500ms 级、A 股 L1 约 3 秒(以交易所规则为准)、加密 depth 快照间隔由交易所定义。
3.2 增量(Incremental / 逐笔)
- 每一个变化单独成事件:一笔成交(trade)、一笔委托挂入或撤出(bookTicker / level2 update)。
- 特点:事件流稠密、信息量大,但依赖连续性——中间丢了一笔,后续状态就错了,必须靠快照补齐。
- 典型实现:币安 WebSocket 的
depth增量频道(u字段为更新序号,可检测缺口)、OKX 的books频道、以及国内逐笔接口。
3.3 订阅模型与回调处理
订阅请求(按合约/按 symbol 列表)
│
▼
行情连接(CTP MdApi / WS 通道)
│
▼
回调分发(on_tick / on_snapshot / on_incremental)
│
▼
业务消费(K 线合成 / 盘口重建 / 落库 / 推给前端)- 回调里只做最轻量的事(写队列、更新内存),合成 K 线、落库等重活移到消费者线程,否则行情脉冲会把回调线程压垮。
- 订阅是「请求-应答」式的:订阅后要校验是否真的订阅成功(部分接口有订阅确认回报),失败要重试并告警。
4. 行情协议
4.1 国内柜台行情 API
- CTP 行情:
CThostFtdcMdApi,登录后SubscribeMarketData订阅合约;回调OnRtnDepthMarketData推送快照。GBK 编码、DLL 形态,线程模型与交易侧相同(详见 02-交易所与柜台.md)。 - 其他柜台(恒生、易盛)形态类似,字段与行为以官方文档为准。
4.2 加密 WebSocket 订阅频道
| 交易所 | 频道 | 内容 |
|---|---|---|
| 币安 | kline_1m 等 | K 线(可拉历史) |
| 币安 | depth / bookTicker | 盘口增量/最优买卖价,u 为更新序号 |
| 币安 | aggTrade | 聚合成交 |
| OKX | books / trades / candle1m | 盘口/成交/K 线 |
| Bybit | orderbook.* / trade.* / kline.* | 盘口/成交/K 线 |
- 频道名称、参数、返回字段各家用例不同,以各交易所开发者文档为准。
- WS 消息没有严格顺序保证的场景下,需依赖交易所给出的序列号(如币安
u)做排序与缺口检测。
4.3 FIX 行情
- 海外市场(CME 等)常用 FIX 的行情变体(如 FIX/FAST、MDP),以消息类型(如 X 系列增量行情)推送,需要按字典解析。
- 特点:二进制压缩(FAST)、按模板解码,工程上与国内柜台的「结构体快照」是两种世界观。
5. 时间与延迟
5.1 时间戳类型
| 时间戳 | 含义 | 说明 |
|---|---|---|
| 交易所时间戳(event time) | 事件在交易所内部发生的时间 | 如成交时间、快照生成时间,各交易所格式不一(毫秒/微秒级,以官方文档为准) |
| 本地接收时间戳(receive time) | 我方进程收到消息的时间 | 必须在第一时间打点,通常用高性能时钟(C++ clock_gettime / Java 可参考系统时间) |
| 业务时间戳(trade date) | 归属哪个交易日 | 由本地按交易日规则换算,见 02-交易所与柜台.md 9.3 |
5.2 NTP 对时
- 所有服务器必须 NTP(或更精确的 PTP)对时,偏差控制在毫秒级(具体目标视策略而定)。
- 对时不准的后果:本地时间戳与交易所时间戳对比失真 → 延迟统计失真 → 套利/高频策略的决策基础全部作废。
5.3 为什么行情延迟重要
- 对高频/套利策略:延迟就是成本,几毫秒的差距决定订单能不能成交在预期价位。
- 对低频策略:延迟影响「决策所依据的数据是否还是最新的」——用 3 秒前的价格下**市价单,滑点**是必然的。
- 对展示系统:延迟导致图表与「真实市场」漂移,用户投诉与信任流失。
5.4 事件时间线
交易所撮合引擎 柜台/网关 我方行情网关 客户端
───────────────── ────────────────── ────────────────── ──────────
订单簿变化/成交
│ A(交易所内部延迟)
▼
行情生成 → 行情推送 B(传输延迟)
▼
快照/增量接收 ── C(网关处理延迟)──▶ 业务订阅者
│ D(渲染延迟)
▼
图表/面板刷新- A:交易所内部到行情输出的时间,不可控。
- B:网络传输,可控部分(专线、就近部署)。
- C:我方处理,必须优化(直接内存操作、避免锁与序列化)。
- D:前端渲染,与交易决策无关但要优化体验。
- 总延迟 = A + B + C(+ D),能优化的只有 B、C,而且 C 是白送的,先优化它。
💡 总延迟 = A + B + C,先优化 C
总延迟 = A + B + C(+ D),能优化的只有 B、C,而且 C 是白送的,先优化它。 A 是交易所内部延迟不可控,B 是网络传输可控部分,C 是我方处理必须优化——直接内存操作、避免锁与序列化,先把白送的 C 吃下。
6. 行情网关架构
行情网关是连接「柜台/交易所」与「内部服务」的唯一入口,承担五件事:
| 能力 | 说明 |
|---|---|
| 连接管理 | 一个行情源一个/多个连接,心跳保活、断线重连、连接状态上报 |
| 订阅管理 | 统一维护「合约 → 订阅者」映射;多前端订阅同一合约时只对上游订阅一次,内部扇出 |
| 多路复用(Fan-out) | 一份行情广播给多个消费者:K 线合成、风控、落库、前端推送 |
| 顺序保证 | 同一合约的事件必须按序分发(单连接单线程/按 symbol 分区),否则盘口重建会错 |
| 主备切换 | 主网关挂了自动切备机,切换时重新全量快照(见第 7 节) |
┌──────────────────────────────────────┐
│ 行情网关(独立进程) │
柜台行情 ──▶ │ 连接管理 → 订阅管理 → 序列化/扇出 │──▶ K 线服务
加密 WS ──▶ │ │──▶ 风控服务
逐笔源 ──▶ │ 内存状态(最新快照/序号) │──▶ 落库服务
│ │──▶ 前端推送
└──────────────────────────────────────┘架构红线:
- 网关独立进程/独立部署:行情网关的崩溃不能影响交易链路;交易链路崩溃不能影响行情。
- 网关内部不做业务:不合成指标、不判断行情——合成放消费者,网关只负责「接到、排序、广播」。
- 网关的消费下游要可降级:落库挂了不能阻塞前端推送(下游各自消费、各自重试)。
7. 断线重连与缺口补齐
7.1 重连策略
- 指数退避重连(如 1s → 2s → 4s → …,上限 60s),重连后先做健康检查(拉一次最新快照验证序号)。
- 重连期间行情缺失要对外暴露状态:前端显示「行情中断」,不要让用户在数据停留在旧值的情况下继续交易。
⚠️ 行情缺失必须对外暴露不要让用户按旧数据交易
重连期间行情缺失要对外暴露状态,不要让用户在数据停留在旧值的情况下继续交易。 断线后前端展示的往往是上一次快照的旧价,策略若按这份旧数据下单,等于是用「过去」去交易「现在」。
7.2 缺口补齐:全量快照 + 增量回放
增量行情断点后,本地状态不可信,补齐的标准流程:
检测到断线 / 序号跳变
│
▼
拉取全量快照(REST depth / 柜台全量行情)
│
▼
从快照的序号开始续接增量(WS 续订,确认起始序号)
│
▼
增量与快照校验(序号连续、盘口数量级合理)
│
▼
恢复对外推送,上报「补齐完成」给监控- 加密交易所增量频道一般支持按序号续订(币安 depth 的
u),断点后可只补差量。 - 国内柜台快照自带全量状态,断线恢复后直接重新订阅即可,但注意重连后的首个快照与本地旧状态的时序,应丢弃重连前缓存。
7.3 对账
- 周期性对账:用独立通道(如 REST 查询)拉一次最新价/成交量,与本地快照对比,偏差超阈值告警。
- 逐笔成交与快照成交量的关系:快照累计量必须等于逐笔之和(允许舍入误差),长期不符说明丢事件。
8. 行情落库
8.1 落库选型
| 存储 | 适合 | 说明 |
|---|---|---|
| 列式存储(ClickHouse 等) | 大规模 tick 分析、回测取数 | 压缩率高、聚合查询快,业界主流 |
| 时序库(TDengine / InfluxDB) | 指标监控、轻量行情 | 按时间索引,适合监控类 |
| 文件归档(Parquet / 自定义二进制) | 全量原始数据冷存 | 原始 tick 保留完整字段,供重建与补数据 |
8.2 落库设计要点
- 分层:热数据(近期,内存/SSD 库)与冷数据(历史,压缩归档)分离;回测一般走冷数据。
- 关键字段:symbol、交易所时间戳、本地接收时间戳、最新价/量、盘口、以及「事件序号」——序号是日后排查缺口的关键。
- 完整性校验:按日/按合约统计条数与序号连续性,缺口清单自动生成,支持按缺口补数据。
- 为什么要存历史 tick:历史逐笔 tick 是量化研究的金矿——回测精度、滑点建模、市场微观结构研究、新策略的信号挖掘全都依赖它。行情是实时的一次性资源,错过就没了,落地为安。
9. 行情延迟测量方法
- 本地时间戳对比:记录本地接收时间 − 交易所时间戳(需先 NTP 对时),这是最简单的端到端延迟估计。注意交易所时间戳的精度与语义(毫秒/微秒、生成于撮合前还是后),结论只做相对比较。
- 事件间隔法:监控同源两路行情(如双通道)同一事件的接收间隔,用于发现单路抖动。
- 序号法:交易所序号不跳、不重(或按规则递增),连续监测序号连续性即可发现丢包与乱序。
- 压力基线:上线前测「行情脉冲下的网关处理耗时 P99」,脉冲时 P99 恶化是常见事故,网关要压测过再上。
延迟测量要形成持续监控(SRE 告警项),而不是上线时测一次就完事——网络抖动、柜台变更都会让延迟悄悄变差。
风险提示
⚠️ 风险提示
行情系统最危险的事故不是「行情慢了」,而是**「看起来正常其实数据是错的」**:断线重连失败却没有告警,前端展示的是过期快照,策略却按这份过期数据下了单;增量缺口没检测,盘口重建后越偏越大,做市策略按错误的盘口报出双边报价。请务必把「行情新鲜度」和「序号连续性」作为与资金同等级别的监控指标,行情中断时必须同步冻结依赖行情的自动交易(或触发风控熔断,见 05-风控与资金管理.md)。