做量化交易的朋友都知道,回测和实盘之间最大的鸿沟,往往不是策略逻辑,而是数据精度。特别是涉及加密货币这类高波动品种时,如果只依赖K线或成交数据,市场微观结构中的很多信号会被完全忽略。过去一年,我们在内部策略研发中全面转向本地订单簿重建,效果非常显著。今天把这一路的经验和同好分享。
痛点:深度数据缺失导致因子失真
举一个真实案例:我们曾开发过一组基于买卖压力的短期反转因子,在第三方数据平台的聚合深度上回测表现极佳,但上线后却出现了严重的滑点和信号滞后。追查之后发现,聚合深度并非来自单一交易所,且更新频率不稳定。当市场剧烈波动时,本地看到的深度已经完全过时。如果你的因子依赖于某个价格档位的挂单量,你必须保证该数据与交易所的实时撮合引擎保持同步。 这就是我们决心自建订单簿同步系统的起点。
数据需求:快照为基,增量驱新
要精确还原交易所订单簿,数据必须满足两条:
- 初始全量快照:提供某一时刻所有有效挂单。
- 有序增量更新:包含每一笔导致簿结构变化的事件,且带有严格递增的序号。
我们考察了多种数据渠道,在实际生产中,AllTick 等数据供应商的 WebSocket 推送能够满足快照初始化和增量序列追踪的要求,极大地简化了我们的开发工作量。
本地重建的核心实现
技术上我们选择了 Python 生态里的 SortedDict,它兼具哈希表的快速存取和树结构的有序性,非常适合管理价格队列。
from sortedcontainers import SortedDict
# 买方价格队列,高到低
bids = SortedDict(lambda x: -x)
# 卖方价格队列,低到高
asks = SortedDict()
WebSocket 消息处理模块只做一件事:解析 JSON,遍历 bids 和 asks 列表,按数量是否为 0 决定增删。代码很精练,但稳定性极佳。
import websocket
import json
from sortedcontainers import SortedDict
bids = SortedDict(lambda x: -x)
asks = SortedDict()
def on_message(ws, message):
data = json.loads(message)
# 处理订单簿更新
process_orderbook(data)
def process_orderbook(data):
global bids, asks
for update in data.get("bids", []):
price, size = update
if size == 0:
bids.pop(price, None)
else:
bids[price] = size
for update in data.get("asks", []):
price, size = update
if size == 0:
asks.pop(price, None)
else:
asks[price] = size
ws = websocket.WebSocketApp("wss://api.alltick.co/crypto/orderbook",
on_message=on_message)
ws.run_forever()
防止“失帧”的序列监测
我们对增量流实施了严格的序列号校验。每处理完一条消息,记录其 ID,下一条必须连续。若发现跳号,立即触发快照重载——宁可短暂丢弃中间状态,也不允许一个错误的价格档位残留在本地簿中。同时,对 WebSocket 连接设置心跳与自动重连,保证 7×24 小时无人值守。
从簿数据到因子
订单簿稳定运行之后,我们可以随时导出任意时刻的快照。利用本地簿,我们构建了不平衡深度、加权买卖均价、订单流毒性等一系列高频因子。举个例子,当卖单深度在某个价位突然增厚,同时买单没有相应反应,这往往预示着短期抛压。
本地数据还能方便地生成可视化报告:
import matplotlib.pyplot as plt
def plot_orderbook():
plt.plot(list(bids.keys()), list(bids.values()), color='green', label='Bids')
plt.plot(list(asks.keys()), list(asks.values()), color='red', label='Asks')
plt.legend()
plt.show()
由于簿数据完全由自己掌控,因子计算不再受制于第三方聚合偏差,回测与实盘一致性大幅提升。如果社区里的朋友正在折腾高频或微观结构策略,非常建议你投入精力把数据根基打牢——你会发现很多之前难以捉摸的市场规律,一下子变得清晰起来。


