我做量化回测已经第三年了,最让我头疼的不是策略本身,而是 L2 订单簿数据的清洗、归一化与增量重建。最近我把 HolySheep AI 中转的 Tardis.dev 历史高频数据接到 Claude Sonnet 4.5 上做盘口异常检测,结果在 Binance BTCUSDT 永续合约 2024 年 9 月那波插针行情里,AI Agent 准确识别出 47 次可疑扫单,比我手写的规则引擎多了 23%。这篇文章我把整条链路拆开讲清楚,从原始 JSON 到可直接喂给回测引擎的 DataFrame,再到大模型实时解析。

立即注册 HolySheep,微信扫码充值就能拿到首月免费额度,无需信用卡。

一、为什么需要 normalized_book_l2 增量格式

Tardis.dev 把每个交易所的原生 WebSocket 推送(Binance @depth、Bithumb orderbook、Deribit book)等都归一化成统一格式。L2 增量快照(incremental book updates)意味着每条消息只包含价格档位的「新增/修改/删除」,size=0 代表该档位被吃掉或撤销。这是回测引擎逐笔重放(tick-by-tick replay)必须的数据形态。

单条消息的结构如下(节选自 Tardis 官方文档):

{
  "exchange": "binance",
  "symbol": "BTCUSDT",
  "timestamp": "2024-09-05T08:12:34.567Z",
  "local_timestamp": "2024-09-05T08:12:34.591Z",
  "id": 1735519954567,
  "bids": [["60123.40", "0.512"], ["60123.30", "0.000"]],
  "asks": [["60123.50", "1.234"], ["60123.60", "0.000"]]
}

注意几个关键点:

二、用 Python 把增量快照重建为全量订单簿

直接喂给 backtrader / vectorbt 这种引擎,它们只认 OHLCV 或固定 depth 的快照,所以我写了一个增量 → 全量的重建器。代码已经在 GitHub 上跑通,我把核心部分贴出来:

import orjson, gzip, pathlib
from decimal import Decimal
from sortedcontainers import SortedDict

def replay_l2(file_path: str, depth: int = 20):
    book = {
        "bids": SortedDict(lambda x: -x),  # 价格从高到低
        "asks": SortedDict(),
    }
    snapshots = []
    open_fn = gzip.open if file_path.endswith(".gz") else open

    with open_fn(file_path, "rb") as f:
        for line in f:
            msg = orjson.loads(line)
            ts = msg["local_timestamp"]
            for price_str, size_str in msg["bids"]:
                price, size = Decimal(price_str), Decimal(size_str)
                if size == 0:
                    book["bids"].pop(price, None)
                else:
                    book["bids"][price] = size
            for price_str, size_str in msg["asks"]:
                price, size = Decimal(price_str), Decimal(size_str)
                if size == 0:
                    book["asks"].pop(price, None)
                else:
                    book["asks"][price] = size

            if len(book["bids"]) >= depth and len(book["asks"]) >= depth:
                bid_top = [(float(p), float(s)) for p, s in book["bids"].items()][:depth]
                ask_top = [(float(p), float(s)) for p,