4、经典做市模型:Avellaneda-Stoikov模型推导与实现

做市策略的核心,说白了就是回答两个问题:报什么价?报多少量?

Avellaneda-Stoikov模型(以下简称A-S模型)是我个人非常推崇的一个经典框架。它不像某些黑箱模型那样让人摸不着头脑,而是给出了一个清晰的数学推导路径。我在2018年做期权做市时,第一次把A-S模型落地到实盘,效果比纯手工报价好了不止一个档次。

4.1 模型的核心思想

A-S模型把做市商看作一个风险厌恶的库存管理者。你想想看,做市商本质上是在赚买卖价差,但手里囤了一堆货(库存)是有风险的。价格一波动,库存价值就跟着变。

模型假设:

  • 做市商的目标是最大化期望效用,而不是最大化期望收益
  • 效用函数采用指数形式:U(x) = -exp(-γx),其中γ是风险厌恶系数
  • 库存s服从一个扩散过程,受成交影响

嗯,这里要注意:指数效用函数的好处是它能把问题简化成一个线性形式。我在项目中遇到过有人用二次效用函数,结果推导出来的报价公式复杂到没法实时计算。

4.2 模型推导过程

推导其实不复杂,我们一步步来。

4.2.1 基本设定

假设做市商在买卖两边同时报价:

  • 买价:p_b = S - δ_b
  • 卖价:p_a = S + δ_a

其中S是中间价,δ_b和δ_a是报价偏移量。库存s的变化由成交决定:

ds = -dN_b + dN_a

N_b和N_a是买卖成交的泊松过程,强度取决于报价偏移量:

λ_b(δ) = A * exp(-k * δ)
λ_a(δ) = A * exp(-k * δ)

这个强度函数很有意思。A是最大到达率,k是订单流对价格的敏感度。k越大,说明市场越敏感,你稍微偏离一点中间价,成交就大幅减少。

4.2.2 价值函数与HJB方程

做市商的价值函数V(t, s)满足HJB方程:

∂V/∂t + max_{δ_b, δ_a} [λ_b(δ_b) * (V(t, s-1) - V(t, s) + δ_b) 
    + λ_a(δ_a) * (V(t, s+1) - V(t, s) + δ_a)] = 0

边界条件:V(T, s) = -exp(-γ * s * S_T),到期时库存按市价清算。

说实话,第一次看到这个方程我也有点头大。但别慌,A-S模型给出了一个漂亮的解析解。

4.2.3 解析解

通过变量分离和泰勒展开,可以得到:

δ_b(s, t) = (1/γ) * ln(1 + γ/k) + γ * σ² * (T-t) * s
δ_a(s, t) = (1/γ) * ln(1 + γ/k) - γ * σ² * (T-t) * s

其中:

  • γ:风险厌恶系数
  • σ:波动率
  • T-t:剩余时间
  • s:当前库存(多头为正,空头为负)

核心洞察:报价偏移量由两部分组成——一个常数项和一个与库存相关的动态项。常数项决定了基础价差宽度,动态项则根据库存方向调整报价。

4.3 库存风险厌恶系数γ

γ是整个模型里最关键的参数。它决定了做市商对库存风险的容忍度。

我个人习惯这样理解γ:

  • γ越大 → 越厌恶风险 → 报价偏移量越大 → 价差越宽
  • γ越小 → 越能承受风险 → 报价偏移量越小 → 价差越窄
γ取值 风险偏好 典型场景
0.01 - 0.05 激进 高流动性市场,如BTC永续合约
0.05 - 0.2 中性 主流股票、主流币种
0.2 - 1.0 保守 低流动性市场,如小市值代币

我曾经在ETH-USDT的做市策略里把γ设成0.1,结果库存一大了就疯狂调价,成交率暴跌。后来改成0.03,效果好了很多。说白了,γ的调参是个经验活,没有银弹。

4.4 最优报价偏移量计算

有了公式,实现起来就简单了。下面是我常用的Python实现:

import numpy as np

class AvellanedaStoikov:
    def __init__(self, gamma=0.1, k=1.5, sigma=0.02, T=1.0):
        """
        gamma: 风险厌恶系数
        k: 订单流敏感度
        sigma: 波动率
        T: 做市周期(年化)
        """
        self.gamma = gamma
        self.k = k
        self.sigma = sigma
        self.T = T
        
    def compute_spread(self, s, t):
        """
        s: 当前库存(正数表示多头)
        t: 当前时间(0到T之间)
        返回: (买价偏移, 卖价偏移)
        """
        # 常数项:基础价差的一半
        base_offset = (1.0 / self.gamma) * np.log(1 + self.gamma / self.k)
        
        # 动态项:库存调整
        inventory_adjust = self.gamma * (self.sigma ** 2) * (self.T - t) * s
        
        delta_b = base_offset + inventory_adjust
        delta_a = base_offset - inventory_adjust
        
        return delta_b, delta_a
    
    def get_quotes(self, mid_price, s, t):
        """返回实际报价"""
        delta_b, delta_a = self.compute_spread(s, t)
        bid = mid_price - delta_b
        ask = mid_price + delta_a
        return bid, ask

实战技巧:我在实盘中会把T设成0.25(3个月),而不是1年。因为做市商的库存周期通常很短,用太长的周期会导致库存调整过于缓慢。

4.5 模型的核心逻辑图

下面这张图展示了A-S模型的完整工作流程:

Avellaneda-Stoikov 做市模型核心逻辑 市场参数 σ, k, A 做市商参数 γ, T 状态变量 s, t HJB方程求解 max[λ_b(δ_b)·(ΔV+δ_b) + λ_a(δ_a)·(ΔV+δ_a)] 解析解 δ_b = (1/γ)·ln(1+γ/k) + γ·σ²·(T-t)·s 最优报价 买价 = S - δ_b,卖价 = S + δ_a 库存更新反馈

4.6 避坑指南

我曾经在实盘里踩过几个坑,分享出来:

坑1:γ设得太大

我曾经把γ设成0.5,结果报价偏移量巨大,价差宽到没人来成交。做市商变成了「只报价不成交」的摆设。建议从0.01开始逐步调大。

坑2:忽略波动率变化

A-S模型假设σ是常数。但实际市场波动率会突变。我建议用滚动窗口估计σ,窗口长度取20-50个tick。

坑3:库存s的符号搞反

这个错误很基础但很容易犯。多头库存s为正时,买价应该降低(δ_b增大),卖价应该提高(δ_a减小)。公式里已经体现了这个逻辑,但实现时一定要检查符号。

4.7 小结

A-S模型是经典中的经典。它把做市问题转化成了一个带解析解的优化问题,这在量化金融里非常难得。你想想看,大部分策略模型都需要数值求解,而A-S模型直接给你一个公式,这在低延迟场景下简直是福音。

不过也要清醒认识到,A-S模型有它的局限性:

  • 假设订单流是泊松过程,实际市场有聚集效应
  • 假设价格是几何布朗运动,实际有跳跃
  • 没有考虑对手方策略的博弈

但这些都不妨碍它成为做市策略的基石。我个人建议,新手做市商先把A-S模型跑通、跑稳,再考虑加各种花哨的改进。


公众号:蓝海资料掘金营,微信deep3321