9.2 做市商策略:Avellaneda-Stoikov 最优报价模型¶
做市商是金融市场的「流动性提供者」¶
做市商的商业模式:
在买卖两侧同时挂限价单,让其他参与者随时可以成交:
- 用 Bid 价格买入(略低于中间价)
- 用 Ask 价格卖出(略高于中间价)
理想情况下,买卖各 50% → 赚取价差 (Ask - Bid),库存归零。
为什么不是「无风险套利」?¶
**库存风险(Inventory Risk)**是核心挑战:
如果市场单边下跌,做市商在 Bid 价买入但价格继续跌, 手中持有的多头头寸不断贬值——这就是逆向选择问题。
Avellaneda-Stoikov 模型的核心贡献¶
Avellaneda & Stoikov(2008)解决了:
在有限时间区间内,考虑库存风险,做市商应该如何动态调整自己的报价?
关键思想:报价偏移随库存动态调整
- 多头库存过多 → 降低买价(减少继续买入),提高卖价(激励卖出去库存)
- 空头库存过多 → 做反向调整
这是量化做市商的理论基础。
学习目标¶
- 理解做市商的盈利来源和库存风险
- 理解 AS 模型的核心直觉:保留价 + 最优价差
- 模拟并分析 AS 做市商的 PNL 和库存变化
1. 这节究竟解决什么实际问题?¶
9.2 做市商策略:Avellaneda-Stoikov 最优报价模型 的具体任务是:用 Avellaneda-Stoikov 框架权衡库存风险与报价竞争。 本节将明确输入数据、计算过程、输出及其在后续研究中的用途。
2. 前置知识¶
理解买卖价、成交量、收益率与基本概率。 对公式的理解应包括变量定义、单位、时间索引和适用条件。
3. 数据说明¶
订单簿和成交数据是教学快照或模拟数据,不是可用于真实撮合的低延迟行情。 运行前先检查 shape、日期范围、列名与缺失值。数据看起来能画图,不代表它适合回答研究问题。
4. 学习目标与完成标准¶
完成本节后,你应能用自己的话解释 9.2 做市商策略:Avellaneda-Stoikov 最优报价模型 的输入、计算步骤、输出和局限;尤其能说明:用 Avellaneda-Stoikov 框架权衡库存风险与报价竞争。 你还应能独立改动一个参数并解释结果为什么变化,指出至少一个会让结论失真的假设。
5. 核心概念与公式¶
订单簿按价格和时间优先级记录未成交委托;排队位置、撤单和到达顺序都会影响成交概率。
报价收益必须扣除价差、排队位置、手续费、冲击和延迟;中间价不是保证成交价。 看到公式时依次确认:每个符号的单位是什么?它属于哪个时点?分母会不会为零?它是描述性统计量、预测量,还是交易规则?
6. 从零实现¶
原始代码按照“准备输入 → 计算中间量 → 得到结果 → 解释输出”的顺序组织。每个单元应检查变量类型、索引、维度和代表性数值。中间变量用于核对公式实现和定位计算错误。
7. 成熟库实现¶
模拟器能展示机制;生产级撮合、风控、监控与交易所接口不在本仓库范围。 使用库函数前先阅读参数含义、默认值和返回对象;库输出一个数字,并不说明这个数字适合你的交易假设。
8. 两种实现的差异¶
从零实现便于核对公式、调试和识别隐含假设;成熟库适合在计算逻辑已验证后复用。两种实现依赖相同的数据口径与时点假设,使用库函数不会改变输入数据的质量。
9. 预期结果与检查范围¶
你应看到维度正确的 Series/DataFrame、日期单调递增的索引,以及能支持“用 Avellaneda-Stoikov 框架权衡库存风险与报价竞争。”这一任务的表格或图形。金融日收益率均值通常接近 0;若出现极大的连续收益、空表、全 NaN 或索引错位,应先排查数据和对齐逻辑,而不是立即相信结果。
10. 常见错误与诊断¶
用中间价成交、忽略撤单与排队、混用事件时间和钟表时间、把模拟成交当真实成交。
诊断顺序:先打印类型与 shape,再看 head()/tail(),接着检查日期和 NaN,最后才比较数值。这样能避免在错误的对象上反复改参数。
11. 研究偏差检查¶
微观结构回测最易高估执行质量;需要最保守的成交假设和延迟压力测试。 结果至少应与一个简单基线比较,并明确哪些部分只是教学演示。
12. 三层练习与答案¶
- 基础练习:验证一个中间变量、补全一段小计算,并用一句话解释输出。
- 研究练习:改变窗口、样本区间、约束或标的,记录哪些结论仍成立。
- 开放项目:把本节方法嵌入模块项目,报告基线、失败结果和限制。
题目在 ../exercises/09_hft/02_market_making_exercises.ipynb;完整推导、可运行参考实现与结果检查在 ../solutions/09_hft/02_market_making_solutions.ipynb。请先独立作答,再打开答案。
13. 延伸阅读¶
延伸材料:Cartea, Jaimungal and Penalva: Algorithmic and High-Frequency Trading。阅读记录应包括研究假设、数据频率、样本范围和结论适用条件。
14. 下一节¶
完成本模块后,进入下一模块的概览页,并把本节的检查清单带到后续研究中。
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
np.random.seed(42)
n = 1000
sigma = 0.01
# 随机游走中间价
S = np.cumsum(np.random.normal(0, sigma, n)) + 100
delta = 0.05 # 单侧价差
# 简单固定价差策略模拟
inventory_simple = 0
cash_simple = 0
pnl_simple = []
for t in range(n):
bid_s = S[t] - delta
ask_s = S[t] + delta
buy_hit = np.random.random() < 0.3 # 30% 概率 Bid 被击中
sell_hit = np.random.random() < 0.3 # 30% 概率 Ask 被击中
if buy_hit: inventory_simple += 1; cash_simple -= bid_s
if sell_hit: inventory_simple -= 1; cash_simple += ask_s
pnl_simple.append(cash_simple + inventory_simple * S[t])
fig, axes = plt.subplots(2, 1, figsize=(12, 6), sharex=True)
S_series = pd.Series(S)
bid = S_series - delta
ask = S_series + delta
axes[0].plot(S[:200], 'k', lw=1, label='中间价')
axes[0].plot(bid[:200], 'g', lw=0.8, alpha=0.7, label=f'Bid=Mid-{delta}')
axes[0].plot(ask[:200], 'r', lw=0.8, alpha=0.7, label=f'Ask=Mid+{delta}')
axes[0].set_title('固定价差做市商报价示意(前200步)')
axes[0].legend(fontsize=8)
pd.Series(pnl_simple).plot(ax=axes[1], color='purple', lw=1)
axes[1].set_title('固定价差做市商 PNL(固定报价无法应对库存积累)')
plt.tight_layout(); plt.show()
第二部分:Avellaneda-Stoikov 模型——最优动态报价¶
核心公式¶
保留价(Reservation Price):做市商根据当前库存调整的内部中间价
$$r = S - q \cdot \gamma \cdot \sigma^2 \cdot (T - t)$$
- $q > 0$(多头库存过多)→ $r < S$:做市商认为「内部」价格更低,会降低报价来减少库存
- $\gamma$:风险厌恶系数(越大,对库存越敏感)
最优价差:
$$\delta^{bid} + \delta^{ask} = \gamma \sigma^2 (T-t) + \frac{2}{\gamma} \ln\left(1 + \frac{\gamma}{k}\right)$$
两个重要影响因素:
- 剩余时间多:价差适当放宽(有时间慢慢成交)
- 波动率高:价差放宽(弥补风险暴露)
np.random.seed(42)
n = 1000
sigma = 0.01; gamma = 0.1; eta = 0.015; T = 1.0
S = np.cumsum(np.random.normal(0, sigma, n)) + 100
inventory = 0; cash = 0
pnl_as, inv_as, bid_as, ask_as = [], [], [], []
for t in range(n):
time_left = T - t/n
r = S[t] - inventory * gamma * sigma**2 * time_left # 保留价
opt_spread = (gamma * sigma**2 * time_left +
2/gamma * np.log(1 + gamma/eta)) # 最优总价差
b = r - opt_spread/2
a = r + opt_spread/2
bid_as.append(b); ask_as.append(a)
lam = eta * np.exp(-eta * max(S[t]-b, 0))
buy = np.random.exponential(1/max(lam, 0.01)) < 1.0
sell = np.random.exponential(1/max(lam, 0.01)) < 1.0
if buy: inventory += 1; cash -= b
if sell: inventory -= 1; cash += a
pnl_as.append(cash + inventory * S[t])
inv_as.append(inventory)
fig, axes = plt.subplots(3, 1, figsize=(12, 9), sharex=True)
axes[0].plot(S[:300], 'k', lw=0.8, label='中间价')
axes[0].plot(bid_as[:300], 'g', lw=0.8, alpha=0.7, label='AS Bid(随库存调整)')
axes[0].plot(ask_as[:300], 'r', lw=0.8, alpha=0.7, label='AS Ask(随库存调整)')
axes[0].set_title('Avellaneda-Stoikov 动态报价(前300步)报价随库存动态偏移——库存多时Bid压低,防止继续积累')
axes[0].legend(fontsize=8)
pd.Series(inv_as).plot(ax=axes[1], color='blue', lw=1)
axes[1].axhline(0, color='gray', lw=0.8)
axes[1].set_title('库存变化(目标:维持在0附近,期末尽量归零)')
pd.Series(pnl_as).plot(ax=axes[2], color='green', lw=1.2)
axes[2].set_title('AS做市商累积 PNL')
plt.tight_layout(); plt.show()
print(f'期末库存: {inv_as[-1]} 期末PNL: {pnl_as[-1]:.4f}')
print('关键: AS报价通过动态调整,使库存自然收敛,避免大量单边持仓')
期末库存: -2 期末PNL: 489.0862 关键: AS报价通过动态调整,使库存自然收敛,避免大量单边持仓
练习¶
- 增大风险厌恶系数 gamma(从0.1到1.0),观察保留价偏移程度和期末库存如何变化、PNL是否更稳定。
- 实现库存硬限制:当库存超过±10时,停止该方向挂单(只挂另一侧),计算对PNL的影响。
- 研究「逆向选择成本」:Glosten-Milgrom 模型怎样解释知情交易者(Informed Traders)对做市商的侵蚀?为什么做市商对大额市价单报价会更宽?
下一节 → ../04_backtesting/10_performance_attribution.ipynb