我做量化回测已经第三年了,最让我头疼的不是策略本身,而是 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"]]
}
注意几个关键点:
- bids/asks 永远是字符串,避免浮点精度漂移,必须用 Decimal 解析。
- size=0 即删除,回测时要清理该档位,否则越积越多内存爆炸。
- local_timestamp 比 exchange timestamp 晚,差值就是网络+本地时钟漂移,做延迟归因时只能用 local_timestamp。
二、用 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,