halo 的技术博客

返回

订单簿不是表格,是图#

打开任何行情软件,你看到的是一张限价订单簿(Limit Order Book, LOB):左边买盘、右边卖盘,每一档挂着价格和数量。大多数机器学习做法把它拍平成一条特征向量丢进模型——但这样丢掉了结构信息

LOB 天然是一张图:

  • 节点:每一个价位,特征是该档的挂单量(深度)。
  • :同侧相邻价位(price ladder 的连续性)、以及买卖最优价之间的价差桥(best bid–ask spread)。
  • 标签:下一刻中间价方向(涨 / 跌)。

这篇文章做两件事:(1) 用一个风格化但平稳的 LOB 模拟器生成数据,(2) 纯 numpy 手写 2 层图卷积网络(GCN),让它在”图”上做节点聚合,预测中间价方向,并和逻辑回归、多数类基线同台对比。最后我会诚实地说清:在这个风格化设定里,图结构并没有带来魔法。

风格化 LOB 模拟器:OFI 引导价格#

真实 LOB 太复杂,我们先建一个可解释的代理。核心微观结构事实:订单流不平衡(Order Flow Imbalance, OFI)Granger 引导中间价——买压领先于上涨,被大量实证研究支持。

限价订单簿快照:买卖两侧深度剖面

上图是一帧典型的订单簿深度剖面:靠近最优价的档位挂单最厚,向两侧指数衰减;买压强时买侧整体更深、卖侧更薄。深度剖面本身就是 OFI 的镜像

订单流不平衡(OFI) Granger 引导中间价:买压领先于上涨

中间价轨迹与 OFI 叠在一起看:OFI 的波峰(买压)往往领先于价格的上行段——这就是”订单流不平衡引导价格”的直观证据。

把 LOB 建成图#

固定一个截面图结构(所有快照共享同一张图),节点是 2L=20 个价位:

  • 同侧相邻价位连边(bid 内、ask 内各连成链)
  • 最优买价与最优卖价之间连一条”价差桥”边

邻接矩阵对称归一化(GCN 标准做法):

def build_graph(L):
    n = 2 * L
    A = np.zeros((n, n))
    # 同侧价格阶梯
    for side in (range(L), range(L, 2*L)):
        s = list(side)
        for a, b in zip(s[:-1], s[1:]):
            A[a, b] = A[b, a] = 1
    # 最优买卖价差桥
    A[L-1, L] = A[L, L-1] = 1
    d = A.sum(1)
    d_inv = 1.0 / np.sqrt(d + 1e-12)
    return d_inv[:, None] * A * d_inv[None, :]   # 对称归一化 \hat{A}

Ahat = build_graph(10)
python

节点特征 X 是该快照的深度剖面 (2L,) 扩展到 (2L, 1) 或拼接更多信息。我们要做的是:让 GCN 把相邻档位的信息聚合到每个节点上,再用聚合后的表示预测”下一刻中间价涨还是跌”。

纯 numpy 手写 2 层 GCN#

GCN 的逐层传播规则是 H(l+1)=σ(A^H(l)W(l))H^{(l+1)} = \sigma(\hat{A} H^{(l)} W^{(l)})。两层堆叠:

def gcn_forward(X, Ahat, W1, W2):
    # X: (N, F_in) 节点特征;Ahat: (N, N) 归一化邻接
    H1 = np.tanh(Ahat @ X @ W1)            # (N, hidden)
    H2 = Ahat @ H1 @ W2                    # (N, 1) 每个节点输出一个标量
    return H2.flatten()

# 用图池化(对所有节点取平均)得到整图表示 → sigmoid 输出涨的概率
def predict(X, Ahat, W1, W2):
    h = gcn_forward(X, Ahat, W1, W2)
    s = np.tanh(h.mean())
    return 1.0 / (1 + np.exp(-s))
python

训练用普通梯度下降 + 二元交叉熵(这里只给核心循环骨架,完整实现见配图脚本):

y = (mid[1:] > mid[:-1]).astype(float)     # 标签:下一刻涨=1
for epoch in range(400):
    p = predict(X, Ahat, W1, W2)
    loss = -(y * np.log(p+1e-9) + (1-y) * np.log(1-p+1e-9)).mean()
    # 反向传播更新 W1, W2(省略链式法则细节)
    ...
python

2 层 GCN 训练:训练/验证损失收敛

训练/验证损失都收敛到约 0.693——这正是随机二分类的交叉熵下界(log 2 ≈ 0.693)。这是个重要信号:GCN 在这个任务上几乎没有学到可泛化的方向性信号

诚实的结果:GCN ≈ 多数类基线#

把三种方法的测试集方向预测准确率摆出来:

模型测试准确率
多数类基线0.603
逻辑回归(flatten 特征)0.506
GCN(图结构)0.603

中间价方向预测:GCN 仅小幅优于 flatten 基线

