4、订单簿重建:从增量数据重建完整订单簿、数据对齐、时间戳处理
好,咱们进入第四章。这一章我打算聊聊订单簿重建。
说实话,这个环节是很多量化新手容易翻车的地方。你拿到的交易所原始数据,往往是增量数据——也就是每次只告诉你「谁变了」,而不是「现在长什么样」。你要做的,就是把这些碎片拼回一张完整的订单簿快照。
我个人习惯把订单簿重建比作拼图。你手头有几千块碎片,每块碎片上写着「第3档,买一,价格100.5,数量200」。你的任务不是记住每块碎片,而是拼出整张图。嗯,这里要注意:拼图过程中,任何一块放错位置,整张图就歪了。
4.1 增量数据 vs 快照数据
先搞清楚两个概念。
- 快照数据:某一时刻订单簿的完整状态。比如「此刻买一到买十、卖一到卖十,所有价格和数量」。交易所通常每隔几秒或几分钟推送一次快照。
- 增量数据:订单簿发生变化的部分。比如「有人撤单了,买一数量减少50」或者「新挂了一笔卖单,价格101.2,数量300」。
为什么不能只用快照?因为快照频率太低。你想想看,高频交易场景下,几秒钟的延迟足以让策略失效。所以我们必须用增量数据来「实时」维护订单簿。
核心原则:增量数据是「变化」,快照数据是「基准」。用快照做初始化,用增量做持续更新。
4.2 重建流程:从零到完整订单簿
我在项目中遇到过最典型的情况:交易所只提供增量数据,没有定期快照。这意味着你必须从第一条增量开始,逐条重建。一旦中间漏掉一条,后面全错。
重建流程大致分三步:
- 初始化:拿到第一张快照(如果有的话),或者从第一条增量开始构建空订单簿。
- 逐条应用增量:每条增量包含操作类型(新增、修改、删除)、价格、数量、方向(买/卖)。
- 定期校验:用快照数据做交叉验证,确保重建结果正确。
下面是一个简化的重建逻辑伪代码:
// 伪代码:订单簿重建核心逻辑
class OrderBook {
bids: Map<price, quantity> // 买单
asks: Map<price, quantity> // 卖单
applySnapshot(snapshot) {
this.bids.clear()
this.asks.clear()
for each level in snapshot.bids {
this.bids.set(level.price, level.quantity)
}
for each level in snapshot.asks {
this.asks.set(level.price, level.quantity)
}
}
applyIncrement(inc) {
if inc.action == 'new' {
// 新增档位
if inc.side == 'buy' {
this.bids.set(inc.price, inc.quantity)
} else {
this.asks.set(inc.price, inc.quantity)
}
} else if inc.action == 'modify' {
// 修改数量
if inc.side == 'buy' {
this.bids.set(inc.price, inc.quantity)
} else {
this.asks.set(inc.price, inc.quantity)
}
} else if inc.action == 'delete' {
// 删除档位
if inc.side == 'buy' {
this.bids.delete(inc.price)
} else {
this.asks.delete(inc.price)
}
}
}
}
你看,逻辑本身不复杂。但实际坑很多。我曾经因为把「修改」和「新增」搞混,导致订单簿里出现重复价格档位,回测结果直接漂移了5%。
避坑指南:有些交易所的增量数据中,「修改」操作可能不包含完整数量,而是只包含变化量。比如「买一数量减少50」,你需要从当前数量中减去50,而不是直接设为50。这个细节我踩过坑,后来加了日志才定位到。
4.3 数据对齐:时间戳是命根子
数据对齐,说白了就是让不同来源的数据在时间上「对上号」。你可能有快照数据、增量数据、成交数据,它们的时间戳必须一致。
我建议的做法:
- 统一时间基准:全部转成UTC毫秒时间戳。别用本地时间,别用字符串。
- 对齐快照和增量:快照的时间戳应该对应增量序列中的某个时刻。比如快照是T=1000ms时的状态,那么T=1000ms之后的增量应该基于这个快照来应用。
- 处理延迟:网络传输有延迟,交易所推送时间和你收到时间不一样。我个人习惯用交易所的时间戳,而不是本地接收时间。
小技巧:如果快照和增量时间戳对不上,可以尝试用「最近快照+后续增量」的方式重建。比如每5分钟拿一次快照,然后在这5分钟内用增量更新。这样即使快照有延迟,误差也在可控范围内。
4.4 时间戳处理的三个坑
时间戳处理,我吃过不少亏。列三个最常见的:
- 精度不一致:有的交易所用秒,有的用毫秒,有的用微秒。你想想看,毫秒级和微秒级混在一起,排序直接乱掉。统一转成微秒或纳秒再处理。
- 时钟不同步:交易所服务器时间和你的服务器时间可能有偏差。我曾经发现某交易所的时间戳比实际慢了200毫秒,导致订单簿重建总是滞后。解决办法是用NTP同步,或者用交易所的行情时间戳做基准。
- 重复或丢失:网络不稳定时,可能收到重复的增量,或者漏掉某条。重复的还好,去重就行。丢失的就麻烦了——你只能等下一次快照来修复。所以定期拿快照做校验很重要。
4.5 重建后的校验方法
重建完订单簿,怎么知道对不对?
我的做法是:拿重建后的订单簿和同一时刻的快照做对比。如果买一价格、卖一价格、深度分布都一致,说明重建正确。如果不一致,就要排查是增量数据的问题,还是重建逻辑的问题。
下面是一个校验示例:
// 校验重建结果
function validateOrderBook(reconstructed, snapshot) {
// 比较买一
if (reconstructed.bids[0].price !== snapshot.bids[0].price) {
console.error('买一价格不匹配')
return false
}
// 比较卖一
if (reconstructed.asks[0].price !== snapshot.asks[0].price) {
console.error('卖一价格不匹配')
return false
}
// 比较前10档深度
for (let i = 0; i < 10; i++) {
if (reconstructed.bids[i].quantity !== snapshot.bids[i].quantity) {
console.warn(`第${i+1}档买单数量不匹配`)
return false
}
}
return true
}
嗯,这里要注意:校验不能太频繁,否则影响性能。我一般每1000条增量或每10秒校验一次。
4.6 本章知识体系
下面这张图展示了订单簿重建的核心逻辑和流程:
从这张图你能看到,整个流程是环环相扣的。增量数据进来,经过重建引擎生成订单簿,然后定期用快照做校验。时间戳处理和数据对齐贯穿始终,是保证正确性的基石。
好了,这一章就到这里。订单簿重建看似基础,但做扎实了,后面的策略开发才能站得住脚。记住:数据质量决定策略上限。