9.2 做市商策略:Avellaneda-Stoikov 最优报价模型¶
做市商是金融市场的「流动性提供者」¶
做市商的商业模式:
在买卖两侧同时挂限价单,让其他参与者随时可以成交:
- 用 Bid 价格买入(略低于中间价)
- 用 Ask 价格卖出(略高于中间价)
理想情况下,买卖各 50% → 赚取价差 (Ask - Bid),库存归零。
为什么不是「无风险套利」?¶
库存风险(Inventory Risk)是核心挑战:
如果市场单边下跌,做市商在 Bid 价买入但价格继续跌, 手中持有的多头头寸不断贬值——这就是逆向选择问题。
Avellaneda-Stoikov 模型的核心贡献¶
Avellaneda & Stoikov(2008)解决了:
在有限时间区间内,考虑库存风险,做市商应该如何动态调整自己的报价?
关键思想:报价偏移随库存动态调整
- 多头库存过多 → 降低买价(减少继续买入),提高卖价(激励卖出去库存)
- 空头库存过多 → 做反向调整
这是量化做市商的理论基础。
学习目标¶
- 理解做市商的盈利来源和库存风险
- 理解 AS 模型的核心直觉:保留价 + 最优价差
- 模拟并分析 AS 做市商的 PNL 和库存变化
In [1]:
Copied!
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
np.random.seed(42)
print('OK')
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
np.random.seed(42)
print('OK')
OK
In [2]:
Copied!
import matplotlib.pyplot as plt
# 1. 设置系统自带的中文字体(这里使用黑体 SimHei)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 如果你想用微软雅黑,可以改成 ['Microsoft YaHei']
# 2. 解决更换字体后,负号(-)显示为方块的问题
plt.rcParams['axes.unicode_minus'] = False
import matplotlib.pyplot as plt
# 1. 设置系统自带的中文字体(这里使用黑体 SimHei)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 如果你想用微软雅黑,可以改成 ['Microsoft YaHei']
# 2. 解决更换字体后,负号(-)显示为方块的问题
plt.rcParams['axes.unicode_minus'] = False
In [3]:
Copied!
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()
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)$$
两个重要影响因素:
- 剩余时间多:价差适当放宽(有时间慢慢成交)
- 波动率高:价差放宽(弥补风险暴露)
In [4]:
Copied!
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报价通过动态调整,使库存自然收敛,避免大量单边持仓')
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
In [ ]:
Copied!