结论很扎心,但也很有教学意义:

  • 标签本身不平衡(涨 0.414 / 跌 0.586),多数类基线就有 0.603
  • GCN 正好是 0.603——等于没比随机强;逻辑回归反而掉到 0.506(过拟合到噪声)。
  • 图结构没有提供增量信息

为什么?因为在这个风格化 LOB 里,所有预测力都浓缩在一个标量 OFI 上。20 个价位的深度剖面,本质都是 OFI 的线性变形;GCN 做空间聚合,聚合出来的也就是”平均深度”,等价于 OFI。跨价位的拓扑结构(哪档连哪档)在这里是退化的——L 个相邻边 + 一条价差桥,信息量远小于那个聚合不平衡。

那图表示到底有没有用?#

有用,但不在这种风格化设定里。图结构真正发力的地方是真实 LOB 的队列位置动力学(queue position dynamics)

  1. 同价位内的队列顺序:谁排在前面、谁会被先成交。这是一张”时间-价格”二维图,节点是队列里的单笔挂单,边是时间先后与同价相邻。GCN / GAT 在这里能学到”我的单离成交还有几手”这类信息,直接服务于智能单路由。
  2. 跨资产订单簿图:把多只相关股票的 LOB 建成一张图(节点=各股的各价位),边的权重=截面相关性。危机时相关性飙升,图结构能捕捉”传染式”的流动性蒸发。
  3. 事件驱动的边:真实 LOB 的边不该是固定的价格阶梯,而该随订单流事件(新单、撤单、成交)动态重连——这更接近动态图神经网络(DGNN)的领地。

换句话说:当你把 LOB 压成”深度剖面 + 一个 OFI 标量”时,图是多余的;当你保留队列、时间、跨资产、事件这些真实结构时,图才是必要的

落到研究的几条提醒#

  1. 先问信号在哪。如果你的 LOB 特征本质是一个聚合标量(OFI),flatten + 逻辑回归就够,上 GCN 是 overhead。
  2. 警惕 loss 收敛到 0.693。这往往意味着模型没学到东西,只是拟合了先验。第一时间画准确率对比图(如上),别被”损失在降”骗了。
  3. 固定邻接矩阵是简化,真实场景用动态图;但即便如此,也要先证明”拓扑”带来了 flatten 没有的信息,而不是只带来参数。
  4. 标签不平衡要报告多数类基线。没有基线,0.60 的准确率毫无意义。

特征工程:别只喂深度#

本文为了可解释,节点特征只用深度剖面。真实建模里,节点特征应该更丰富:

  • 各档的 order book imbalance(bid_vol - ask_vol) / (bid_vol + ask_vol),比裸深度更贴近 OFI。
  • 成交队列长度 / 排队位置:你在最优价的单前面还有几手,决定你多久被成交。
  • 档位的时间驻留(time-on-book):挂单待了多久没动,反映真实意图。
  • 跨快照的差分特征Δdepthdepth 信息量大得多,因为它直接编码了订单流的进出。

一个实用的拼接方式:

node_feat = np.stack([
    depth,                       # 原始深度
    np.gradient(depth),         # 深度斜率(哪侧在变薄)
    (depth - depth.mean()) / (depth.std() + 1e-9),  # z-score
], axis=1)                      # (2L, 3) 多维节点特征
python

如何把这套框架接上真实数据#

风格化模拟的价值是给你一套可运行、可证伪的基线。要走向真实,三步走:

  1. 拿逐笔数据(tick / message data)重建 LOB:用 python-break 或交易所提供的 ITCH 流,还原每一刻的买卖各 L 档深度与队列。
  2. 把固定图换成动态图:每次订单流事件(新单/撤单/成交)后重算邻接矩阵,用 DGNN(如门控图神经网络)滚动更新节点表示。
  3. 标签换成可交易目标:不要只预测「涨/跌」(0.60 准确率的游戏),改成预测「未来 N 秒的中间价漂移」「价差收窄概率」或「被逆向选择概率」,这些才是能接进执行算法的信号。

做完这三步,再回过头画一次「GCN vs flatten 基线 vs 多数类」的对比图——如果 GCN 仍然贴着多数类基线,说明你的图结构还是没提供增量信息,继续调特征、调边定义,直到图真正「说话」。

边界声明#

这个 LOB 是高度风格化的:OFI→价格用了人为设定的 60% 同向概率,深度剖面是 OFI 的确定性函数,没有真实逐笔报单、没有队列、没有撤单风暴、没有跨资产联动。因此”GCN 不优于基线”是这个设定的结论,不能外推到真实 LOB——真实数据里图结构(尤其队列位置)的价值,需要你拿真实逐笔数据重新验证。本文的价值在于给了一套可运行、可证伪的基线框架,而不是一个”GCN 战胜市场”的故事。

订单簿的图表示学习与限价单簿动力学:当 GCN 遇见微观结构
https://blog.halo26812.eu.org/blog/orderbook-graph-dynamics
Author halo
Published at 2026年7月11日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