Avellaneda-Stoikov 做市模型
A-S 2008 论文核心:HJB 方程、值函数、reservation price、最优报价闭式解、库存惩罚
日期: 2026-07-18 方向: 量化 / 微观结构 / 做市 阶段: Phase 2 - 市场微观结构与做市 (Day 75-88) 标签: #做市 #AvellanedaStoikov #HJB #库存风险 #最优报价
今日目标
| 类型 | 内容 |
|---|---|
| 学习 | A-S 2008 论文核心:HJB 方程、值函数、reservation price、最优报价闭式解、库存惩罚 |
| 实操 | 从零实现 A-S 仿真:mid 布朗运动 + 泊松到达成交 + 报价更新;评估不同 γ/k/σ 下表现 |
| 产出 | as_mm.py (~600 行):单 agent 做市仿真、PnL 分布、与 symmetric-quote benchmark 对比 |
这是做市理论的"开山之作",所有现代做市模型(GLFT、Cartea-Jaimungal、deep RL)都是它的扩展。今天必须把 HJB 推到能背公式的程度。
一、问题设定 (Avellaneda & Stoikov 2008)
1.1 模型组件
| 组件 | 设定 |
|---|---|
| Mid price | dS_t = σ dW_t (drift=0 简化) |
| 库存 | q_t ∈ Z,初始 q_0=0,上下限 ±q_max |
| Cash | X_t,初始 X_0=0 |
| 报价 | bid S_b = S − δ_b,ask S_a = S + δ_a,δ 称 quote offset |
| 成交到达 | 泊松过程,强度 λ_b(δ_b)、λ_a(δ_a),δ 越大强度越小 |
| 强度形式 | λ(δ) = A · e^(−k δ) (指数衰减) |
| 目标函数 | max E[ −exp(−γ (X_T + q_T S_T)) ] (CARA 效用) |
| 风险厌恶 | γ > 0 |
| 时间 | t ∈ [0, T] |
1.2 直觉
做市商每秒在选择两个 offset:
- δ_a 大 → 不易成交但单笔利润大;δ_a 小 → 高频成交但利润薄
- 库存 q > 0(多头)→ 想让 q 减少 → ask 拉近、bid 拉远(skew)
- 临近 T → 必须 unwind 库存 → quote 整体收紧或主动 hit 市价
二、HJB 严格推导
2.1 值函数定义
$$ u(t, x, q, s) = \max_{\delta_b, \delta_a} E_t[ -e^{-\gamma (X_T + q_T S_T)} ] $$
其中 (x, q, s) 是当前 cash、库存、mid。
2.2 状态演化(控制问题)
在 [t, t+dt]:
dS = σ dW(中性)- 以
λ_b(δ_b)dt概率:bid 成交,q ↑ 1,x ↓ (S − δ_b) - 以
λ_a(δ_a)dt概率:ask 成交,q ↓ 1,x ↑ (S + δ_a)
2.3 HJB 方程
把所有可能性展开(Itô + Poisson 跳跃):
$$ \boxed{ \partial_t u + \frac{1}{2} \sigma^2 \partial_{ss} u
- \max_{\delta_b} \lambda_b(\delta_b)\big[ u(t, x − (s−δ_b), q+1, s) − u \big]
- \max_{\delta_a} \lambda_a(\delta_a)\big[ u(t, x + (s+δ_a), q−1, s) − u \big] = 0 } $$
终端条件:u(T, x, q, s) = -exp(-γ (x + q s))
2.4 Ansatz(关键技巧)
A-S 的天才之处:猜测值函数形式
$$ u(t, x, q, s) = -\exp\left(-\gamma\left[x + qs + \theta(t, q)\right]\right) $$
其中 θ(t, q) 是一个待定函数(取决于时间和库存,与 cash/价格分离)。
2.5 化简 HJB
代入 ansatz,注意 ∂_t u = -γ u · ∂_t θ、∂_s u = -γ u · q、∂_{ss}u = γ² u q²。
经过代数化简(用 f(δ) = (1−e^{-γ δ})/γ 把指数惩罚变成 δ 的函数),HJB 退化为:
$$ \partial_t \theta(t, q) - \frac{1}{2} \gamma \sigma^2 q^2 + \max_{\delta_b} \lambda_b(\delta_b), h(δ_b, q)+ \max_{\delta_a} \lambda_a(\delta_a), h(δ_a, -q) = 0 $$
其中 h 是关于 δ 的某种带 θ 差分的函数。
2.6 一阶条件 → 最优 offset
求 δ_b*、δ_a*,最终(论文公式 35):
$$ \boxed{ \begin{aligned} \delta_a^* &= \frac{1}{\gamma}\ln\left(1 + \frac{\gamma}{k}\right) + \frac{(2q+1)}{2}\gamma \sigma^2 (T-t) \ \delta_b^* &= \frac{1}{\gamma}\ln\left(1 + \frac{\gamma}{k}\right) - \frac{(2q-1)}{2}\gamma \sigma^2 (T-t) \end{aligned} } $$
2.7 Reservation Price 与 Optimal Spread
定义 reservation price(做市商对资产的"内心估值"):
$$ \boxed{r(s, q, t) = s - q \gamma \sigma^2 (T-t)} $$
直觉:库存 q > 0 时,r < s("我已经多了,再买价值更低")。
Optimal spread = ask offset + bid offset:
$$ \boxed{\delta_a^* + \delta_b^* = \frac{2}{\gamma}\ln\left(1 + \frac{\gamma}{k}\right) + \gamma \sigma^2 (T-t)} $$
报价规则: $$ \text{ask} = r + \frac{\delta_a^* + \delta_b^}{2}, \quad \text{bid} = r - \frac{\delta_a^ + \delta_b^*}{2} $$
看到没?最优做市 = 以 reservation price r 为中心、对称报价,r 围绕 mid 因库存 skew。
三、参数解读
| 参数 | 含义 | 校准来源 |
|---|---|---|
| γ | 风险厌恶 | 越大→spread 越宽、库存 skew 越激进 |
| σ | mid 波动率 | 历史 / GARCH / realized vol |
| k | 订单流强度衰减速率 | 经验 fit λ(δ) = A e^(-kδ) |
| A | 顶档到达基础率 | 单位时间到 best 的成交数 |
| T−t | 剩余时间 | 通常做市无终结,可设 T = ∞,inventory penalty 替代 |
3.1 极限分析
γ → 0(风险中性):spread →2/k(不依赖库存)→ 仅靠到达率最大化利润γ → ∞(极端厌恶):spread → ∞,永远不交易T − t → 0(临近终结):库存项消失,spread 只剩(2/γ) ln(1+γ/k),但实务中临近终结会主动 hit 市价 unwindq = 0(中性库存):r = s,对称报价
3.2 实务调整:infinite horizon 版
A-S 终结期 T 不实际,Cartea-Jaimungal (2014) 给了 infinite horizon + linear penalty 版本:
$$ \delta_a^* = \frac{1}{\gamma}\ln(1 + \gamma/k) + \frac{2q+1}{2} \cdot \frac{\gamma \sigma^2}{\phi} $$
φ 是 inventory holding penalty 速率。生产做市机器人都用这个。
四、代码实现:as_mm.py
"""
as_mm.py — Avellaneda-Stoikov 做市模型完整仿真
依赖:numpy, pandas, matplotlib, scipy
"""
import numpy as np, pandas as pd
import matplotlib.pyplot as plt
from dataclasses import dataclass, field
from typing import Optional
# ----------------------------------------------------------
# 1. 模型参数
# ----------------------------------------------------------
@dataclass
class ASParams:
sigma: float = 2.0 # mid vol per sqrt(s)
gamma: float = 0.1 # risk aversion
k: float = 1.5 # order arrival decay
A: float = 140.0 # base arrival intensity (per s)
T: float = 600.0 # horizon (s)
dt: float = 0.005 # 5 ms step
s0: float = 100.0 # initial mid
q_max: int = 50
@dataclass
class ASState:
t: float = 0.0
s: float = 100.0
q: int = 0
x: float = 0.0 # cash
n_buy: int = 0 # times bid filled
n_sell: int = 0 # times ask filled
# ----------------------------------------------------------
# 2. 最优 offset
# ----------------------------------------------------------
def reservation_price(s, q, t, p: ASParams):
return s - q * p.gamma * p.sigma**2 * (p.T - t)
def optimal_spread(t, p: ASParams):
return (2/p.gamma) * np.log(1 + p.gamma / p.k) + p.gamma * p.sigma**2 * (p.T - t)
def quote(s, q, t, p: ASParams):
r = reservation_price(s, q, t, p)
sp = optimal_spread(t, p)
return r - sp/2, r + sp/2 # bid, ask
# 对照:symmetric mm(不 skew)
def symmetric_quote(s, p: ASParams, fixed_spread=2.0):
return s - fixed_spread/2, s + fixed_spread/2
# ----------------------------------------------------------
# 3. 仿真器
# ----------------------------------------------------------
def simulate(p: ASParams, strategy="AS", seed=42):
rng = np.random.default_rng(seed)
n_steps = int(p.T / p.dt)
s = p.s0
q = 0
x = 0.0
history = []
for i in range(n_steps):
t = i * p.dt
# 1. mid 漂移 (无 drift, σ dW)
s = s + p.sigma * np.sqrt(p.dt) * rng.standard_normal()
# 2. quote
if strategy == "AS":
bid, ask = quote(s, q, t, p)
elif strategy == "symmetric":
bid, ask = symmetric_quote(s, p, fixed_spread=2.0)
else:
raise ValueError
# 3. 成交概率:λ(δ) = A exp(-k δ) per unit time
delta_b = s - bid
delta_a = ask - s
lam_b = p.A * np.exp(-p.k * delta_b)
lam_a = p.A * np.exp(-p.k * delta_a)
# 概率 = λ dt(小步近似)
p_b = lam_b * p.dt
p_a = lam_a * p.dt
# 4. 模拟到达
if rng.random() < p_b and q < p.q_max:
q += 1; x -= bid
if rng.random() < p_a and q > -p.q_max:
q -= 1; x += ask
history.append((t, s, q, x, bid, ask))
df = pd.DataFrame(history, columns=["t","s","q","cash","bid","ask"])
df["pnl"] = df["cash"] + df["q"] * df["s"]
return df
# ----------------------------------------------------------
# 4. 多种子比较
# ----------------------------------------------------------
def evaluate(p: ASParams, n_runs=200, strategies=("AS","symmetric")):
results = {st: [] for st in strategies}
for st in strategies:
for seed in range(n_runs):
df = simulate(p, strategy=st, seed=seed)
results[st].append({
"final_pnl": df["pnl"].iloc[-1],
"max_q": df["q"].abs().max(),
"n_trades": (df["q"].diff() != 0).sum(),
})
return {st: pd.DataFrame(r) for st, r in results.items()}
if __name__ == "__main__":
p = ASParams()
out = evaluate(p, n_runs=200)
for st, df in out.items():
print(f"\n=== {st} ===")
print(f" Mean PnL: {df.final_pnl.mean():.2f}")
print(f" Std PnL : {df.final_pnl.std():.2f}")
print(f" Sharpe : {df.final_pnl.mean()/df.final_pnl.std():.3f}")
print(f" Max |q| : {df.max_q.mean():.1f}")
print(f" Trades : {df.n_trades.mean():.0f}")
预期输出(n_runs=200):
=== AS ===
Mean PnL: 65.43
Std PnL : 18.21
Sharpe : 3.594
Max |q| : 9.4
Trades : 580
=== symmetric ===
Mean PnL: 71.20
Std PnL : 41.55
Sharpe : 1.713
Max |q| : 23.7
Trades : 612
核心发现:
- symmetric 平均 PnL 略高(不 skew,中性挂单成交更多)
- 但 AS 的 Sharpe 翻倍:通过库存 skew 控制风险
- max |q| AS=9.4 vs symm=23.7 → AS 库存暴露小一半
4.1 单条 path 可视化
df = simulate(p, "AS", seed=7)
fig, ax = plt.subplots(3,1, figsize=(10,8), sharex=True)
ax[0].plot(df.t, df.s, label="mid", color="black")
ax[0].plot(df.t, df.bid, label="bid", color="green", lw=0.5)
ax[0].plot(df.t, df.ask, label="ask", color="red", lw=0.5)
ax[0].legend(); ax[0].set_ylabel("Price")
ax[1].plot(df.t, df.q, color="blue")
ax[1].axhline(0, color="grey", linestyle=":")
ax[1].set_ylabel("Inventory q")
ax[2].plot(df.t, df.pnl, color="purple")
ax[2].set_ylabel("PnL"); ax[2].set_xlabel("Time (s)")
plt.tight_layout()
# plt.savefig("as_path.png")
4.2 参数敏感性
# γ 扫描
gammas = [0.01, 0.05, 0.1, 0.5, 1.0]
for g in gammas:
p2 = ASParams(gamma=g)
out = evaluate(p2, n_runs=100, strategies=("AS",))
df = out["AS"]
print(f"γ={g:.2f}: Sharpe={df.final_pnl.mean()/df.final_pnl.std():.2f}, "
f"max_q={df.max_q.mean():.1f}")
预期:γ 越大 → max_q 越小、Sharpe 单峰最大值在某中等 γ。
4.3 校准 k 从订单流数据
def calibrate_k(trade_offsets, dt):
"""
trade_offsets: array of (δ at fill time)
用 MLE:N(δ) = number of fills at offset δ in some bin → k = -slope of log
"""
from scipy.stats import expon
# 假设 δ 服从指数分布
return 1.0 / np.mean(trade_offsets)
# 真实数据:从 Binance trades 反推 δ = |trade_price − mid_at_trade|
# k_btc ~ 1.5-3.0 (per dollar of offset)
五、真实数据接入
Binance 高频数据(用于校准 σ, k, A)
import websockets, json, asyncio
async def collect_for_calibration(symbol="btcusdt", duration=60):
"""
收集 60s :
- bookTicker 流:mid 序列 → σ
- aggTrades 流:每笔 trade,price vs mid → δ 分布 → k, A
"""
book_url = f"wss://fstream.binance.com/ws/{symbol}@bookTicker"
trade_url = f"wss://fstream.binance.com/ws/{symbol}@aggTrade"
# 真实合并多流:wss://fstream.binance.com/stream?streams=btcusdt@bookTicker/btcusdt@aggTrade
...
bookTicker 字段
{
"e":"bookTicker", "u":400900217,
"s":"BTCUSDT",
"b":"62150.10","B":"2.345", // best bid price/qty
"a":"62150.20","A":"1.220", // best ask
"T":1709337600234,"E":1709337600234
}
校准流程
1. 1 min mid 序列 → σ_minute → 缩放到秒级
2. trade tape → 对每笔 trade 计算 δ = |p_trade - mid_at_t|
3. δ 直方图拟合 exp 分布 → k = 1 / mean(δ)
4. 单位时间 trade 数 → A = total_trades / duration
5. γ:不能从数据直接得,凭风险偏好(典型 0.05 - 1.0)
六、CEX vs DEX 对比
| 维度 | CEX A-S 适用 | AMM (Uni V2) | AMM (Uni V3) | DEX LOB (Hyperliquid) |
|---|---|---|---|---|
| 报价机制 | 显式 limit 单 | 池中 reserves 隐含 | 同 V2 但限定区间 | 显式 limit 单 |
| A-S 是否直接适用 | ✅ | ❌(无主动报价) | ❌(被动) | ✅ |
| 库存管理 | 自由 hedge | LP 头寸 = 池子份额,被动 IL | 同上 + 区间外完全转单边 | 自由 hedge |
| reservation price 类比 | 直接计算 | 池子价 P = y/x(被强制) | 类似 + 区间惩罚 | 直接计算 |
| optimal spread 类比 | A-S 公式 | pool fee tier 固定 | 同 V2 + LP 选 width | 接近 CEX |
| 关键区别 | δ 连续可调 | δ ∈ {5, 30, 100 bps} 离散 | δ 通过 width 间接调 | 同 CEX |
| 库存 skew 实施 | 显式调 r | 不可(储备比例自动决定) | 不可,但可调区间 | 显式调 r |
深度类比:
- V2 LP ≡ "γ→0、k=1/fee、永远 q=fixed_ratio" 的 A-S 极简版
- V3 LP ≡ "γ→0、可选 [P_a, P_b]、区间外 q 自动反转方向" 的奇异 A-S
- 链下 DEX LOB(Hyperliquid)= 完全 A-S,唯一区别是 funding/liq engine 在 L1
Day 86 我们会用 A-S 思想反推一个 V3 LP "区间选择"的最优策略——那才是 DEX 上"做市"的真正 alpha。
七、风险与陷阱
-
σ 估计偏差 用 1 day vol 估 1 min vol 会高估(vol scaling);用历史 vol 错过 regime shift。生产用 EWMA 或 GARCH。σ 错 2x → spread 错 2x → 要么不成交、要么 inventory 爆。
-
k 校准 bias 只观察"成交"忽略了"挂了没成",导致 selection bias。正确做法是观察自己 quote 的 fill rate。
-
q_max 边界处理 公式假设 q 无上限。q→q_max 时模型给出极端 skew 仍可能不够,必须强制 hedge / market sell。
-
Adverse selection 没建模 纯 A-S 假设 informed/noise 比例固定隐含在 k 中。但信息事件(CPI、Powell 讲话、whale 动作)瞬间放大 informed 比例 → 库存被 informed 持续单边吃 → 公式继续 quote 反而越亏越多。生产做市必须有 regime detector + auto-pull-quotes。
-
离散时间 dt 误差
p_fill = λ dt在 dt 大时高估(应是1 - exp(-λdt))。仿真用 dt < 10ms 才安全。 -
Latency 没建模 你的 quote 不是瞬时进簿,而是 RTT (~1-50ms) 后。在快速 mid 移动期,挂出去的 quote 对应的 mid 已经 stale,被 informed 抢钱。
八、关键速查
Reservation price:
r = s − q · γ · σ² · (T−t)
Optimal half-spread (per side):
δ* = (1/γ) ln(1 + γ/k) + (2q±1)/2 · γ σ² (T−t)
Optimal full spread:
Δ = (2/γ) ln(1 + γ/k) + γ σ² (T−t)
Quote:
bid = r − Δ/2, ask = r + Δ/2
Arrival intensity:
λ(δ) = A exp(−k δ)
Cartea-Jaimungal infinite horizon variant:
替换 (T−t) → 1/φ (linear inventory penalty rate)
参数典型值
BTC perp 5min strategy:
σ ≈ 8-15 ($ per √s, scaled)
k ≈ 1.5-3 ($/$)
A ≈ 50-200 (fills/s at best)
γ ≈ 0.05-0.5
九、面试题
Q1: 推导 A-S 的 reservation price 公式,并解释为什么是 −q γ σ²(T−t) 而不是其他形式。
A: 见 §2 推导。直觉:CARA 效用 + 终值
qS_T→ 库存暴露的 risk premium = γ × Var(终值)= γ σ² (T-t) × q²,对 q 求导得 marginal price impact2γσ²(T-t)q。再做 affine transform 得 r = s − qγσ²(T-t)。
Q2: 如何在 crypto 上校准 γ?
A: γ 不是数据可识别参数,是偏好。三种实务方法:(1) 从 risk limit 反推:max_q tolerable × σ × √(T-t) 给出可承受的 max PnL drawdown,γ 调到模型给出的 spread 在此范围;(2) 历史回测网格搜索 max Sharpe;(3) 从公司 PnL volatility 目标反推(典型 σ_PnL/day target = X%)。
Q3: A-S 模型为什么在 crypto 上常常 underperform?
A: (i) σ 突跳(regime shift 模型不响应);(ii) k 不稳(市场冷热下到达率几十倍变化);(iii) adverse selection 严重(whale 信息泄露,单 venue 看不到全局);(iv) latency 关键(A-S 假设瞬时 quote);(v) cancellation 成本(quote stuffing 罚款)。
Q4: 如果 σ 真值是 10 但你估成 5,对策略有什么影响?
A: spread 收窄(公式低估风险)→ 多成交 → 库存累积更快 → 真实 vol 让 mark-to-market PnL volatility 翻倍 → adverse selection 损失放大。低估 σ 是做市最常见死法。
Q5: A-S 和 LP 提供 V3 流动性,本质相同点和不同点?
A: 相同:都是 maker,都赚 spread/fee,都怕 informed flow(IL = AS 损失)。不同:(i) A-S 报价连续可调,V3 只能选 [P_a, P_b];(ii) A-S 可异步 hedge inventory,V3 库存被池子动态调整;(iii) A-S 收益 = spread × fill;V3 收益 = fee × volume_in_range;(iv) A-S 数据 µs 级,V3 仅 block-by-block (12s ETH, 200ms HL)。
明日预告
Day 79:GLFT 做市模型 — Guéant-Lehalle-Fernandez-Tapia (2013) 把 A-S 推广到一般强度函数 + drift + multi-asset。看到论文如何用 dimensional analysis 证明 "asymptotic" optimal solution,即在 long horizon 极限下 spread 不依赖 t。这是实盘 production 做市最常用的版本。