3、数据模型设计:统一行情数据模型(Tick、K线、OrderBook)、时间戳规范化处理、数据版本控制策略

好,咱们进入第三章。说实话,数据模型设计这块,是整个多源聚合方案的基石。你想想看,如果地基没打好,上层建筑再花哨也是白搭。我在早期做行情系统时,就吃过这个亏——不同交易所的数据格式五花八门,硬生生把聚合逻辑写成了意大利面条,维护起来简直想哭。

所以这一章,咱们重点聊聊怎么设计一套统一、健壮、可扩展的行情数据模型。我会把我在实战中踩过的坑、总结的经验,都揉碎了讲给你听。

3.1 统一行情数据模型

说白了,我们要做的是把不同交易所的“方言”翻译成“普通话”。常见的行情数据无非三种:Tick、K线、OrderBook。咱们一个一个来。

3.1.1 Tick 数据模型

Tick 是最细粒度的数据,记录每一笔成交。不同交易所的 Tick 字段差异很大,有的带买方ID,有的带卖方ID,有的甚至没有成交方向。我的做法是:抽象出最小公共集,再通过扩展字段兜底

// 统一 Tick 模型(Go 语言示例)
type UnifiedTick struct {
    Symbol    string    `json:"symbol"`     // 交易对,如 BTCUSDT
    Exchange  string    `json:"exchange"`   // 交易所标识,如 binance
    Price     float64   `json:"price"`      // 成交价
    Volume    float64   `json:"volume"`     // 成交量
    Side      int32     `json:"side"`       // 成交方向:0-未知,1-买,2-卖
    Timestamp int64     `json:"timestamp"`  // 统一时间戳(纳秒)
    TradeID   string    `json:"trade_id"`   // 全局唯一成交ID
    Extra     map[string]interface{} `json:"extra"` // 扩展字段,存放交易所特有信息
}

嗯,这里要注意 Side 字段。有些交易所的 Tick 不提供方向,我就默认填 0。另外 TradeID 我建议用字符串,因为不同交易所的 ID 格式差异太大,有的是纯数字,有的是带前缀的字符串。

我的小技巧: 在 Extra 字段里,我会用 exchange_xxx 作为 key 前缀,比如 exchange_binance_taker_id。这样既保留了原始信息,又不会污染公共字段。

3.1.2 K线数据模型

K线相对简单,但坑也不少。最大的坑是时间对齐。不同交易所的 K线开盘时间可能差几毫秒,聚合时如果不处理,就会出现“锯齿”。

// 统一 K线模型
type UnifiedKline struct {
    Symbol    string  `json:"symbol"`
    Exchange  string  `json:"exchange"`
    Interval  string  `json:"interval"`   // 周期,如 1m, 5m, 1h
    Open      float64 `json:"open"`
    High      float64 `json:"high"`
    Low       float64 `json:"low"`
    Close     float64 `json:"close"`
    Volume    float64 `json:"volume"`
    Turnover  float64 `json:"turnover"`   // 成交额
    OpenTime  int64   `json:"open_time"`  // 统一开盘时间(纳秒)
    CloseTime int64   `json:"close_time"` // 统一收盘时间(纳秒)
    Count     int64   `json:"count"`      // 成交笔数
}

我建议把 OpenTime 作为主键的一部分。为什么?因为同一个交易对、同一个周期、同一个开盘时间,理论上只有一条 K线。如果发现重复,说明数据源有问题,需要告警。

注意: 有些交易所的 K线是“左闭右开”的,有些是“左闭右闭”。聚合时一定要统一成一种策略。我个人习惯用“左闭右开”,即 [OpenTime, CloseTime),这样更容易做时间分片。

3.1.3 OrderBook 数据模型

OrderBook 是最复杂的。因为它有快照增量两种模式。快照是全量数据,增量是变化数据。设计模型时,必须同时支持这两种模式。

// 统一 OrderBook 模型
type UnifiedOrderBook struct {
    Symbol    string      `json:"symbol"`
    Exchange  string      `json:"exchange"`
    Bids      []PriceLevel `json:"bids"`      // 买盘,按价格降序
    Asks      []PriceLevel `json:"asks"`      // 卖盘,按价格升序
    Timestamp int64       `json:"timestamp"` // 快照时间戳
    Sequence  int64       `json:"sequence"`  // 序列号,用于增量更新
    IsSnapshot bool       `json:"is_snapshot"` // 是否为快照
}

