多市场挑战:数据源异构性、时间同步问题、价格单位归一化

做过多市场交易的朋友都知道,把不同交易所的数据拉到一起,看着简单,实际上坑特别多。我刚开始做这个的时候,以为不就是几个API嘛,调一下数据就来了。结果呢?数据格式五花八门,时间戳对不上,价格单位还不一样。今天我就把这些坑一个一个说清楚。

数据源异构性:每个交易所都有自己的脾气

每个交易所的数据结构都不一样。有的用JSON,有的用Protobuf,有的甚至直接给你CSV。字段命名更是随心所欲——有的叫"bid",有的叫"buy",还有的叫"best_bid"。你想想看,如果代码里写死了字段名,换个交易所就得重写一遍,那得多累。

我在项目中遇到过最离谱的一次:某个交易所的订单簿深度数据里,价格和数量字段是反的。文档上写的是[price, volume],实际返回的是[volume, price]。要不是我做了校验,整个策略就全错了。

核心思路:抽象一个统一的数据模型,把各个交易所的原始数据映射成标准格式。这样上层逻辑就不用关心底层数据长什么样了。

具体怎么做呢?我一般会定义一个标准结构:

// 标准订单簿条目
struct OrderBookLevel {
    double price;      // 价格,统一用浮点数
    double volume;     // 数量,统一用浮点数
    int64_t timestamp; // 时间戳,统一用纳秒
    string exchange;   // 来源交易所
};

// 标准订单簿
struct OrderBook {
    string symbol;     // 交易对,比如 "BTC/USDT"
    vector<OrderBookLevel> bids;  // 买单
    vector<OrderBookLevel> asks;  // 卖单
    int64_t update_time;          // 更新时间
};

然后每个交易所写一个适配器,把原始数据转成这个标准格式。说白了就是一层转换层,脏活累活都在这里干完。

时间同步问题:你以为的时间不是你以为的时间

时间同步是个大问题。不同交易所的服务器时间不一样,有的用本地时间,有的用UTC,有的甚至用自己定义的时间戳。更麻烦的是,网络延迟会导致数据到达的时间不一致。

我记得有一次,两个交易所的订单簿数据差了整整2秒。2秒在量化交易里是什么概念?足够价格波动好几个档位了。如果直接合并,你会看到买单和卖单在时间上错位,合并出来的订单簿完全是乱的。

注意:千万不要直接拿本地时间给数据打标签。本地时间和交易所时间可能有偏差,而且这个偏差还会波动。

我的做法是这样的:

  1. 统一时间基准:所有数据都转成UTC纳秒时间戳。不管交易所给的是什么格式,先转成纳秒再说。
  2. 时间对齐:把不同交易所的数据按时间戳对齐。比如每100毫秒切一个时间窗口,窗口内的数据算作同一时刻的。
  3. 处理延迟:对于明显延迟的数据(比如时间戳比当前时间晚了1秒以上),直接丢弃或者标记为过期。

我曾经踩过一个坑:某个交易所的时间戳是毫秒级的,但精度只有10毫秒。也就是说,连续两个数据的时间戳可能是一样的。如果按时间戳排序,顺序就不对了。后来我加了一个序列号字段,用序列号来保证顺序。

价格单位归一化:别让小数点坑了你

价格单位归一化看起来简单,实际上最容易出问题。不同交易所对同一交易对的价格精度不一样。有的用8位小数,有的用2位小数,还有的用整数。

举个例子:比特币在交易所A的价格是50000.12345678,在交易所B是50000.12。如果你直接合并,精度高的数据会被精度低的数据拖累,或者反过来,精度低的数据看起来像是有误差。

我的习惯:统一用最高精度。所有价格都保留到小数点后8位,不足的补零。这样不会丢失信息,而且计算起来也方便。

还有一个更隐蔽的问题:价格单位本身可能不一样。有的交易所用"聪"(比特币的最小单位),有的用"BTC"。如果你不归一化,价格差几个数量级,合并出来的订单簿完全没法看。

我一般会定义一个价格转换函数:

// 价格归一化函数
double normalizePrice(double raw_price, string exchange, string symbol) {
    // 根据交易所和交易对,获取价格精度和单位
    int decimals = getDecimals(exchange, symbol);
    double multiplier = getMultiplier(exchange, symbol);
    
    // 先乘单位转换系数,再四舍五入到指定精度
    double normalized = raw_price * multiplier;
    double factor = pow(10, decimals);
    return round(normalized * factor) / factor;
}

嗯,这里要注意:四舍五入的时候要小心浮点数精度问题。我建议用整数运算,把价格转成最小单位(比如"分"或"聪")再计算,最后再转回来。

知识体系总览

下面这张图把这三个挑战的关系画清楚了。你看,数据源异构性是入口问题,时间同步是中间处理问题,价格归一化是输出问题。三个问题串起来,才能得到一份可用的合并订单簿。

多市场订单簿合并核心挑战 数据源异构性 不同格式 不同字段命名 不同数据结构 时间同步问题 时间基准不一致 网络延迟 时间戳精度差异 价格单位归一化 精度不一致 单位不同 浮点数精度问题 统一、可用的合并订单簿 三个挑战依次解决,才能得到高质量的合并数据

你看这张图,从左到右,数据源异构性解决的是"数据长什么样"的问题,时间同步解决的是"数据什么时候发生"的问题,价格归一化解决的是"数据值是多少"的问题。三个问题都解决了,合并出来的订单簿才能用。

总结一下:
  • 数据源异构性:用适配器模式,统一数据模型
  • 时间同步问题:统一时间基准,按时间窗口对齐
  • 价格单位归一化:统一精度和单位,用整数运算避免浮点数误差

这三个问题,说白了就是数据清洗的三个维度。做量化交易,数据质量决定了策略质量。数据没处理好,后面再牛的算法也是白搭。我见过太多人花大把时间调参数,结果问题出在数据上——时间戳错了,价格单位没归一化,合并出来的订单簿根本不能用。

嗯,今天就先聊到这儿。这些坑我都踩过,希望你能少走点弯路。


专注资料整理