跳到主要内容

本地订单簿同步

本系统提供两种深度频道配合使用,实现高效的本地订单簿维护:

  • depth@{symbol},{level} — 深度快照(200ms 节流)
  • depth_update@{symbol} — 深度增量更新(100ms 节流)
  • GET /fapi/v1/depth?symbol=X&with_id=true — REST 深度快照

方案一:仅使用快照频道(简单模式)

  1. 订阅 depth@{symbol},20
  2. 每次收到推送直接替换本地订单簿
  3. 无需增量同步逻辑

缺点:200ms 节流、档位有限,可能丢失中间变化。

方案二:快照 + 增量(精确模式)

初始化阶段

步骤 1: 订阅 depth_update@{symbol},开始缓存增量事件(暂不应用)
步骤 2: 获取快照 — 两种方式任选其一:
a) 订阅 depth@{symbol},{level},等待第一条推送
b) 调用 REST GET /fapi/v1/depth?symbol={symbol}&with_id=true
步骤 3: 记录快照的 lastUpdateId(u 或 id 字段)
步骤 4: 用快照数据初始化本地 bids / asks 有序列表

同步阶段

步骤 5: 从缓存事件中找到首个满足 U <= lastUpdateId+1 && u >= lastUpdateId+1 的事件
丢弃该事件之前的所有缓存
步骤 6: 对该事件及后续每条增量应用更新:
- 数量 > 0 → 插入或更新
- 数量 = 0 → 删除
- 更新本地 lastUpdateId = 事件的 u
步骤 7: 校验连续性:
- U > 本地 lastUpdateId+1 → 有缺口,重新初始化
- u <= 本地 lastUpdateId → 过期事件,丢弃
- 否则正常应用

异常处理

场景处理方式
WS 断线重连重新执行完整初始化(步骤 1-4)
增量事件有缺口(U > lastUpdateId + 1重新获取快照并初始化
长时间无增量推送定期用 REST 快照校验
快照获取失败重试,或降级为方案一

时序图

Client Server
| |
|-- subscribe depth_update@X --->| 步骤 1
|-- GET /fapi/v1/depth?with_id ->| 步骤 2
|<-------- snapshot (id=100) ----| 步骤 3: lastUpdateId=100
|<-- depth_update U=99,u=101 ----| 首个有效事件,应用
|<-- depth_update U=101,u=103 ---| 应用
|<-- depth_update U=200,u=201 ---| 缺口! 重新初始化

完整示例

class OrderBook {
constructor(symbol) {
this.symbol = symbol;
this.bids = new Map();
this.asks = new Map();
this.lastUpdateId = 0;
this.buffer = [];
this.ready = false;
}

async init(ws) {
this.ready = false;
this.buffer = [];

ws.subscribe(`depth_update@${this.symbol}`, (data) => {
if (!this.ready) { this.buffer.push(data); return; }
this.applyUpdate(data);
});

const resp = await fetch(`/fapi/v1/depth?symbol=${this.symbol}&with_id=true`);
const snapshot = (await resp.json()).data;

this.bids.clear(); this.asks.clear();
for (const [p, q] of snapshot.bids) this.bids.set(p, q);
for (const [p, q] of snapshot.asks) this.asks.set(p, q);
this.lastUpdateId = snapshot.id;

for (const event of this.buffer) {
if (event.u <= this.lastUpdateId) continue;
if (event.U > this.lastUpdateId + 1) return this.init(ws);
this.applyUpdate(event);
}
this.buffer = [];
this.ready = true;
}

applyUpdate(event) {
if (event.u <= this.lastUpdateId) return;
if (event.U > this.lastUpdateId + 1) {
this.ready = false;
return;
}
for (const [p, q] of event.b) {
if (q === '0') this.bids.delete(p); else this.bids.set(p, q);
}
for (const [p, q] of event.a) {
if (q === '0') this.asks.delete(p); else this.asks.set(p, q);
}
this.lastUpdateId = event.u;
}
}