type PriceLevel struct {
    Price    float64 `json:"price"`
    Volume   float64 `json:"volume"`
    OrderNum int32   `json:"order_num"` // 订单数量(部分交易所提供)
}

这里有个关键点:Sequence 字段。它是增量更新的“锚点”。我曾经遇到过一个交易所,它的序列号会回滚,导致增量数据错乱。后来我加了一个校验逻辑:如果新序列号小于旧序列号,就丢弃增量,重新请求快照。

3.2 时间戳规范化处理

时间戳是行情数据的灵魂。但不同交易所的时间戳格式五花八门:秒级、毫秒级、微秒级,甚至还有字符串格式。如果不统一处理,聚合时就会乱套。

我的策略很简单:统一使用纳秒级时间戳。为什么是纳秒?因为高频交易场景下,微秒级的精度都不够用。而且纳秒可以向下兼容所有其他精度。

// 时间戳规范化函数
func NormalizeTimestamp(raw interface{}) (int64, error) {
    switch v := raw.(type) {
    case int64:
        // 假设是毫秒级,乘以 1e6 转为纳秒
        return v * 1e6, nil
    case float64:
        // 可能是秒级,乘以 1e9
        return int64(v * 1e9), nil
    case string:
        // 解析 ISO 8601 格式
        t, err := time.Parse(time.RFC3339Nano, v)
        if err != nil {
            return 0, err
        }
        return t.UnixNano(), nil
    default:
        return 0, fmt.Errorf("unsupported timestamp type: %T", raw)
    }
}
核心原则: 所有数据在进入系统后,第一时间完成时间戳规范化。不要在后续处理中再做转换,否则容易出错。

另外,我建议在数据库里也存储纳秒级时间戳。虽然会多占一点存储空间,但查询和聚合时不需要再做转换,性能更好。我在项目中实测过,纳秒级时间戳做范围查询,比字符串格式快 3-5 倍。

3.3 数据版本控制策略

行情数据是不断变化的。同一个交易对、同一个时间点,可能因为数据源不同、处理逻辑不同,产生多个版本的数据。如果没有版本控制,数据回溯时就会一团糟。

我的做法是引入数据版本号。每个数据记录都带一个版本号,版本号递增。当数据发生变更时,不是覆盖旧数据,而是写入新版本。

版本号 数据来源 时间戳 状态
1 交易所A 2024-01-01 10:00:00.000 已废弃
2 交易所B 2024-01-01 10:00:00.001 已废弃
3 聚合引擎 2024-01-01 10:00:00.002 当前有效

你看,同一个时间点的数据,有三个版本。版本1和版本2来自不同交易所,版本3是聚合后的结果。如果后续发现聚合逻辑有 bug,我们可以回退到版本2,重新聚合。

避坑指南: 我曾经因为没做版本控制,导致一次数据回溯花了整整两天。后来我设计了一个简单的版本管理方案:每个数据表都加一个 version 字段,再建一个 data_versions 表记录版本元信息。这样回溯时,只需要切换版本号,不需要动数据本身。

版本控制还有一个好处:支持数据回放。在策略回测时,我们可以指定使用哪个版本的数据。比如,我想测试某个策略在“未聚合”的原始数据上的表现,就可以选择版本1或版本2的数据。

3.4 知识体系总览

说了这么多,咱们用一张图来总结一下整个数据模型设计的核心逻辑。这张图是我自己画的,涵盖了 Tick、K线、OrderBook 的模型设计,以及时间戳规范化和版本控制的流程。

统一行情数据模型核心逻辑 多源数据输入 时间戳规范化 统一数据模型 Tick 模型 K线模型 OrderBook 模型 数据版本控制(版本号 + 版本元信息表) 统一存储(支持多版本回溯) 图:统一行情数据模型核心逻辑流程图

这张图清晰地展示了数据从多源输入,到时间戳规范化,再到统一模型,最后经过版本控制存入存储的完整流程。你可以把它当作设计时的参考框架。

总结一下: 数据模型设计不是一蹴而就的。我建议你先从 Tick 和 K线入手,把基础打牢,再逐步加入 OrderBook 和版本控制。每一步都要考虑扩展性,因为行情数据的变化速度,远比你想象的要快。

好了,这一章的内容就到这里。数据模型是系统的骨架,骨架正了,后面的路就好走了。下一章咱们聊聊数据采集层的设计,那又是另一番天地了。