数据结构设计:快照、增量与合并统一结构
做量化交易系统,有个坎儿你迟早得迈过去——就是行情数据的合并。我刚开始接触这个领域时,天真地以为直接把数据拼起来就行。结果呢?回测曲线漂亮得像假的一样,实盘一跑就露馅。后来才明白,问题出在数据结构上。
今天咱们就聊聊三种核心数据结构:快照(Snapshot)、增量(Delta),以及合并后的统一结构。说白了,这是整个行情处理系统的地基。
快照数据结构(Snapshot)
快照是什么?就是某一时刻市场的完整状态。你想想看,就像给市场拍了一张高清照片,所有信息都在里面。
我在项目中遇到过最典型的场景:每天开盘前,交易所会推送一次全量快照。这时候你拿到的是当前所有可交易品种的完整行情。
核心字段:
- timestamp:快照生成时间,精确到微秒
- symbol:合约代码,比如"BTC-USDT"
- bid/ask:买卖十档价格和数量
- last_price:最新成交价
- volume:累计成交量
- open_interest:持仓量(期货特有)
// 快照结构示例(Go语言)
type Snapshot struct {
Timestamp int64 // 微秒时间戳
Symbol string // 合约代码
Bids [][2]float64 // [价格, 数量] 按价格降序
Asks [][2]float64 // [价格, 数量] 按价格升序
LastPrice float64
Volume float64
OpenInterest float64
}
嗯,这里要注意:快照虽然完整,但体积大、推送频率低。一般交易所每100ms才推一次。如果你只靠快照做高频交易,那黄花菜都凉了。
增量数据结构(Delta)
增量数据,说白了就是「变化的部分」。交易所不会每次都把完整数据推给你,那样带宽扛不住。它们只告诉你:「刚才那笔成交后,买一价变了,卖二量少了...」
我曾经踩过一个坑:以为增量数据只包含价格变化。结果发现有些交易所的增量还包含「删除档位」的操作。比如某个价格档位被吃光了,增量里会明确告诉你「删掉这个价格」。
避坑指南:不同交易所的增量格式差异很大。有的用数组下标定位,有的用价格作为key。我建议你统一封装一层,别让业务代码直接对接交易所原始协议。
// 增量结构示例
type Delta struct {
Timestamp int64
Symbol string
BidChanges []PriceLevelChange // 买盘变化
AskChanges []PriceLevelChange // 卖盘变化
Trade *Trade // 可选:最新成交
}
type PriceLevelChange struct {
Price float64
Quantity float64 // 0表示删除该档位
Side string // "bid" 或 "ask"
}
增量数据的特点是:体积小、频率高。有些交易所每笔成交都会推送增量。你想想看,如果每秒处理几千笔增量,数据结构设计不好,CPU直接飙到100%。
合并后的统一数据结构
现在问题来了:快照和增量怎么合到一起?
我个人习惯的做法是:以快照为基础,用增量去「更新」它。就像搭积木一样,先搭好骨架(快照),然后不断往上添砖加瓦(增量)。
我的经验:合并时一定要做「时间戳校验」。我曾经遇到过增量比快照还早到达的情况——网络延迟导致的。如果直接合并,数据就乱套了。所以我会在合并前加一个判断:增量时间戳必须大于快照时间戳。
// 合并后的统一结构
type UnifiedMarketData struct {
Timestamp int64
Symbol string
Bids [][2]float64 // 始终是最新的十档
Asks [][2]float64
LastPrice float64
Volume float64
OpenInterest float64
UpdateType string // "snapshot" 或 "delta"
SequenceNum int64 // 序列号,用于去重和排序
}
合并逻辑其实不复杂,核心就三步:
- 初始化:收到第一个快照,建立完整订单簿
- 更新:后续增量到达,逐条应用到订单簿上
- 校验:定期用新快照做全量校验,修正累积误差
第三步特别重要。为什么?因为增量数据在传输过程中可能丢包。虽然TCP保证了不丢,但应用层可能因为处理不过来而丢弃。我见过一个团队,连续跑了三天没做快照校验,结果订单簿深度差了0.5个BTC——这在高频交易里可是致命的。
三种数据结构的对比
| 特性 | 快照 | 增量 | 统一结构 |
|---|---|---|---|
| 数据完整性 | 完整 | 仅变化部分 | 完整 |
| 推送频率 | 低(100ms-1s) | 高(每笔成交) | 按需 |
| 内存占用 | 大 | 小 | 中等 |
| 处理复杂度 | 低 | 中 | 高 |
| 适用场景 | 初始化、校验 | 实时更新 | 策略计算 |
核心逻辑流程图
下面这张图展示了快照和增量数据合并的完整流程。我画的时候特意把「校验」环节突出了一下——这是最容易出问题的地方。
从流程图你能看到,整个合并过程其实是个「状态机」。快照负责重置状态,增量负责更新状态。我建议你在代码里用状态模式来实现,这样扩展性会好很多。
核心要点总结:
- 快照是「全量」,增量是「变化」,统一结构是「合并结果」
- 合并时一定要做时间戳校验和序列号去重
- 定期用快照做全量校验,防止误差累积
- 不同交易所的增量格式差异大,建议统一封装
好了,数据结构这块就聊到这儿。记住一句话:数据结构设计得不好,后面所有优化都是白搭。我见过太多团队在合并逻辑上翻车了——不是丢数据就是数据错乱。希望今天的内容能帮你少走些弯路。