数据结构设计:快照、增量与合并统一结构

做量化交易系统,有个坎儿你迟早得迈过去——就是行情数据的合并。我刚开始接触这个领域时,天真地以为直接把数据拼起来就行。结果呢?回测曲线漂亮得像假的一样,实盘一跑就露馅。后来才明白,问题出在数据结构上。

今天咱们就聊聊三种核心数据结构:快照(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   // 序列号,用于去重和排序
}

合并逻辑其实不复杂,核心就三步:

  1. 初始化:收到第一个快照,建立完整订单簿
  2. 更新:后续增量到达,逐条应用到订单簿上
  3. 校验:定期用新快照做全量校验,修正累积误差

第三步特别重要。为什么?因为增量数据在传输过程中可能丢包。虽然TCP保证了不丢,但应用层可能因为处理不过来而丢弃。我见过一个团队,连续跑了三天没做快照校验,结果订单簿深度差了0.5个BTC——这在高频交易里可是致命的。

三种数据结构的对比

特性 快照 增量 统一结构
数据完整性 完整 仅变化部分 完整
推送频率 低(100ms-1s) 高(每笔成交) 按需
内存占用 中等
处理复杂度
适用场景 初始化、校验 实时更新 策略计算

核心逻辑流程图

下面这张图展示了快照和增量数据合并的完整流程。我画的时候特意把「校验」环节突出了一下——这是最容易出问题的地方。

行情快照与增量合并流程 交易所数据流 数据类型? 快照 初始化/重建订单簿 增量 时间戳校验 校验失败 丢弃/请求重传 校验成功 应用增量到订单簿 统一数据结构(UnifiedMarketData) 定期快照全量校验

从流程图你能看到,整个合并过程其实是个「状态机」。快照负责重置状态,增量负责更新状态。我建议你在代码里用状态模式来实现,这样扩展性会好很多。

核心要点总结:

  • 快照是「全量」,增量是「变化」,统一结构是「合并结果」
  • 合并时一定要做时间戳校验和序列号去重
  • 定期用快照做全量校验,防止误差累积
  • 不同交易所的增量格式差异大,建议统一封装

好了,数据结构这块就聊到这儿。记住一句话:数据结构设计得不好,后面所有优化都是白搭。我见过太多团队在合并逻辑上翻车了——不是丢数据就是数据错乱。希望今天的内容能帮你少走些弯路。


专注资料整理