4、数据清洗与标准化:不同交易所的数据格式差异,统一成标准OrderBook和Trade结构

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

我个人觉得,不是策略亏钱,而是数据打架。

你连上币安、OKX、Bybit三家交易所,拿到的OrderBook结构完全不一样。有的用bids,有的用b,有的价格和数量顺序还反着来。这要是直接喂给策略,不出错才怪。

所以这一章,咱们就聊聊怎么把这些乱七八糟的数据,统一成一套标准结构。

4.1 为什么需要数据清洗?

说白了,交易所不是为你家策略设计的。

每家都有自己的历史包袱。有的早期为了省带宽,字段名用单字母;有的为了兼容老系统,数据结构改了好几版。你想想看,一个聚合引擎要同时对接10家交易所,每家格式都不一样,代码里全是if-else,维护起来简直要命。

我在项目中遇到过最离谱的一次——某交易所的OrderBook里,price字段有时候是字符串,有时候是数字。同一个字段,类型不固定。嗯,这种坑踩过一次就记住了。

核心原则: 数据清洗的目标不是“改对”,而是“统一”。让所有数据进入下游前,都长一个样。

4.2 常见的数据格式差异

我整理了一下,不同交易所的数据差异主要集中在以下几个方面:

差异类型 举例 影响
字段命名 币安用bids,某小交易所用buy_orders 代码无法通用
数据类型 价格有时是字符串"123.45",有时是浮点123.45 计算时报错
数据顺序 有的bids按价格降序,有的升序 撮合逻辑出错
时间格式 Unix毫秒、ISO8601、字符串"2024-01-01T00:00:00Z" 时间对齐困难
精度位数 有的价格保留8位小数,有的保留2位 精度丢失或计算偏差

你看,光一个OrderBook,就有这么多坑。更别提Trade数据了,有的带trade_id,有的带tid,有的干脆没有唯一ID。

4.3 标准OrderBook结构

我个人习惯,先把目标结构定死。不管上游怎么变,下游只认这一套。

// 标准OrderBook结构
{
  "exchange": "binance",      // 交易所名称
  "symbol": "BTCUSDT",        // 交易对
  "timestamp": 1704067200000, // 更新时间,Unix毫秒
  "bids": [                   // 买单,按价格降序
    ["45000.12", "1.2345"],   // [价格, 数量],字符串类型
    ["45000.11", "0.5678"]
  ],
  "asks": [                   // 卖单,按价格升序
    ["45000.13", "2.3456"],
    ["45000.14", "1.7890"]
  ]
}

为什么价格和数量用字符串?

因为浮点数在计算机里是不精确的。0.1 + 0.2 不等于 0.3,这个大家都知道。用字符串传递,到计算层再转Decimal,能避免精度问题。我曾经因为这个吃过亏,算出来的价差差了0.0001,导致套利策略频繁误触发。

小技巧: 统一用字符串存价格和数量,计算时用Python的Decimal或C++的boost::multiprecision。别用float/double直接算。

4.4 标准Trade结构

Trade数据比OrderBook简单一些,但同样需要统一。

// 标准Trade结构
{
  "exchange": "okx",
  "symbol": "ETHUSDT",
  "trade_id": "123456789",    // 唯一成交ID
  "timestamp": 1704067200123, // 成交时间,Unix毫秒
  "price": "2200.50",         // 成交价格
  "quantity": "0.1234",       // 成交数量
  "side": "buy",              // 买卖方向:buy/sell
  "is_maker": true            // 是否maker成交
}

这里有个细节——trade_id。有些交易所的trade_id是数字,有些是字符串,还有些是UUID。统一转成字符串最省事。另外,is_maker字段不是所有交易所都提供,没有的话就默认false。

4.5 清洗流程实战

光说理论不行,咱们看看实际代码怎么写。下面是一个Python示例,把币安的OrderBook转成标准格式:

