订单簿不是表格,是图#
打开任何行情软件,你看到的是一张限价订单簿(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 引导中间价——买压领先于上涨,被大量实证研究支持。
import numpy as np
np.random.seed(7)
T = 4000
# 1) OFI:AR(1) 均值回复过程,模拟买卖压力的时间聚集
ofi = np.zeros(T)
ofi[0] = 0.0
for t in range(1, T):
ofi[t] = 0.92 * ofi[t-1] + 0.08 * np.random.randn()
# 2) 中间价:OFI 符号以 60% 概率决定下一刻方向
mid = np.zeros(T)
mid[0] = 100.0
for t in range(1, T):
sign = np.sign(ofi[t-1])
prob_up = 0.5 + 0.1 * sign # |sign|=1 → 60% 概率同向
step = np.random.choice([+0.05, -0.05], p=[prob_up, 1-prob_up])
mid[t] = mid[t-1] + step
# 3) 由 OFI 构造深度剖面:买压(ofi>0)→买侧更深、卖侧更薄
L = 10
def depth_profile(ofi_t):
base = np.exp(-np.arange(L) / 3.0) # 越远离最优价量越薄
bid = base * (1 + 0.3 * ofi_t)
ask = base * (1 - 0.3 * ofi_t)
return np.concatenate([bid, ask]) # (2L,) 节点特征python
上图是一帧典型的订单簿深度剖面:靠近最优价的档位挂单最厚,向两侧指数衰减;买压强时买侧整体更深、卖侧更薄。深度剖面本身就是 OFI 的镜像。

中间价轨迹与 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 的逐层传播规则是 。两层堆叠:
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
训练/验证损失都收敛到约 0.693——这正是随机二分类的交叉熵下界(log 2 ≈ 0.693)。这是个重要信号:GCN 在这个任务上几乎没有学到可泛化的方向性信号。
诚实的结果:GCN ≈ 多数类基线#
把三种方法的测试集方向预测准确率摆出来:
| 模型 | 测试准确率 |
|---|---|
| 多数类基线 | 0.603 |
| 逻辑回归(flatten 特征) | 0.506 |
| GCN(图结构) | 0.603 |

结论很扎心,但也很有教学意义:
- 标签本身不平衡(涨 0.414 / 跌 0.586),多数类基线就有 0.603。
- GCN 正好是 0.603——等于没比随机强;逻辑回归反而掉到 0.506(过拟合到噪声)。
- 图结构没有提供增量信息。
为什么?因为在这个风格化 LOB 里,所有预测力都浓缩在一个标量 OFI 上。20 个价位的深度剖面,本质都是 OFI 的线性变形;GCN 做空间聚合,聚合出来的也就是”平均深度”,等价于 OFI。跨价位的拓扑结构(哪档连哪档)在这里是退化的——L 个相邻边 + 一条价差桥,信息量远小于那个聚合不平衡。
那图表示到底有没有用?#
有用,但不在这种风格化设定里。图结构真正发力的地方是真实 LOB 的队列位置动力学(queue position dynamics):
- 同价位内的队列顺序:谁排在前面、谁会被先成交。这是一张”时间-价格”二维图,节点是队列里的单笔挂单,边是时间先后与同价相邻。GCN / GAT 在这里能学到”我的单离成交还有几手”这类信息,直接服务于智能单路由。
- 跨资产订单簿图:把多只相关股票的 LOB 建成一张图(节点=各股的各价位),边的权重=截面相关性。危机时相关性飙升,图结构能捕捉”传染式”的流动性蒸发。
- 事件驱动的边:真实 LOB 的边不该是固定的价格阶梯,而该随订单流事件(新单、撤单、成交)动态重连——这更接近动态图神经网络(DGNN)的领地。
换句话说:当你把 LOB 压成”深度剖面 + 一个 OFI 标量”时,图是多余的;当你保留队列、时间、跨资产、事件这些真实结构时,图才是必要的。
落到研究的几条提醒#
- 先问信号在哪。如果你的 LOB 特征本质是一个聚合标量(OFI),flatten + 逻辑回归就够,上 GCN 是 overhead。
- 警惕 loss 收敛到 0.693。这往往意味着模型没学到东西,只是拟合了先验。第一时间画准确率对比图(如上),别被”损失在降”骗了。
- 固定邻接矩阵是简化,真实场景用动态图;但即便如此,也要先证明”拓扑”带来了 flatten 没有的信息,而不是只带来参数。
- 标签不平衡要报告多数类基线。没有基线,0.60 的准确率毫无意义。
特征工程:别只喂深度#
本文为了可解释,节点特征只用深度剖面。真实建模里,节点特征应该更丰富:
- 各档的 order book imbalance:
(bid_vol - ask_vol) / (bid_vol + ask_vol),比裸深度更贴近 OFI。 - 成交队列长度 / 排队位置:你在最优价的单前面还有几手,决定你多久被成交。
- 档位的时间驻留(time-on-book):挂单待了多久没动,反映真实意图。
- 跨快照的差分特征:
Δdepth比depth信息量大得多,因为它直接编码了订单流的进出。
一个实用的拼接方式:
node_feat = np.stack([
depth, # 原始深度
np.gradient(depth), # 深度斜率(哪侧在变薄)
(depth - depth.mean()) / (depth.std() + 1e-9), # z-score
], axis=1) # (2L, 3) 多维节点特征python如何把这套框架接上真实数据#
风格化模拟的价值是给你一套可运行、可证伪的基线。要走向真实,三步走:
- 拿逐笔数据(tick / message data)重建 LOB:用
python-break或交易所提供的 ITCH 流,还原每一刻的买卖各 L 档深度与队列。 - 把固定图换成动态图:每次订单流事件(新单/撤单/成交)后重算邻接矩阵,用 DGNN(如门控图神经网络)滚动更新节点表示。
- 标签换成可交易目标:不要只预测「涨/跌」(0.60 准确率的游戏),改成预测「未来 N 秒的中间价漂移」「价差收窄概率」或「被逆向选择概率」,这些才是能接进执行算法的信号。
做完这三步,再回过头画一次「GCN vs flatten 基线 vs 多数类」的对比图——如果 GCN 仍然贴着多数类基线,说明你的图结构还是没提供增量信息,继续调特征、调边定义,直到图真正「说话」。
边界声明#
这个 LOB 是高度风格化的:OFI→价格用了人为设定的 60% 同向概率,深度剖面是 OFI 的确定性函数,没有真实逐笔报单、没有队列、没有撤单风暴、没有跨资产联动。因此”GCN 不优于基线”是这个设定的结论,不能外推到真实 LOB——真实数据里图结构(尤其队列位置)的价值,需要你拿真实逐笔数据重新验证。本文的价值在于给了一套可运行、可证伪的基线框架,而不是一个”GCN 战胜市场”的故事。