本地订单簿同步
本系统提供两种深度频道配合使用,实现高效的本地订单簿维护:
depth@{symbol},{level}— 深度快照(200ms 节流)depth_update@{symbol}— 深度增量更新(100ms 节流)GET /fapi/v1/depth?symbol=X&with_id=true— REST 深度快照
方案一:仅使用快照频道(简单模式)
- 订阅
depth@{symbol},20 - 每次收到推送直接替换本地订单簿
- 无需增量同步逻辑
缺点: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;
}
}