4、数据清洗与对齐:去重、时间戳对齐、异常值处理、订单簿重建逻辑

数据清洗,说白了就是给原始数据「洗澡」。

我刚开始做订单簿回溯时,拿到交易所的原始数据直接往里灌,结果回测曲线惨不忍睹。后来才发现,数据里藏着各种「脏东西」——重复记录、时间错位、价格异常……嗯,从那以后,我养成了一个习惯:数据不洗三遍,绝不进策略

4.1 去重:别让同一笔订单算两次

交易所的API有时候会重复推送同一条数据。尤其是WebSocket断线重连后,补发的数据里经常混着已经处理过的记录。

我的做法很简单:

  • (交易对, 时间戳, 订单ID) 作为唯一键
  • 维护一个哈希集合,来一条查一条
  • 重复的直接丢弃

核心原则:去重必须在数据进入管道的第一时间做。等数据进了数据库再查重,成本高十倍。

def deduplicate(records):
    seen = set()
    clean = []
    for r in records:
        key = (r['symbol'], r['timestamp'], r['order_id'])
        if key not in seen:
            seen.add(key)
            clean.append(r)
    return clean

避坑指南:我曾经遇到过交易所的订单ID是自增整数,但不同交易对之间会重复。所以一定要把交易对也加进唯一键里。

4.2 时间戳对齐:把不同时钟拉到同一张表上

不同交易所的时间戳格式五花八门。有的用毫秒,有的用微秒,还有的用字符串。更头疼的是,有些交易所的服务器时钟本身就有偏移。

我建议统一做三件事:

  1. 全部转成纳秒时间戳(Python的 pd.Timestamp 默认支持纳秒)
  2. 对齐到整秒或整毫秒,根据你的策略精度需求来
  3. 做一次时间偏移校正,用交易所的公开时间戳API做参考
def align_timestamp(ts, freq='1s'):
    # 将时间戳对齐到整秒
    return pd.Timestamp(ts).floor(freq)

注意:对齐时间戳会丢失部分精度。如果你的策略依赖微秒级订单簿变化,建议保留原始时间戳,只在聚合分析时做对齐。

4.3 异常值处理:那些离谱的价格是怎么来的?

你想想看,一个正常交易对的价格突然跳了100倍,这合理吗?

我在项目中遇到过好几次,原因是交易所的撮合引擎在某个瞬间产生了「脏数据」——比如价格字段被错误地填成了订单ID,或者数量字段出现了负数。

我的过滤规则:

  • 价格不能为0或负数
  • 数量不能为0或负数
  • 价格不能偏离前N笔的均值超过3个标准差
  • 时间戳不能是未来的(超过当前时间5秒以上)

个人经验:我曾经用3倍标准差过滤掉了一批「异常」数据,结果发现那其实是某次大行情前的试单。后来我改成了「5倍标准差 + 人工复核」的策略。

def filter_outliers(records, window=20, z_thresh=5):
    prices = [r['price'] for r in records]
    mean = np.mean(prices[-window:])
    std = np.std(prices[-window:])
    return [r for r in records 
            if abs(r['price'] - mean) < z_thresh * std]

4.4 订单簿重建逻辑:从零拼出一张完整的挂单表

订单簿重建,说白了就是「用增量数据还原全量快照」。

交易所通常提供两种数据:

  • 全量快照:某一时刻的完整买卖盘
  • 增量更新:每次订单变化时的变动记录

重建逻辑其实不复杂:

  1. 拿到一个全量快照作为基准
  2. 后续每来一条增量,更新对应的价格档位
  3. 如果增量中的价格不在当前订单簿中,新增一个档位
  4. 如果增量中的数量为0,删除该档位

避坑指南:我曾经遇到增量数据丢失的情况,导致订单簿越拼越错。后来我加了一个「校验机制」——每1000条增量后,请求一次全量快照做对比。如果偏差超过阈值,就重新从快照开始重建。

class OrderBook:
    def __init__(self, snapshot):
        self.bids = {p: q for p, q in snapshot['bids']}
        self.asks = {p: q for p, q in snapshot['asks']}
    
    def apply_update(self, update):
        side = self.bids if update['side'] == 'bid' else self.asks
        if update['quantity'] == 0:
            side.pop(update['price'], None)
        else:
            side[update['price']] = update['quantity']

4.5 整体流程:一张图看懂

我把整个数据清洗与对齐的流程画成了下面这张图。你跟着箭头走一遍,基本就清楚了。

数据清洗与对齐流程 原始数据流入 去重(唯一键校验) 时间戳对齐 异常值过滤 订单簿重建(快照+增量) 交易所API / WebSocket 丢弃重复记录 统一到纳秒级 价格/数量/时间校验 生成可用的订单簿

总结一下:数据清洗不是一次性工作,而是贯穿整个回溯过程的持续任务。每当你发现回测结果异常,第一个怀疑的应该是数据质量,而不是策略逻辑。

嗯,这一章的内容就到这里。数据清洗虽然枯燥,但它是所有量化策略的基石。地基不稳,楼盖得再高也得塌。


蓝海数据掘金营,专注资料整理