def clean_binance_orderbook(raw, symbol):
    """
    清洗币安OrderBook数据
    raw: 从WebSocket收到的原始数据
    symbol: 交易对
    """
    # 检查数据完整性
    if 'bids' not in raw or 'asks' not in raw:
        raise ValueError("缺少bids或asks字段")
    
    # 统一时间戳
    timestamp = raw.get('E', raw.get('lastUpdateId', 0))
    # 币安的时间戳是毫秒,直接使用
    
    # 构建标准结构
    standard = {
        "exchange": "binance",
        "symbol": symbol,
        "timestamp": timestamp,
        "bids": [],
        "asks": []
    }
    
    # 清洗bids:确保价格降序,数量转字符串
    for price, qty in raw['bids']:
        # 有些交易所返回的是数字,转成字符串
        price_str = str(price) if not isinstance(price, str) else price
        qty_str = str(qty) if not isinstance(qty, str) else qty
        standard['bids'].append([price_str, qty_str])
    
    # 清洗asks:确保价格升序
    for price, qty in raw['asks']:
        price_str = str(price) if not isinstance(price, str) else price
        qty_str = str(qty) if not isinstance(qty, str) else qty
        standard['asks'].append([price_str, qty_str])
    
    # 排序校验
    standard['bids'].sort(key=lambda x: float(x[0]), reverse=True)
    standard['asks'].sort(key=lambda x: float(x[0]))
    
    return standard
注意: 有些交易所的OrderBook是增量更新,不是全量推送。这种情况下,你需要在本地维护一个OrderBook快照,然后应用增量。清洗逻辑会更复杂,需要处理sequence number的连续性。

4.6 数据校验与异常处理

数据清洗完了,不代表就万事大吉。你还得校验一下。

我一般会做这几项检查:

  • 空值检查: bids或asks为空?那这数据大概率有问题,直接丢弃。
  • 价格合理性: 价格突然变成0或者负数?肯定是脏数据。
  • 时间单调性: 新数据的时间戳比上一条还早?说明数据乱序了,需要重排或丢弃。
  • 买卖价差: 如果bids的最高价大于asks的最低价,说明买卖交叉了。这在正常市场里不应该出现,可能是数据延迟或错误。

我曾经在生产环境遇到过一个问题——某交易所的WebSocket偶尔会推送重复数据。同样的trade_id出现两次,导致策略重复计算成交。后来我在清洗层加了个去重逻辑,用LRU缓存最近1000个trade_id,重复的直接跳过。

4.7 整体流程一览

说了这么多,咱们用一张图把整个流程串起来:

数据清洗与标准化流程 交易所A (币安格式) 交易所B (OKX格式) 交易所C (Bybit格式) 数据清洗层 字段映射 类型转换 排序校验 异常过滤 标准OrderBook / Trade 结构 统一字段名 | 统一类型 | 统一排序 | 统一时间格式 下游:K线生成 / 策略引擎 / 存储

你看,整个流程其实不复杂。上游不管来什么格式,经过清洗层统一处理后,下游只认标准结构。这样每个模块各司其职,出了问题也好排查。

4.8 避坑指南

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

  • 别信文档: 交易所的API文档经常和实际返回不一致。我建议你每次对接新交易所,先打印原始数据看10分钟,再写清洗代码。
  • 注意空数组: 有些交易所行情清淡时,bids或asks可能为空数组。你的清洗逻辑要能处理这种情况,别直接抛异常。
  • 时间戳陷阱: 有的交易所返回秒级时间戳,有的是毫秒级。差1000倍,策略直接废掉。统一转成毫秒,加个判断:如果时间戳小于1e12,说明是秒级,乘以1000。
  • 增量更新要小心: 如果做OrderBook增量更新,一定要校验sequence number的连续性。丢了一条增量,整个OrderBook就废了。我建议你加个监控,如果连续丢3条增量,主动重新订阅全量快照。
我的习惯: 每个交易所的清洗函数单独写一个文件,命名如cleaner_binance.pycleaner_okx.py。这样新增交易所时,不影响已有逻辑。代码结构清晰,维护起来也省心。

数据清洗这事儿,看起来琐碎,但做不好后面全是坑。你想想看,如果连数据都是错的,再牛逼的策略也是白搭。所以,花时间把清洗层做扎实,绝对是值得的。


公众号:蓝海资料掘金营,微信deep3321