9.1 订单簿微观结构:LOB、价差与订单流¶
为什么要研究订单簿?¶
传统量化分析看分钟级或日级数据,使用价格和成交量。 高频交易(HFT)在毫秒级就完成一笔交易,它关注的是:
- 下一秒价格是涨 0.01 元还是跌 0.01 元?
- 谁在买?谁在卖?订单流是否存在不平衡?
- 如何在 1 毫秒内完成最优报价,同时控制库存风险?
这些问题都依赖于对订单簿微观结构的深入理解。
微观结构的核心问题¶
价格是如何从一个水平移动到另一个水平的? 每一笔成交都是买方和卖方之间的博弈—— 谁先让步,谁付出成本,谁获取价差?
学习目标¶
- 理解限价委托订单簿(LOB)的结构和运行机制
- 掌握买卖价差的来源和意义
- 理解订单流不平衡(OFI)对价格的短期预测
- 分析 Kyle's Lambda(价格冲击系数)
1. 这节究竟解决什么实际问题?¶
9.1 订单簿微观结构:LOB、价差与订单流 的具体任务是:读懂订单簿层级、价差和订单流,理解价格为何不是单一数字。 本节将明确输入数据、计算过程、输出及其在后续研究中的用途。
2. 前置知识¶
理解买卖价、成交量、收益率与基本概率。 对公式的理解应包括变量定义、单位、时间索引和适用条件。
3. 数据说明¶
订单簿和成交数据是教学快照或模拟数据,不是可用于真实撮合的低延迟行情。 运行前先检查 shape、日期范围、列名与缺失值。数据看起来能画图,不代表它适合回答研究问题。
4. 学习目标与完成标准¶
完成本节后,你应能用自己的话解释 9.1 订单簿微观结构:LOB、价差与订单流 的输入、计算步骤、输出和局限;尤其能说明:读懂订单簿层级、价差和订单流,理解价格为何不是单一数字。 你还应能独立改动一个参数并解释结果为什么变化,指出至少一个会让结论失真的假设。
5. 核心概念与公式¶
订单簿按价格和时间优先级记录未成交委托;排队位置、撤单和到达顺序都会影响成交概率。
报价收益必须扣除价差、排队位置、手续费、冲击和延迟;中间价不是保证成交价。 看到公式时依次确认:每个符号的单位是什么?它属于哪个时点?分母会不会为零?它是描述性统计量、预测量,还是交易规则?
6. 从零实现¶
原始代码按照“准备输入 → 计算中间量 → 得到结果 → 解释输出”的顺序组织。每个单元应检查变量类型、索引、维度和代表性数值。中间变量用于核对公式实现和定位计算错误。
7. 成熟库实现¶
模拟器能展示机制;生产级撮合、风控、监控与交易所接口不在本仓库范围。 使用库函数前先阅读参数含义、默认值和返回对象;库输出一个数字,并不说明这个数字适合你的交易假设。
8. 两种实现的差异¶
从零实现便于核对公式、调试和识别隐含假设;成熟库适合在计算逻辑已验证后复用。两种实现依赖相同的数据口径与时点假设,使用库函数不会改变输入数据的质量。
9. 预期结果与检查范围¶
你应看到维度正确的 Series/DataFrame、日期单调递增的索引,以及能支持“读懂订单簿层级、价差和订单流,理解价格为何不是单一数字。”这一任务的表格或图形。金融日收益率均值通常接近 0;若出现极大的连续收益、空表、全 NaN 或索引错位,应先排查数据和对齐逻辑,而不是立即相信结果。
10. 常见错误与诊断¶
用中间价成交、忽略撤单与排队、混用事件时间和钟表时间、把模拟成交当真实成交。
诊断顺序:先打印类型与 shape,再看 head()/tail(),接着检查日期和 NaN,最后才比较数值。这样能避免在错误的对象上反复改参数。
11. 研究偏差检查¶
微观结构回测最易高估执行质量;需要最保守的成交假设和延迟压力测试。 结果至少应与一个简单基线比较,并明确哪些部分只是教学演示。
12. 三层练习与答案¶
- 基础练习:验证一个中间变量、补全一段小计算,并用一句话解释输出。
- 研究练习:改变窗口、样本区间、约束或标的,记录哪些结论仍成立。
- 开放项目:把本节方法嵌入模块项目,报告基线、失败结果和限制。
题目在 ../exercises/09_hft/01_order_book_basics_exercises.ipynb;完整推导、可运行参考实现与结果检查在 ../solutions/09_hft/01_order_book_basics_solutions.ipynb。请先独立作答,再打开答案。
13. 延伸阅读¶
延伸材料:Cartea, Jaimungal and Penalva: Algorithmic and High-Frequency Trading。阅读记录应包括研究假设、数据频率、样本范围和结论适用条件。
14. 下一节¶
下一节是 02_market_making。本节的概念会成为理解它的输入,而不是孤立的名词。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
np.random.seed(42)
print('OK')
OK
import matplotlib.pyplot as plt
# 1. 设置系统自带的中文字体(这里使用黑体 SimHei)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 如果你想用微软雅黑,可以改成 ['Microsoft YaHei']
# 2. 解决更换字体后,负号(-)显示为方块的问题
plt.rcParams['axes.unicode_minus'] = False
第一部分:限价委托订单簿(LOB)的结构¶
什么是订单簿?¶
所有处于「等待成交」状态的限价订单,按价格排列形成两列:
卖方(Ask Side) 买方(Bid Side)
价格 数量 价格 数量
100.06 1200 ← → 100.00 800
100.04 800 ← 最优卖价 → 99.98 1500 ← 最优买价
100.02 500 ← Spread → 99.96 2000
↑ Best Ask Best Bid ↑
Spread = 100.02 - 100.00 = 0.02
关键术语¶
| 术语 | 公式 | 含义 |
|---|---|---|
| Best Bid | 买方最高报价 | 市价卖单可立即成交的价格 |
| Best Ask | 卖方最低报价 | 市价买单可立即成交的价格 |
| 中间价(Mid) | (Bid+Ask)/2 | 市场公认的「公允价格」 |
| Spread | Ask - Bid | 流动性成本,也是做市商收入来源 |
| 深度(Depth) | 各价位的委托数量 | 市场承接能力 |
为什么价差(Spread)不为零?¶
做市商提供流动性(随时愿意买卖),承担库存风险,因此要求补偿—— 价差就是这个补偿。市场流动性越好,价差越小(竞争越激烈)。
# 可视化完整订单簿深度
bid_prices = [100.00, 99.98, 99.96, 99.94, 99.92]
ask_prices = [100.02, 100.04, 100.06, 100.08, 100.10]
bid_vols = [800, 1500, 2000, 3000, 1200]
ask_vols = [500, 800, 1200, 600, 900]
mid = (bid_prices[0] + ask_prices[0]) / 2
spread = ask_prices[0] - bid_prices[0]
fig, ax = plt.subplots(figsize=(11, 6))
ax.barh(bid_prices, [-v for v in bid_vols], color='#2ecc71', alpha=0.75, label='买方委托(Bid)')
ax.barh(ask_prices, ask_vols, color='#e74c3c', alpha=0.75, label='卖方委托(Ask)')
ax.axhline(mid, color='blue', lw=2, linestyle='--',
label=f'中间价 Mid={mid:.2f}')
ax.axhspan(bid_prices[0], ask_prices[0], alpha=0.15, color='yellow',
label=f'价差 Spread={spread:.2f} ({spread/mid*10000:.1f}bp)')
ax.set_xlim(-4000, 2000)
ax.set_xlabel('委托数量(负数=买方,正数=卖方)')
ax.set_ylabel('价格')
ax.set_title('限价委托订单簿(LOB)深度可视化绿色=买方委托,红色=卖方委托')
ax.legend(loc='right', fontsize=9)
ax.grid(alpha=0.25)
plt.tight_layout(); plt.show()
print(f'Best Bid: {bid_prices[0]} Best Ask: {ask_prices[0]}')
print(f'中间价: {mid} 价差: {spread} ({spread/mid*10000:.1f}bps)')
print(f'买方前5档深度: {sum(bid_vols)} 股 卖方前5档深度: {sum(ask_vols)} 股')
Best Bid: 100.0 Best Ask: 100.02 中间价: 100.00999999999999 价差: 0.01999999999999602 (2.0bps) 买方前5档深度: 8500 股 卖方前5档深度: 4000 股
第二部分:订单流不平衡(OFI)——短期价格预测因子¶
什么是主动成交?¶
每笔成交都由一方「主动」发起:
- 买方主动(Buyer-initiated):机构用市价单买入,价格通常上升压力
- 卖方主动(Seller-initiated):机构用市价单卖出,价格通常下行压力
订单流不平衡(Order Flow Imbalance, OFI)¶
$$\text{OFI} = \sum_{\tau=1}^{t} (V_{\tau}^{\text{buy}} - V_{\tau}^{\text{sell}})$$
OFI > 0 → 买压强,预测短期价格上涨
OFI < 0 → 卖压强,预测短期价格下跌
OFI 的预测原理¶
大机构在买入时,为了避免推高价格,会把订单拆成大量小单分批执行。 在成交过程中,OFI 逐渐积累,价格慢慢上移——这就是订单流的信息含量。
np.random.seed(42)
n_trades = 2000
# 随机生成买卖方向(主动买=1,主动卖=-1)
# 微弱买压:p=0.52
direction = np.random.choice([1, -1], n_trades, p=[0.52, 0.48])
volume = np.random.randint(100, 2000, n_trades)
mid_start = 100.0
# 价格冲击:主动买单推价格上涨,卖单压价格
price_impact = 0.0001 * volume * direction
mid_prices = mid_start + np.cumsum(price_impact)
# OFI: 滚动20笔净订单流
signed_vol = direction * volume
ofi = pd.Series(signed_vol).rolling(20).sum()
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 7), sharex=True)
ax1.plot(mid_prices, color='steelblue', lw=1, alpha=0.9)
ax1.set_title('模拟中间价走势(受订单流推动)')
ax1.set_ylabel('价格'); ax1.grid(alpha=0.3)
ax2.plot(ofi, color='darkorange', lw=1)
ax2.axhline(0, color='black', lw=0.8)
ax2.fill_between(range(n_trades), ofi, 0, where=(ofi>0), alpha=0.3, color='green')
ax2.fill_between(range(n_trades), ofi, 0, where=(ofi<0), alpha=0.3, color='red')
ax2.set_title('订单流不平衡(OFI,20笔滚动)绿=买压,红=卖压')
ax2.set_ylabel('OFI'); ax2.grid(alpha=0.3)
plt.tight_layout(); plt.show()
# OFI 对短期价格变化的预测力
ofi_lag = ofi.shift(1).dropna()
price_chg = pd.Series(mid_prices).diff().dropna()
min_len = min(len(ofi_lag), len(price_chg))
from scipy import stats
corr, p = stats.pearsonr(ofi_lag.iloc[:min_len], price_chg.iloc[:min_len])
print(f'OFI(t-1) vs 价格变化(t) 相关系数: {corr:.4f} (p={p:.4f})')
print('正相关验证:买压(正OFI)确实对短期价格有向上预测力')
练习¶
- 用 OFI 滞后 1 步预测下一步价格变化,计算 Spearman IC,OFI 对短期价格有多少预测力?
- 模拟「大单隐蔽拆分」:一个 50000 股的大买单被拆成 50 个 1000 股,分析拆单过程中 OFI 积累情况和价格推动。
- 研究 Kyle's Lambda(价格冲击系数):用 OLS 回归 价格变化 = Lambda * signed_volume,Lambda 越大意味着该股票流动性越差。
下一节 → 02_market_making.ipynb