4、数据标准化:不同交易所数据结构差异与统一模型设计

做量化交易,最头疼的事之一是什么?

我个人觉得,就是对接不同交易所的数据格式。

你想想看,Binance 的深度数据长一个样,OKX 的又是另一个样,Coinbase 还给你玩点小花样。我早期做这套系统时,光是解析这些数据就写了七八个适配器,代码里全是 if-else,维护起来简直想骂人。

后来我学乖了。与其被交易所牵着鼻子走,不如自己搞一套统一模型。说白了,就是给所有数据「定规矩」——不管外面怎么变,进到我系统里,都得按我的格式来。

4.1 交易所数据结构的「三大乱象」

先看看实际中会遇到什么问题。我总结了一下,主要有三类差异:

  • 字段命名不统一:有的叫 "symbol",有的叫 "pair",还有的叫 "instrument_id"。你看着都头疼。
  • 数据类型不一致:价格有的用字符串传,有的用浮点数。时间戳有的用秒,有的用毫秒,甚至还有用微秒的。
  • 数据结构层级不同:深度数据有的是一维数组,有的是二维嵌套。K线数据有的带 volume,有的带 turnover,还有的给你塞一堆额外字段。

核心问题:如果不在入口层做标准化,后续的策略计算、风控逻辑、回测系统,全都要跟着适配不同格式。这简直是灾难。

4.2 统一模型设计:我的「三层抽象」方案

我在项目中实际落地的方案,是三层抽象模型。嗯,这里要注意,不是越复杂越好,够用就行。

第一层:原始数据层(Raw Data Layer)

这一层不做任何处理。WebSocket 收到什么,就原样存下来。为什么?因为有时候你需要回溯排查问题,原始数据是最可靠的证据。

第二层:标准化层(Normalized Layer)

这是核心。我定义了一套标准的数据结构,所有交易所的数据进来后,先经过一个适配器,转换成统一格式。比如:

// 我定义的统一深度数据模型
{
  "exchange": "binance",      // 来源交易所
  "symbol": "BTCUSDT",        // 统一交易对命名
  "timestamp": 1700000000000, // 统一毫秒时间戳
  "bids": [
    [price, quantity],
    [price, quantity]
  ],
  "asks": [
    [price, quantity],
    [price, quantity]
  ]
}

第三层:业务层(Business Layer)

策略和风控只跟标准化数据打交道。换交易所?改个配置就行,代码不用动。

我的经验:标准化层一定要做「数据校验」。我曾经遇到过某交易所突然把价格字段从字符串改成了科学计数法,要不是校验层及时发现,策略就全跑偏了。

4.3 关键字段的标准化规则

我整理了一份常用字段的标准化对照表,你可以直接参考:

业务含义 统一字段名 统一类型 常见坑点
交易对 symbol string 大小写、分隔符不同(BTCUSDT vs BTC-USDT)
时间戳 timestamp int64(毫秒) 有的交易所给秒,有的给毫秒,甚至微秒
价格 price float64 字符串转浮点要小心精度丢失
数量 quantity float64 注意是 base 数量还是 quote 数量
买卖方向 side string("buy"/"sell") 有的用 "bid"/"ask",有的用 0/1

避坑指南:我曾经因为时间戳单位没统一,导致回测数据全部错位。后来我强制要求所有时间戳在入口处就转成毫秒,并在字段名上加后缀 "_ms" 来提醒自己。

4.4 代码实现:一个简单的适配器示例

下面是我常用的适配器写法。以深度数据为例:

class DepthNormalizer:
    def normalize(self, raw_data, exchange):
        if exchange == "binance":
            return {
                "exchange": "binance",
                "symbol": raw_data["s"],
                "timestamp": raw_data["E"],  # 已经是毫秒
                "bids": [[float(p), float(q)] for p, q in raw_data["b"]],
                "asks": [[float(p), float(q)] for p, q in raw_data["a"]]
            }
        elif exchange == "okx":
            # OKX 的数据结构不同,需要特殊处理
            return {
                "exchange": "okx",
                "symbol": raw_data["arg"]["instId"].replace("-", ""),
                "timestamp": int(raw_data["ts"]),  # 注意转成毫秒
                "bids": [[float(p), float(q)] for p, q in raw_data["data"][0]["bids"]],
                "asks": [[float(p), float(q)] for p, q in raw_data["data"][0]["asks"]]
            }
        else:
            raise ValueError(f"不支持的交易所: {exchange}")

你看,代码其实不复杂。但关键是要把每个交易所的「特殊之处」都列出来,写成文档。我习惯在代码里加注释,标注每个字段的来源和转换逻辑。

4.5 统一模型的核心流程图

下面这张图,是我做数据标准化时的核心思路。你可以看到数据从交易所到策略的完整流转路径:

数据标准化三层模型 原始数据层(Raw Data Layer) Binance WebSocket | OKX WebSocket | Coinbase WebSocket 标准化层(Normalized Layer) 字段映射 | 类型转换 | 时间戳统一 | 数据校验 业务层(Business Layer) 策略计算 | 风控检查 | 订单管理 | 回测系统 注:标准化层是核心,所有交易所差异在此处消除

4.6 实际项目中的避坑经验

最后,分享几个我踩过的坑:

  • 不要信任交易所的文档:文档写的和实际返回的,经常不一样。我习惯先打印原始数据,对照着写适配器。
  • 注意数据延迟:不同交易所的时间戳精度不同。有的精确到微秒,有的只到秒。统一成毫秒后,要注意精度损失。
  • 预留扩展字段:统一模型里最好加一个 "extra" 字段,用来存放交易所特有的信息。比如某些交易所会返回 "trade_id",你可以在 extra 里保留它。
  • 写单元测试:每个适配器都要有对应的测试用例。我每次对接新交易所,都会先写测试,再写代码。

我的习惯:在标准化层加一个「数据快照」功能。每收到一条数据,就打印到日志里。这样出问题时,可以快速定位是哪个环节出了问题。

数据标准化这件事,看起来简单,做起来全是细节。但只要你把统一模型设计好了,后续的开发和维护会轻松很多。嗯,今天就聊到这里,希望对你有帮助。

专注资料整理