做市的本质从来不是预测方向,而是把「流动性」这件事当成商品来定价。买方愿意为「立刻成交」付出溢价,卖方愿意为「立刻脱手」付出折让,两者之差就是做市商收的钱。这个钱看起来好赚——挂两个被动单,价差就到手——但任何在真实订单簿上挂过几天的人都知道,被动成交是有方向的:当价格要往上走时,你的卖单先被打掉;当价格要往下走时,你的买单先被打掉。统计上看,做市商的库存几乎总是「逆着即将到来的价格运动」。这就是逆选择(adverse selection),它把一份看似稳定的价差收入变成一种与信息流博弈的高维风险敞口。
把这件事写成可执行策略,就要回答三个问题:报价应该挂在哪里?挂多远?什么时候撤? Avellaneda-Stoikov 在 2008 年给出了一个有解析形式的答案:在风险厌恶 γ、波动率 σ、剩余时间 T−t、当前库存 q 的联立下,最优买卖报价偏移可以从一个 Hamilton-Jacobi-Bellman(HJB)方程闭式解出。这套结果至今仍是教科书里讲做市的标准切入点,工业界用来生产报价的引擎大多在它的骨架上做加法:库存软硬限制、订单流毒性监控、跨所对冲、撤单频率约束、监管层面的「不构成操纵」证据链。
上一篇《智能订单路由》解决的是「我要主动吃流动性,怎么吃最便宜」;这一篇要解决的是反向问题——「我要被动供给流动性,怎么报价才能不被吃穿」。这两类策略在订单簿的两侧相遇,构成了真实市场最重要的微观博弈。
代码示例使用 Python 3.11、numpy 1.26、scipy 1.11、pandas 2.2。所有数据均为本地仿真,不涉及任何具体场所或对手方的真实订单簿数据。
风险提示:本文出现的所有公式、阈值、参数仅用于阐释做市策略的方法论本身,不构成任何投资或交易建议。Avellaneda-Stoikov 模型基于扩散过程与指数到达强度的强假设,真实市场的肥尾、跳跃、订单簿事件聚集与做市商竞争都会显著偏离模型;任何由此推导的报价偏移、库存阈值、撤单条件都不能保证盈利。把示例代码搬到生产前,必须重新核对:风险参数 γ 是否与组合层 VaR 限额一致、波动率估计是否使用 PIT 数据、对冲腿是否真实可成交、监管对「报价不真实成交意图」(spoofing)的认定标准是否覆盖你的撤单频率与挂单距离。做市需要的注册资质、做市商协议、做市商义务在不同市场差异巨大,本文不替代任何合规审核。
一、做市的经济学
把做市抽象到最小,市场上有三类参与者:信息交易者(informed trader)、噪声交易者(noise trader / liquidity trader)、做市商(market maker)。前两者主动消耗流动性,做市商被动提供流动性。信息交易者掌握短期内会让价格偏移的信息,他们的成交对做市商而言是有毒的——刚卖出去价格就涨、刚买回来价格就跌。噪声交易者只是因为现金流、再平衡、随机偏好需要交易,他们的成交对做市商而言是干净的,平均下来不带方向。
做市商收益的来源很清楚:对噪声订单流收一份「即时性溢价」——也就是买卖价差的一半,行话叫 half spread。但同样这份价差,在面对信息订单流时变成「逆选择税」——做市商必然在错误的时点接盘。这两份现金流的代数和,加上库存持有期间价格漂移带来的 mark-to-market,再扣掉撤单费、基础设施、托管资金的资金成本,才是做市策略真正的 PnL。
一点一、买卖价差的三个组成部分
经典分解(Stoll 1989;Huang & Stoll 1997)把任何挂单的最小价差拆成三块:
- 指令处理成本(order processing cost):交易所撮合费、清算费、技术基建摊销。这部分是确定的,量大就能摊薄。
- 库存持有成本(inventory holding cost):做市商被动接到一笔单后,在脱手之前承担价格波动风险。这部分与 σ²、持有时间、库存上限的乘积成正比。
- 逆选择成本(adverse selection cost):与信息交易者博弈的预期损失。这部分由订单流毒性决定,事前不可观测,事后才能从「成交后短期价格漂移」反推出来。
任何一个做市商若把价差报得低于这三项之和,长期一定亏钱。把价差报得高于这三项之和,又会被竞争对手抢单——别人挂得更窄就先成交,你长期挂在最优一档之外,库存周转率(inventory turnover)下降,规模就上不去。这是一个典型的 width-vs-fill-rate 权衡,做市策略的全部工程就是在这个权衡上做局部最优。
一点二、为什么必须谈库存
如果做市商的资金无限、风险偏好是中性的、对每一笔成交都能瞬时对冲到外部市场,那么库存就不重要——只要价差 > 三项之和,就闭着眼挂单。但真实世界里:
- 资金不是无限的:交易所的初始保证金、维持保证金、风控限额给库存设了硬上限。
- 风险偏好不是中性的:基金经理、做市台、风控部都对单边敞口设软上限。
- 对冲不是瞬时的:你想去另一个交易所对冲,要付那边的吃单费、要承担跨所滑点、要等链上确认(加密资产里很要命)。
这三件事合起来,意味着库存 q 必须进入定价函数。库存为零的时候报价对称,库存正向(长仓)的时候卖价靠近中间价、买价远离中间价(鼓励减仓),库存负向(短仓)反之。这个「报价随库存对称偏移」的直觉,就是 Avellaneda-Stoikov 模型的核心。
一点三点五、做市与做市策略的术语区分
中文语境里「做市」和「做市策略」常常混用,但工程上要分清:
- 做市(market making)作为业务:指持续在买卖双方挂单提供流动性这件事本身。它可以由专门的做市商团队做、也可以是某个量化基金的子策略。
- 做市作为监管身份:在某些场所(NYSE DMM、Nasdaq Market Maker、Cboe Lead Market Maker、欧洲 MiFID II 注册做市商)需要正式注册,并承担最低报价时间、最大价差、最低尺寸等义务。
- 做市策略(market-making strategy):在算法层面挂双边被动单的具体实现,可以由非注册做市商身份的团队跑(不享激励、也不承担义务)。
这三件事的法律、工程、合规要求差别很大。本文讨论的是第三件——算法层面的策略实现——但落地时必须先在前两件事上把身份与义务确认清楚。
一点四、做市规模与流动性深度的关系
做市策略不能脱离规模谈。同一份价差与库存模型,在每秒撮合 10 笔的小盘股上和每秒撮合 10 万笔的主流加密永续上,需要的工程和数学完全不同:
- 小盘流动性低:一次成交就把库存从 0 推到上限,逆选择损失占主导。这种场景做市更像「挂单等套利」,对模型的精度要求其实没那么高,对何时不挂的判断更重要。
- 大盘流动性高:一秒钟可能成交几十次,库存波动是连续的,可以用扩散过程逼近。Avellaneda-Stoikov 这一类基于 SDE 的模型在这里才能真正用上。
- 极高频做市:撮合周期接近交易所匹配引擎的最小时间粒度(微秒到百微秒),模型的解析解已经不重要,决定竞争力的是排队(queue position)、延迟(latency)、撤改频率、共置(co-location)。这部分留给下一篇《HFT 架构》展开。
本文的位置在第二档:把模型讲清楚、给出能跑的实现,但不假装能直接覆盖纯延迟竞争的场景。
二、Glosten-Milgrom:信息不对称下的报价
Glosten 与 Milgrom 在 1985 年提出了一个极简但极有解释力的做市模型。把它当成 Avellaneda-Stoikov 的「微观基础」来看最合适——它告诉我们「为什么必须有一个买卖价差」,AS 再告诉我们「这个价差在动态库存下应该是多少」。
二点一、模型设定
资产真实价值 V 是随机变量,做市商不知道它具体是多少,只知道先验分布。市场上每一笔来单是这样产生的:
- 以概率 α 是信息交易者,他知道 V 的真实值。如果 V > 当前 ask,他会买;如果 V < 当前 bid,他会卖。
- 以概率 1−α 是噪声交易者,他随机买或卖(各 50%)。
做市商挂出 bid 和 ask,每来一笔单就更新对 V 的后验估计——一笔买入到达本身就是「V 偏高」的贝叶斯证据,一笔卖出到达就是「V 偏低」的证据。
二点二、零利润均衡
在竞争性做市环境(多家做市商相互压价直到边际利润为零)下,做市商挂出的 ask 必须等于「在收到一笔买单后对 V 的后验期望」,bid 必须等于「在收到一笔卖单后的后验期望」:
ask = E[V | 来单是买入]
bid = E[V | 来单是卖出]
这是一个非常深刻的结果:价差不是为了给做市商赚钱,而是为了把信息不对称的成本转嫁给主动方。每一个吃单的人都付出了「他这一笔单本身泄漏出去的信息」的费用。
二点三、价差的代数表达
把贝叶斯公式展开,记 V 取两个可能值 V_H 和 V_L(先验概率各 0.5),可以推出:
spread = ask − bid = α · (V_H − V_L)
价差只与「信息交易者占比 α」和「真实价值的不确定性 V_H − V_L」相乘成正比。这正好对应做市价差里的逆选择那一块——其余两块(订单处理、库存持有)在 GM 模型中是被忽略的(因为 GM 假设做市商风险中性、瞬时对冲)。
二点四、对工程的启发
GM 给出的不是公式,而是结构性结论:
- 价差是 endogenous——信息越多、不确定性越大,价差越宽,做市商必须把这件事写进定价。
- 每一笔成交都是信号——单边连续成交说明 α 在升高(毒性在升高),价差应该立刻拉宽。这是后面 VPIN 的直接动机。
- 做市商的「正确响应」不是停止报价,而是把价差调到能补偿当前 α 的水平——除非 α 高到价差宽于做市商被允许的最大挂单距离。
GM 模型的局限同样明显:它没有库存、没有时间、没有有限风险厌恶、没有尺寸(每笔单都是 1 单位)。把这些维度补回来就是 Avellaneda-Stoikov。
三、Avellaneda-Stoikov 模型
Avellaneda 与 Stoikov 在 2008 年的论文「High-frequency trading in a limit order book」给出一个连续时间的最优做市问题,并通过 HJB 方程求解。它解决的核心问题是:给定一个有限的做市时段 [0, T],做市商应当把买卖单挂在距离中间价多远的位置,使得期末效用最大?
三点一、模型假设
- 中间价过程:S_t 是布朗运动(无漂移),dS_t = σ dW_t。这是模型最强的假设,意味着信息交易者已经被吸收进 σ 里、不显式区分。
- 做市商挂单:在中间价两侧分别挂出价差 δ⁺(ask 偏移)与 δ⁻(bid 偏移)。挂单价格分别是 S_t + δ⁺ 和 S_t − δ⁻。
- 成交强度:买卖到达过程是 Poisson 强度 λ⁺(δ⁺) 和 λ⁻(δ⁻),挂得越远成交越少。AS 论文里用指数函数 λ(δ) = A · exp(−κδ),A 与 κ 由数据拟合。
- 库存动态:q_t 是当前持仓数(正为多头),每次卖单成交 q 减 1、每次买单成交 q 加 1。
- 效用函数:CARA(指数效用)U(x) = −exp(−γx),γ 是绝对风险厌恶。
- 终止条件:在时刻 T 把所有库存按中间价 S_T 平掉(mark-to-market)。
三点二、值函数与 HJB
定义值函数 u(t, x, q, s),x 是现金、q 是库存、s 是中间价。HJB 方程长这样:
∂u/∂t + (1/2) σ² ∂²u/∂s²
+ max_{δ⁺} λ⁺(δ⁺) [ u(t, x + (s+δ⁺), q−1, s) − u(t, x, q, s) ]
+ max_{δ⁻} λ⁻(δ⁻) [ u(t, x − (s−δ⁻), q+1, s) − u(t, x, q, s) ]
= 0
边界条件:u(T, x, q, s) = −exp(−γ (x + q·s)),即期末把库存平掉的总财富的 CARA 效用。
直接解这个 PDE 困难,AS 用一个标准技巧:把 u 写成 u(t, x, q, s) = −exp(−γx) · exp(−γ θ(t, q, s)),其中 θ 是「reservation」函数。在指数到达强度 λ(δ) = A · exp(−κδ) 与 σ² 的二阶项可线性化的近似下,HJB 变成对 θ 的线性 PDE,得到闭式解。
三点三、Reservation Price 与 optimal spread
AS 给出的两个核心结果:
Reservation price(也叫 indifference price):
r(s, q, t) = s − q · γ · σ² · (T − t)
这是做市商在当前库存 q 下「对资产的私人估值」——把价格在 r 上方卖出、在 r 下方买入,效用上没有差别。库存越多,r 越偏离 s 向下(更愿意卖),库存越空,r 越偏离 s 向上(更愿意买)。
Optimal spread(围绕 reservation price 对称展开):
δ⁺ + δ⁻ = γ σ² (T − t) + (2/γ) · ln(1 + γ/κ)
最终挂出的价格是:
ask* = r + (δ⁺ + δ⁻)/2
bid* = r − (δ⁺ + δ⁻)/2
把 r 展开就能看出库存的作用是把整个报价区间整体平移,而价差宽度只与 γ、σ²、T−t、κ 有关,与库存无关。这是一个非常方便的工程结论:库存控制偏移、市场参数控制宽度,两件事可以解耦。
三点四、几何直觉
把 ask、bid、r、s 画在「价格 vs 库存 q」的坐标系里,得到下面这张图。
- 蓝色横线是中间价 s。
- 紫色斜线是 reservation price r,斜率 −γσ²(T−t)。
- 橙色与绿色平行于紫线,分别对应 ask* 与 bid*。
- 当 q=0 时,r=s,bid 与 ask 关于 s 对称。
- 当 q=+3 时,r 下移,ask 也下移(更主动卖出),bid 同时下移(不那么积极买入);q=−3 反之。
这套机制把「做市商不希望把库存堆到上限」这个朴素直觉,编码进了一个连续可微、可解析、可数值仿真的报价器。
三点四点五、HJB 推导的几步关键代换
很多读者读 AS 论文时卡在 PDE 这一段。把它讲得稍微具体一点,避免只写「闭式解 = 公式」这种空话。
第一步:把 u 写成乘积形式。猜 u(t, x, q, s) = −exp(−γ x) · w(t, q, s)。代入 HJB,时间项与空间项里的 exp(−γ x) 都被提出来,方程退化成对 w 的方程。
第二步:把 w 写成 exp(−γ θ) 形式。再次代换 w(t, q, s) = exp(−γ θ(t, q, s)),把指数效用进一步拉平。这一步之后,对 θ 的方程在「δ⁺、δ⁻」上的最大化变成一个逐点的优化——max 的对象是 λ(δ) · (1 − exp(−γ Δθ)) 这种形式,对 δ 求一阶条件可解。
第三步:解一阶条件。在 λ(δ) = A · exp(−κ δ) 的具体形式下,对 δ 求导得:
δ* = (1/κ) · (1 − γ ∂θ/∂q) + (1/γ) · ln(1 + γ/κ)
加上 reservation 项的代换 ∂θ/∂q ≈ q · σ² · (T−t)(在小 q、对称近似下),就拼出第三点三里的 δ⁺ + δ⁻ 公式。
第四步:边界条件回代。终值 u(T, x, q, s) 给出 θ(T, q, s) = q · s − (γ/2) σ² 的二次形式(CARA 把库存折成 mean-variance),把它代回得到 reservation price 的线性表达。
整个推导的关键不是「数学多难」,而是两次乘性代换把指数效用与 Poisson 强度拉平到对 δ 逐点最大化。理解这一点之后,对模型的几个改动(替换强度函数、引入二次惩罚、加入跳跃过程)都可以沿同一套代换重新展开,不必每次重头做 HJB。
三点五、AS 模型的几个常被忽略的细节
- κ 的标定不是常数:κ = ∂(ln λ)/∂δ 是订单簿对深度变化的敏感度。在不同时段(开盘、午盘、收盘)、不同波动率制度下,κ 完全不同。生产里通常滚动估计、按时段分桶。
- A 与 σ² 在加密里高度时变:A 由近 N 笔成交反推、σ² 由近 N 个 mid-price 变化反推。N 选太短噪声大、太长不及时。常用做法是 EWMA + 最低样本数兜底。
- T−t 的物理意义在永续市场里需要重新解释:AS 原文里 T 是交易日终。永续市场没有「日终」,常见替换是「直到下一次资金费率结算」或「直到风控规定的策略评估窗口」。这个 T 的选取直接影响 reservation 偏移幅度,是工程里最大的隐式参数。
- 离散订单簿对最小价差有约束:算出来的 δ 可能小于一个 tick,必须 round 到最近的合法报价。
- AS 假设没有竞争对手:现实里另一个做市商挂得更窄你就拿不到单。要么把 κ 重新解释成「考虑竞争后的成交强度」,要么把模型推广到 Cartea-Jaimungal 的非对称竞争形式。
四、库存约束与软硬限制
AS 模型给出的库存动态自带「漂移惩罚」(reservation price 偏移),但它有两个工程上不能直接用的弱点:
- q 在数学上可以无限大或无限小,模型里只是用风险厌恶 γ 来约束;但生产里有硬性的资金、保证金、风控上限。
- γ 是「全局」参数,没有把「q 接近上限时应当指数级抑制」这件事编码进去。
实践里几乎所有做市引擎都在 AS 之上加一层库存惩罚(inventory penalty)。常见三种写法。
四点一、二次惩罚(最常用)
在效用函数里直接加一项 −η q²:
U(x, q) = x − γ Var − η q²
η 是「库存厌恶」,量纲是「价格 / 库存²」。它的效果是把 reservation 偏移从「线性在 q」变成「线性在 q + 修正项」,但因为是平方惩罚,q 越大边际成本越高,软性地把库存往零拉。这种写法可以直接代入 AS 框架,闭式解只在常数项上有变化。
四点二、对数屏障(接近上限时强抑制)
在效用里加一项 −β · [−ln(1 − |q|/q_max)]:
U(x, q) = x − γ Var − β · barrier(q)
当 |q| 接近 q_max 时,barrier 趋于无穷大,自动把 reservation 偏移推到极端,从而强制减仓。这种写法的好处是把硬限制 q_max 显式编码进策略,不需要再额外写「q 越界就停止挂买/卖单」的分支逻辑。
四点三、双阈值(工程上最常落地)
更工程化的写法是两条阈值线:
- 软上限 q_soft(例如 q_max 的 60%):在此以内按 AS 报价。
- 软上限到硬上限之间(60%–95%):把同方向的挂单距离按指数级拉宽,反方向距离按指数级收窄;甚至直接撤掉同方向挂单。
- 硬上限 q_max:同方向挂单强制下线,反方向挂单收窄到极致(best bid 或 best ask 上挂 IOC + post-only),力求快速减仓。
对应伪代码:
if abs(q) < q_soft:
delta_same, delta_opp = avellaneda_stoikov(q)
elif abs(q) < q_hard:
scale = exp((abs(q) - q_soft) / (q_hard - q_soft))
delta_same *= scale
delta_opp /= scale
else:
cancel_orders(side=same)
place_aggressive_reduce_only(side=opp)这种「分段」的写法损失了模型的解析连续性,但在生产里可读、可调、可独立测试每一段,远比「调一个 η」更可控。
四点四、动态偏移与时间维度
库存惩罚还有一个常被忽略的时间维度:收盘前的库存比开盘后的库存「更贵」。剩余可交易时间 T−t 越短,相同库存把价格风险落地的窗口越窄,惩罚应当线性甚至超线性放大。AS 公式里 (T−t) 项已经天然带这种效果,但在永续市场用替代时钟时(资金费率结算时点、或风控评估窗口),需要显式地把「距离窗口结束的时间」当成衰减因子写进偏移。
四点五、把库存惩罚标定到组合层风控
η(二次惩罚系数)不是策略层独立可调的旋钮,它必须从组合层的 VaR / ES 限额反推。给定单符号的 1 日 95% VaR 上限 V_lim,可以推出库存上限大约为 V_lim / (1.65 σ_daily · price),再用这个上限反推 η 让大部分时间库存分布的 99 分位数落在上限以内。
工程做法:
- 跑一个不带 η 的 AS 仿真,记录库存分布。
- 把 99 分位数 q_99 与目标上限 q_target 比较,若 q_99 > q_target,按 (q_99 / q_target)² 的比例增大 η,重新仿真直到收敛。
- 把最终 η 写入策略配置,并标注「与 V_lim 联动」。
- 任何时候 V_lim 调整(组合层风控更新),η 必须自动重算,不能手工改。
这样 η 与组合风控形成单向链路——风控改了,策略自动跟随;策略不能反向改 η 来「绕过」风控。这是做市策略与组合层风控解耦的关键纪律。
五、对抗逆选择
AS 模型的世界里,订单流是匀质 Poisson,没有信息含量。真实市场里,订单流的毒性是非平稳的——某几分钟全是信息单,某几分钟全是噪声单。做市策略必须有一个机制,实时检测自己正在与谁交易,并据此动态调整甚至临时下线。
五点一、VPIN 的直觉
VPIN(Volume-synchronized Probability of Informed trading)由 Easley、López de Prado、O’Hara 在 2012 年提出。它的核心思想是:
- 把成交量按等量切成「volume bucket」(例如每个 bucket 50 BTC 的成交量),而不是按时间切。
- 在每个 bucket 内部,估计买方主动成交量 V_buy 与卖方主动成交量 V_sell。
- 计算单 bucket 的不平衡 |V_buy − V_sell| / V,再在最近 N 个 bucket 上做平均。
得到的 VPIN ∈ [0, 1] 反映「最近这一段单边订单流有多极端」。VPIN 高意味着信息交易者集中、做市商面临的逆选择风险高。
五点二、为什么按成交量分桶
按时间分桶在不同流动性时段下意义不同:早盘 1 分钟可能 50 BTC、午盘 1 分钟可能 5 BTC,方差差一个数量级,跨时段不可比。按成交量分桶把 buckets 做成「同等信息密度」的容器,让 VPIN 在不同时段、不同标的之间可比。这一点是 VPIN 相对早期 PIN 模型的最大改进。
五点三、估计 V_buy / V_sell
实际订单流不直接告诉你「这一笔是主动买还是主动卖」,要用规则推断。常见两种:
- Tick rule:本笔成交价高于上一笔则记为主动买,低于则主动卖,相等则继承前一笔标记。简单但在 mid-price 不变时会偏。
- Bulk volume classification(BVC):在 bucket 级别,假设 V_buy / V ≈ Φ((ΔP / σ_ΔP)),用 bucket 内价格变化的标准化值通过正态 CDF 估计买方占比。是 VPIN 论文里推荐的方法。
工业里两种都会跑、互为 sanity check。
五点四、把 VPIN 接入做市引擎
典型的接入方式是三档响应:
- VPIN < threshold_low(例如 0.3):正常报价,按 AS + 库存惩罚走。
- threshold_low ≤ VPIN < threshold_high(0.3–0.6):把价差宽度按 (1 + α·VPIN) 拉大,挂单尺寸按 (1 − β·VPIN) 缩小。
- VPIN ≥ threshold_high(0.6+):临时下线——撤掉所有 maker 单,库存若超过软上限则用 IOC 主动减仓,等 VPIN 回落再重新挂。
阈值的标定需要回测:用历史成交流跑 VPIN,画 VPIN 的 CDF,把 70/95 分位数当作 threshold_low/threshold_high 的初值。生产里再按真实成交后短期价格漂移做事后校验:如果 VPIN > threshold_high 时段成交后的 1 分钟漂移确实显著大于 VPIN 低时段,则阈值有效;否则要重新估。
五点四点五、VPIN 的几个真实问题
VPIN 在论文里被讲得很漂亮,但落到生产里有几件事必须先想清楚。
bucket 体量的选择:bucket_volume 太小,VPIN 在每一笔大单到来时都会跳;太大,VPIN 反应迟钝、跟不上闪崩节奏。一个能用的经验法则是把 bucket_volume 设成「ADV / (期望 N 个 bucket 数)」,N 取 50—200。在波动期,自动按近 1 小时实际成交量重新校准 bucket_volume,否则 VPIN 在低流动性时段会因「半天填不满一个 bucket」而严重滞后。
信号的领先性:原论文声称 VPIN 对 2010 年闪崩有领先信号。后续多篇复现工作(Andersen & Bondarenko 2014)质疑这一说法,认为在去除微观结构噪声之后 VPIN 的领先性弱于报告值。工程上的处理是:不把 VPIN 当成预测信号,只当成「当前订单流毒性」的同期度量——它告诉你「现在挂在这里被宰的概率」,不告诉你「下一秒价格往哪走」。
与 spread 的相关性:高 VPIN 时段往往同时是高 spread 时段,两个信号有冗余。生产里的做法是只在 spread 与 VPIN 同时偏高时才进入「hostile」状态,避免单一信号误触发。
事后校验:每个 hostile 状态都要在一周后回看一次「这段时间的成交后短期漂移是否真的更大」。漂移确实更大就保留阈值;不显著就降低阈值灵敏度,避免做市时间被无效熔断切碎。
五点五、其他毒性度量
VPIN 不是唯一的工具,常见配套还有:
- Order Flow Imbalance(OFI):基于 L2 订单簿事件流的瞬时不平衡,比 VPIN 更高频。
- Trade Sign Autocorrelation:连续主动单同号的程度。高自相关意味着有人在分单(child orders of a large parent)。
- Spread / Depth 比例:spread 拉宽且 depth 变薄通常是做市商集体撤单的信号,自己是这群里的一个,但要识别其他人也在撤。
- Cross-asset signal:同一资产在另一交易所先动,本所做市商应当立刻把价差拉开(latency arbitrage 防御)。
工业做市引擎里这些信号是并联跑的,任何一个触发都进入「降级」模式,由风控层做最终聚合决策。
六、加密做市的特殊性
把 AS 模型搬到加密永续合约上,有几件事不能照搬。
六点一、资金费率与 mark-to-market
永续合约没有交割日,但每 8 小时(币安、OKX)或每 1/4/8 小时(不同所不一)结算一次资金费率(funding rate)。多头给空头付费率,或反之。资金费率本身是一种对持仓的强制现金流,对做市商来说:
- 持有正库存意味着如果接下来 8 小时 funding 为正,需要付钱。
- 这相当于在 reservation price 上叠加一个确定性漂移项:
r_funding(s, q, t) = r_AS(s, q, t) − q · expected_funding · time_to_next_settlement / s
在 funding 显著偏离零(例如 +0.1%/8h)的时段,做市商应当把 r 偏移再加一个 funding 修正,鼓励减少同向库存。
六点二、跨所对冲的不对称
加密市场没有统一撮合,做市商常见配置是「A 所做被动 maker,B 所做主动 taker 对冲」。这要求:
- A、B 两所的合约规格、保证金币种、保证金率必须能折算到同一个 PnL 维度。
- 跨所价差本身有偏移(basis),需要建模、不能假设两所价格瞬时收敛。
- 链上提现 / 跨所转账有延迟(数十分钟级),意味着两所资金在短时间内不可调度,必须各自留足保证金缓冲。
工程上对冲腿的执行延迟是关键参数:从 A 所成交到 B 所对冲完成的端到端延迟 τ,决定了 A 所做市的真实库存敞口窗口是 τ 而不是 0。在 AS 框架里,τ 进入一个等效的「不可对冲库存波动率」σ_eff = σ · sqrt(τ / 一段参考期),让模型对短窗内库存更敏感。
六点三、闪崩防护
加密市场闪崩(flash crash)是常态而非例外。做市商必须有独立的「最坏情景」断路器:
- 价格变化阈值:mid price 在过去 N 秒内变化超过 X%,立即撤所有挂单。
- 深度蒸发阈值:top-5 depth 在过去 N 秒内萎缩到不足历史 P10,立即撤所有挂单。
- 基差阈值:与现货(spot)或另一所永续之间的 basis 在过去 N 秒内偏离超过 X bps,立即撤所有挂单。
- 强平簇阈值:交易所推送的 liquidation feed 上短时间内单边连续清算,立即撤所有挂单。
这些断路器与 VPIN 是互补关系——VPIN 描述「订单流毒性」,闪崩断路器描述「价格本身的极端运动」。两者都要有,且独立触发,谁先触发听谁的。
六点四、做市商激励合约
主流加密交易所对头部做市商有专属做市商协议(market maker agreement),典型条款:
- maker 返佣(rebate),常见 −0.005% 到 −0.02%;规模/质量越好返佣越多。
- 报价义务:要求做市商在特定符号上一定百分比的时间内挂在最优 K 档之内、一定的最小尺寸。
- 撤单频率上限:超过则收 cancel fee 或被取消做市商资格。
这些条款会反过来约束你的策略——例如「90% 的交易时间必须挂在最优 3 档以内」会逼迫你即使 VPIN 飙高也不能完全下线,必须把价差拉宽到最大允许距离继续挂。把这些条款显式写进策略的硬约束(hard constraint),是做市工程的一部分。
六点五、稳定币与计价单位
加密做市还有一件常被忽略的事:报价计价单位本身有信用风险。USDT、USDC 等稳定币虽然名义锚定 1 美元,但历史上多次出现脱锚(depeg),尤其是 USDC 在 2023 年 SVB 事件中短暂跌至 0.87。做市商挂的所有 USDT 计价订单,本质上都是对「USDT/USD = 1」这一假设的隐式多头敞口。
工程上的对应做法:
- 监控稳定币现货价格(多个 CEX、多个 DEX 聚合),偏离 1.0 超过阈值即触发去 stable 风险。
- 在 reservation price 上叠加一个稳定币溢价 / 折价项,让买单(接稳定币空头)变贵、卖单(接稳定币多头)变便宜。
- 极端脱锚下,所有以该稳定币计价的策略全部下线,等锚定恢复或转移到 USD 计价的合约。
这件事不属于 AS 模型的范畴,但它是加密做市真实风险的一部分,必须独立编码。
六点六、链上做市与 AMM 的位置
链上 AMM(Uniswap、Curve 等)是一种与传统 limit-order-book 做市完全不同的模式:流动性提供者把双币按比例锁进池子,定价由 x·y=k 这类不变量自动决定,不存在主动挂改撤。AMM LP 的本质是对未来波动率卖一个 always-on 跨式期权,收的是 swap 手续费,付出的是「无常损失」(impermanent loss,实质是凸性损失)。
CEX 做市与 AMM LP 不是替代关系而是互补关系:CEX 做市商常常用 AMM 池作为对冲腿(套利方向反过来——CEX 价格领先时把 AMM 价格拉过来),AMM LP 则把对冲外包给套利者。把 AMM LP 写成 AS 框架下的特例,可以视作「不可撤改、报价由曲线决定的做市商」,但它的库存动态完全由价格路径决定,γ 这一类参数在 LP 里失效。本文不展开 AMM 的具体数学,留给独立专题。
七、监管视角
做市与「市场操纵」之间有一条不一定明亮的边。任何想做实盘做市的团队都必须在动手之前把这条边搞清楚。
七点一、被认定为操纵的几类典型行为
监管一般关注以下几类行为(具体定义因辖区不同,下面是综述性概括,不是某一国法规原文):
- Spoofing:挂大额单制造方向假象,等对手反应后撤单。判定的关键证据是「成交意图缺失」——挂单后短时间内主动撤单、且撤单前后没有市场状态实质性变化。
- Layering:在同一侧密集挂多档假单制造深度假象,配合反向小单成交。
- Wash trading:同一受益方在交易所两端挂单互相成交,制造成交量假象。
- Marking the close:在收盘前几秒大量主动单推动收盘价,影响估值或衍生品交割。
- Quote stuffing:超高频报撤但不真实成交,干扰对手系统。
七点二、做市与上述行为的边界
做市策略的所有行为表面上都和这些行为「形似」——挂单后撤单、挂多档、双边都挂、撤单频率高。监管区分的核心是意图与模式:
- 做市商的撤单与「市场状态变化」有强相关——mid price 一变、best 层一动、波动率一升,立即撤改是合理响应。
- 做市商的挂单是有真实成交意图的——若被吃到也愿意成交,对应的资金、保证金、对冲腿都在位。
- 做市商的双边挂单是对称的,目的是赚价差,而不是诱导某一方向。
工程上要为「我的撤单是合理的做市行为」准备证据链:
- 每次撤单必须可归因——记录触发撤单的事件类型(mid 变化、VPIN 飙升、库存到上限、对冲腿失败等)。
- 撤单率指标——按符号、按时段统计 cancel-to-trade ratio,给一个监管可解释的上限(具体数值因交易所而异)。
- 挂单深度合理性——挂单距离最优一档的分布、挂单尺寸分布要在「真实做市」的合理范围内(典型:挂单尺寸≥某个百分位的成交单尺寸;挂单距离主要分布在最优 1–5 档之内)。
- 存活时间分布——单从挂出到撤销的存活时间分布若极度集中在毫秒级、且与市场事件无关,就需要解释。
七点二点五、把合规当成系统约束而不是事后审计
很多团队把合规当成「事后审计」——交易完了之后法务来翻日志。这种工作流在做市策略上行不通,因为做市每秒产生上千条挂改撤事件,事后审计没法覆盖。正确做法是把合规约束直接编码进策略:
- 撤单理由白名单:每次撤单必须 attach 一个枚举值的「原因码」,且必须在系统预定义的白名单内(mid_change、vpin_high、inventory_cap、hedge_fail、circuit_breaker 等)。撤单代码路径上没有合法原因码就阻塞,由开发者必须在合并前补齐枚举。
- 挂单尺寸下限:同一符号的挂单尺寸必须 ≥ 该符号过去 N 日成交单尺寸的 10 分位数。低于这个值认为是「试探性挂单」,进入合规复核。
- 挂单距离与 best 层的最大偏离:超过最大偏离的挂单视为「定价异常」,自动撤回并报警。
- 同侧 K 秒内挂改撤总数上限:超过则限速,拒绝继续发单到匹配引擎之前。
这些约束在策略调参时会限制最优值,但它们不是「策略可调的参数」,而是合规层硬约束——研究员不能为了 backtest Sharpe 高一点而把它们改松。把硬约束放在策略对象之外、由独立的 envelope 进程检查,才是符合监管期望的工程结构。
七点三、规模与做市义务
某些场所对「做市商」身份有正式认定,对应一组做市义务(minimum quote presence、minimum size、maximum spread)和一组激励(rebate、低费率、回报报告)。把策略注册成做市商有两个工程后果:
- 必须有备份链路、备份逻辑——做市义务是连续时间约束,主链路宕机要能自动切换。
- 必须有合规报表——交易所通常按月或按季要求提交 quote presence、average spread、average depth 报表,自己内部要把这套统计跑通且与交易所统计口径一致。
不正式注册做市商身份的策略也可以做市,但失去 rebate、可能遭遇较严的撤单率限制、且在监管调查时缺少「我是 registered MM」的天然抗辩。两条路各有权衡。
八、工程实现
把前七节讲的东西落地成一份能跑的代码。下面三段实现按从理论到工程的顺序展开:先是 AS 仿真器(把模型跑起来看看),再是带库存惩罚的双边报价器(把模型变成生产级回路),最后是 VPIN 触发的撤单逻辑(把对抗逆选择接入)。
代码使用 Python 3.11、numpy 1.26、scipy 1.11。所有「市场」都是合成的,仅用于演示策略逻辑。
八点一、Avellaneda-Stoikov 仿真
这一段实现 AS 论文里的离散仿真:扩散中间价、指数到达强度、CARA 效用、最优买卖偏移闭式解。仿真目标是看「策略 PnL 与库存路径在不同 γ 下的差异」。
import numpy as np
from dataclasses import dataclass
@dataclass
class ASParams:
sigma: float # mid-price volatility per sqrt(time unit)
gamma: float # CARA risk aversion
kappa: float # order arrival sensitivity to depth
A: float # base arrival rate at zero depth
T: float # horizon
dt: float # simulation step
s0: float = 100.0 # initial mid
def as_optimal_quotes(s, q, t, p: ASParams):
"""Closed-form AS optimal bid/ask offsets and quotes."""
tau = max(p.T - t, 0.0)
r = s - q * p.gamma * p.sigma**2 * tau
half_spread = 0.5 * (p.gamma * p.sigma**2 * tau
+ (2.0 / p.gamma) * np.log(1.0 + p.gamma / p.kappa))
return r - half_spread, r + half_spread, r, half_spread
def simulate_as(p: ASParams, seed=0):
rng = np.random.default_rng(seed)
n_steps = int(np.round(p.T / p.dt))
s = p.s0
q = 0
cash = 0.0
history = []
for k in range(n_steps):
t = k * p.dt
bid, ask, r, hs = as_optimal_quotes(s, q, t, p)
delta_bid = s - bid
delta_ask = ask - s
# arrival probabilities for this step
lam_bid = p.A * np.exp(-p.kappa * delta_bid) * p.dt
lam_ask = p.A * np.exp(-p.kappa * delta_ask) * p.dt
u = rng.random(2)
if u[0] < lam_bid:
cash -= bid
q += 1
if u[1] < lam_ask:
cash += ask
q -= 1
# mid evolves
s = s + p.sigma * np.sqrt(p.dt) * rng.standard_normal()
history.append((t, s, q, cash, bid, ask, r, hs))
# mark to market at T
pnl = cash + q * s
return pnl, history
if __name__ == "__main__":
p = ASParams(sigma=2.0, gamma=0.1, kappa=1.5, A=140.0, T=1.0, dt=0.005)
pnls = []
for seed in range(2000):
pnl, _ = simulate_as(p, seed=seed)
pnls.append(pnl)
pnls = np.array(pnls)
print(f"E[PnL] = {pnls.mean(): .3f}")
print(f"Std[PnL] = {pnls.std(): .3f}")
print(f"Sharpe-like = {pnls.mean() / pnls.std(): .3f}")
print(f"VaR(95%) = {np.quantile(pnls, 0.05): .3f}")跑出来的几个看点:
- 提高 γ:库存路径的振幅显著缩小,PnL 均值下降但方差也下降,Sharpe 改善。
- 提高 σ:价差宽度自动拉大(公式里 σ² 项),PnL 均值上升但方差大幅上升,Sharpe 一般是先升后降。
- 提高 κ:成交对距离的敏感度高,挂得稍远就大幅减少成交量,最优半价差自动收窄。
把仿真器跑成「参数 → 期望 PnL / Sharpe / 库存分布」的 lookup table,是后续做线上 γ 自适应的基础。
八点二、含库存惩罚的双边报价器
把上面的仿真包装成一个「报价器对象」:实时维护 mid、库存、时段,每次被外部事件(mid 更新、库存变化、定时 tick)触发时输出新的 bid/ask,并显式应用三类库存惩罚。
import numpy as np
from dataclasses import dataclass, field
@dataclass
class QuoterConfig:
sigma: float
gamma: float
kappa: float
T: float
eta: float = 0.0 # quadratic inventory penalty
q_soft: float = 5.0 # soft inventory cap
q_hard: float = 10.0 # hard inventory cap
tick: float = 0.1 # min price increment
min_size: float = 1.0 # min order size
max_size: float = 10.0 # max order size per side
class ASQuoter:
def __init__(self, cfg: QuoterConfig):
self.cfg = cfg
self.t = 0.0
self.q = 0.0
self.last_mid = None
self.cash = 0.0
def reservation(self, s):
c = self.cfg
tau = max(c.T - self.t, 1e-9)
# AS reservation + quadratic inventory penalty
r = s - self.q * (c.gamma * c.sigma**2 + 2.0 * c.eta) * tau
return r, tau
def half_spread(self, tau):
c = self.cfg
return 0.5 * (c.gamma * c.sigma**2 * tau
+ (2.0 / c.gamma) * np.log(1.0 + c.gamma / c.kappa))
def quote(self, mid: float):
c = self.cfg
r, tau = self.reservation(mid)
hs = self.half_spread(tau)
bid = r - hs
ask = r + hs
# tier 2: inventory bands -- widen same-side, tighten opposite-side
aq = abs(self.q)
if aq >= c.q_soft and aq < c.q_hard:
band = (aq - c.q_soft) / (c.q_hard - c.q_soft)
scale = np.exp(band)
if self.q > 0:
# long: discourage further buying, encourage selling
bid -= hs * (scale - 1)
ask -= hs * (scale - 1) * 0.5
else:
ask += hs * (scale - 1)
bid += hs * (scale - 1) * 0.5
# tier 3: hard cap -- one-sided only
ban_bid = (self.q >= c.q_hard)
ban_ask = (self.q <= -c.q_hard)
# round to tick
bid = np.floor(bid / c.tick) * c.tick
ask = np.ceil(ask / c.tick) * c.tick
# size scaling: shrink toward zero as inventory grows on the same side
size_bid = c.max_size if not ban_bid else 0.0
size_ask = c.max_size if not ban_ask else 0.0
if self.q > 0:
size_bid *= max(0.0, 1.0 - aq / c.q_hard)
else:
size_ask *= max(0.0, 1.0 - aq / c.q_hard)
size_bid = max(size_bid, 0.0)
size_ask = max(size_ask, 0.0)
if size_bid > 0 and size_bid < c.min_size:
size_bid = c.min_size
if size_ask > 0 and size_ask < c.min_size:
size_ask = c.min_size
return {
"bid_price": bid, "bid_size": size_bid,
"ask_price": ask, "ask_size": size_ask,
"reservation": r, "half_spread": hs,
}
def on_fill(self, side: str, price: float, size: float):
if side == "bid":
self.cash -= price * size
self.q += size
elif side == "ask":
self.cash += price * size
self.q -= size
def step_time(self, dt: float):
self.t += dt
def mark_to_market(self, mid: float):
return self.cash + self.q * mid落到生产里,这个报价器外面还要套一层订单管理:
- 同一侧 best price 没变就不撤改(避免无谓的 cancel-to-trade ratio)。
- 价格变化超过半个 tick 才撤改。
- 撤改有最小间隔(throttle),通常 50–200 ms,因符号而异。
- 任何风控信号(mid jump、VPIN、深度蒸发)触发都强制撤所有单,比报价器自己说了算优先级高。
八点三、VPIN 触发的撤单逻辑
把 VPIN 实现成一个独立模块,输入是逐笔成交流,输出是 VPIN 数值与一个状态枚举(normal / cautious / hostile),下游报价器订阅这个状态做响应。
import numpy as np
from collections import deque
from dataclasses import dataclass
from scipy.stats import norm
@dataclass
class VPINConfig:
bucket_volume: float = 50.0 # volume per bucket
n_buckets: int = 50 # rolling window
sigma_window: int = 200 # for BVC sigma estimate
threshold_low: float = 0.3
threshold_high: float = 0.6
class VPINMonitor:
def __init__(self, cfg: VPINConfig):
self.cfg = cfg
self.cur_volume = 0.0
self.cur_dp_sum = 0.0 # accumulate (P_t - P_{t-1}) inside bucket
self.cur_v_buy = 0.0
self.cur_v_sell = 0.0
self.last_price = None
self.dp_buffer = deque(maxlen=cfg.sigma_window)
self.bucket_imbalances = deque(maxlen=cfg.n_buckets)
def on_trade(self, price: float, size: float):
if self.last_price is not None:
dp = price - self.last_price
self.dp_buffer.append(dp)
self.cur_dp_sum += dp
self.last_price = price
self.cur_volume += size
# while bucket is full, close it
while self.cur_volume >= self.cfg.bucket_volume:
overflow = self.cur_volume - self.cfg.bucket_volume
in_bucket = size - overflow
self._close_bucket(in_bucket_dp=self.cur_dp_sum)
self.cur_volume = overflow
self.cur_dp_sum = 0.0
size = overflow
def _close_bucket(self, in_bucket_dp: float):
if len(self.dp_buffer) < 30:
return
sigma = float(np.std(list(self.dp_buffer)))
if sigma <= 1e-9:
buy_share = 0.5
else:
buy_share = float(norm.cdf(in_bucket_dp / sigma))
v_buy = buy_share * self.cfg.bucket_volume
v_sell = self.cfg.bucket_volume - v_buy
imb = abs(v_buy - v_sell) / self.cfg.bucket_volume
self.bucket_imbalances.append(imb)
@property
def vpin(self) -> float:
if not self.bucket_imbalances:
return 0.0
return float(np.mean(self.bucket_imbalances))
@property
def state(self) -> str:
v = self.vpin
if v < self.cfg.threshold_low:
return "normal"
if v < self.cfg.threshold_high:
return "cautious"
return "hostile"报价器与 VPIN 协同工作的伪代码:
def on_market_event(event, quoter, vpin_mon, sender, risk):
if event.kind == "trade":
vpin_mon.on_trade(event.price, event.size)
elif event.kind == "mid_update":
# standard quoting
if vpin_mon.state == "normal":
q = quoter.quote(event.mid)
sender.replace(q)
elif vpin_mon.state == "cautious":
q = quoter.quote(event.mid)
# widen spread further, shrink size
q["bid_price"] -= 2 * quoter.cfg.tick
q["ask_price"] += 2 * quoter.cfg.tick
q["bid_size"] *= 0.5
q["ask_size"] *= 0.5
sender.replace(q)
elif vpin_mon.state == "hostile":
# full pull-back: cancel all maker orders
sender.cancel_all()
# if inventory > soft cap, hedge aggressively
if abs(quoter.q) > quoter.cfg.q_soft:
risk.hedge(quoter.q, side="reduce_only")
elif event.kind == "tick":
quoter.step_time(event.dt)八点三点五、把仿真接入回测引擎
上面三段代码各自独立可跑。把它们组合成一个完整的回测闭环还需要一个事件驱动的撮合层——本系列《回测引擎》给出了通用骨架,做市回测在它上面要追加几件事:
- 被动单的成交模型必须基于队列位置,不能简单按「价格触及就成交」。同一档挂单中,只有排在前面的人先吃到。生产级实现要重放 L2 事件流、维护每一档的本地 queue,自家挂单的 queue position 由插入时的 ahead size 决定,每次别人成交在自家前面 ahead size 减少。
- 撮合时延必须建模:撤单 / 改单从发出到生效有 round-trip 延迟,期间被吃到不能反悔。回测里至少要把「请求时间」与「生效时间」分开。
- 成交后的 mid 漂移必须用真实订单簿事件,不能用合成布朗运动——逆选择损失高度依赖真实订单簿的事件流。
- 撤单费、连接费、保证金占用费必须按交易所档位计入,而不是单一费率。
把这些做好后,AS 的解析参数(γ、κ、A、σ²)才能在回测里得到有意义的标定。否则回测结果对参数的敏感度会被仿真器自己的瑕疵主导。
八点四、撤改频率与风控熔断
策略上线前要明确一组硬性数字,这些数字不进入策略「最优化」,由风控独立配置:
- 每秒最大撤改次数:典型 50–200 次/秒。超过则限速并报警。
- 单符号 cancel-to-trade ratio 上限:典型 50–200。超过即整体降级(拉宽价差、缩小尺寸)。
- 单分钟最大成交量:超过即怀疑「自我对刷」或风控失效,强制下线。
- 库存超硬限止损:q 超过 q_hard 后 N 秒仍未回落,强制 IOC 平仓(不再依赖被动减仓)。
- PnL 日内回撤止损:当日 mark-to-market 超过 X%,强制下线、人工 review。
- 网络延迟熔断:与交易所的 round-trip 超过阈值,撤所有单(避免在「看不清当前最优价」的时候继续挂)。
每一项都对应独立的进程或独立的代码路径,最好不与策略主进程共享内存——主进程发疯时仍要能被独立熔断。
八点五、双边报价循环的状态机
把上面的所有逻辑画成一个状态机,方便代码 review 和合规审查:
+-----------+ VPIN > high +-------------+
| NORMAL | -------------------------> | HOSTILE |
| quoting | <------------------------- | no quoting |
+-----+-----+ VPIN < high (with cool) +-------------+
|
flash crash / depth shock
v
+-----------+
| HALTED | cancel all, manual ack required
+-----------+
每次状态切换都打 audit log,记录触发条件、当前 mid、当前 q、当前 VPIN、过去 N 秒的事件。这份 log 在监管调查、内部复盘、参数迭代时都极其关键,工程上不能省。
八点六、PnL 分解与归因
最后一件事——把 PnL 分解成可独立解释的几块。这是判断策略「是否在按预期赚钱」的唯一手段。
主要组成:
- Spread capture:每笔被动成交相对当时 mid 的偏移之和(正贡献)。
- Maker rebate:成交量 × 返佣率(正贡献)。
- Inventory PnL:库存路径与 mid 变化的乘积积分 ∫ q · dS(可正可负)。
- Adverse selection cost:每笔被动成交后 τ 秒的 mid 漂移(通常负贡献)。
- Hedge cost:跨所对冲腿的吃单费 + 滑点(负贡献)。
- Cancel / infra cost:撤单费、连接费、托管费摊销(负贡献)。
import numpy as np
import pandas as pd
def attribute_pnl(fills: pd.DataFrame, mid_series: pd.Series,
rebate_bps: float, cancel_count: int,
cancel_fee: float, hedge_fills: pd.DataFrame | None,
tau_seconds: int = 60) -> dict:
"""
fills columns: ['ts', 'side', 'price', 'size'] side in {'bid','ask'}
mid_series: index=ts, values=mid_price
"""
# spread capture: for each fill, signed (mid - price) * size with sign
fills = fills.copy()
mids = mid_series.reindex(fills["ts"], method="nearest").values
sign = np.where(fills["side"].values == "bid", -1.0, 1.0)
fills["mid_at_fill"] = mids
fills["spread_pnl"] = sign * (fills["price"].values - mids) * fills["size"].values
spread_pnl = fills["spread_pnl"].sum()
# rebate
notional = (fills["price"] * fills["size"]).sum()
rebate_pnl = notional * rebate_bps / 1e4
# inventory PnL: build cum inventory then integrate over mid changes
fills_sorted = fills.sort_values("ts")
delta_q = np.where(fills_sorted["side"] == "bid",
fills_sorted["size"], -fills_sorted["size"])
q_path = np.cumsum(delta_q)
# join with mid grid (right-fill q between fills)
grid = mid_series.copy().to_frame("mid")
grid["q"] = np.nan
for ts, q in zip(fills_sorted["ts"], q_path):
grid.loc[grid.index >= ts, "q"] = q
grid["q"] = grid["q"].fillna(0.0)
grid["dmid"] = grid["mid"].diff().fillna(0.0)
inv_pnl = (grid["q"].shift(1).fillna(0.0) * grid["dmid"]).sum()
# adverse selection: post-fill mid drift over tau seconds
adv = 0.0
for _, row in fills.iterrows():
try:
future_mid = mid_series.asof(row["ts"] + pd.Timedelta(seconds=tau_seconds))
drift = (future_mid - row["mid_at_fill"])
adv += sign_for(row["side"]) * drift * row["size"] * (-1.0)
except KeyError:
continue
# hedge cost
hedge_cost = 0.0
if hedge_fills is not None and len(hedge_fills) > 0:
h_mids = mid_series.reindex(hedge_fills["ts"], method="nearest").values
h_sign = np.where(hedge_fills["side"].values == "bid", 1.0, -1.0)
hedge_cost = -(h_sign * (hedge_fills["price"].values - h_mids)
* hedge_fills["size"].values).sum()
# infra
infra_cost = -cancel_count * cancel_fee
net = spread_pnl + rebate_pnl + inv_pnl + adv + hedge_cost + infra_cost
return {
"spread_capture": spread_pnl,
"rebate": rebate_pnl,
"inventory": inv_pnl,
"adverse_sel": adv,
"hedge_cost": hedge_cost,
"infra_cost": infra_cost,
"net": net,
}
def sign_for(side: str) -> float:
return -1.0 if side == "bid" else 1.0把这几项每天独立报出来,做几件事:
- Spread capture 应该长期 > 0;若 < 0 说明价差报反了或 mid 估计错了。
- Rebate 应该接近「成交量 × 协议返佣率」;若偏低说明 maker 占比下降,要检查为什么主动单变多。
- Inventory PnL 应该围绕 0 上下波动;长期偏负说明库存暴露错了方向。
- Adverse selection 长期会是负的;它的绝对值与 spread capture 的比值是策略「健康度」的核心指标。健康做市商这个比值应当 < 0.5;接近 1 就要重做。
- Hedge cost / Infra cost 是确定性成本,超出预算就是工程或合约谈判问题。
把这套归因写成 daily report,自动发到风控、研究、运营三方,是做市策略可持续运行的最后一块拼图。
八点六点五、参数自适应:γ、κ、A、σ² 的滚动估计
把策略上线后,AS 公式里的四个参数都不应当是常数。生产中的滚动估计方案:
σ²(mid-price 波动率):用 EWMA(realized variance, λ=0.94) 在 1 秒级别更新。日内开盘、收盘、宏观数据公布前后会有结构性突跳,这些时段切换到「事件 σ」——按历史相同事件的实证波动率取代滚动估计,避免滚动估计跟不上。
A(基础到达强度):A = (近 N 笔 maker 成交数) / (策略挂在 best 的总秒数)。N 取 200—1000,分桶按时段(亚洲/欧洲/美洲)独立估计。
κ(深度敏感度):把过去 K 个 bucket 的 (depth_offset, fill_count) 配对回归 ln(fill_count) = ln(A) − κ·offset,κ 取拟合斜率的负值。注意排除毒性高时段,否则 κ 会被逆选择污染。
γ(风险厌恶):γ 不直接从市场估计,由组合层 VaR 限额反推,见四点五。
把这四组估计写成独立的 estimator 进程,每个 1 秒推一次更新到策略主进程。任何一个 estimator 崩溃,策略主进程使用最近的 last-good 值并报警,不能直接停摆——做市义务往往要求最低在线时间。
八点七、上线前的最小验证清单
最后给一份可勾选的 checklist,作为策略从仿真到实盘的最低门槛:
- AS 仿真在 ≥ 1000 个独立 seed 下 PnL 均值 > 0、Sharpe > 1、库存分布 99 分位数 < q_hard。
- 双边报价器在重放真实 L2 事件流的回测下 cancel-to-trade ratio < 交易所阈值的一半。
- VPIN 阈值在 1 个月真实订单流上事后校验:hostile 时段成交后 1 分钟漂移显著大于 normal 时段(t 检验 p < 0.01)。
- 闪崩断路器在过去 N 次历史闪崩事件上回放,每次都在前 1 秒内触发。
- 跨所对冲腿的端到端延迟实测分布的 99 分位 < 策略允许的 τ_max。
- PnL 归因脚本能在每日跑出六块独立数值,且总和等于账户层 mark-to-market 变化(误差 < 1 bp)。
- 合规层硬约束(撤单原因码、最小尺寸、最大偏离、限速)在 staging 环境注入异常事件下全部触发预期阻塞。
- 风控熔断(库存、PnL 回撤、网络延迟)在 staging 环境模拟下全部能在 < 100 ms 内撤完所有单。
任何一项没过都不上实盘——做市策略的失误不像方向性策略那样「亏一笔下次再来」,而是会在毫秒级累积成结构性损失。把上线门槛设高,是这门生意能长期做的唯一办法。
九、把全文压成五个断言
断言一:做市的本质是为流动性定价,价差由订单处理、库存持有、逆选择三块组成;任何只盯价差不盯库存的做市策略都只是在「赌噪声占比」。
断言二:Avellaneda-Stoikov 的价值不是公式本身,而是它把「库存控制偏移、市场参数控制宽度」这件事变成可解析、可工程化的两件事;生产里 γ、σ²、κ 必须按时段滚动估,T 必须显式选择。
断言三:单靠模型挡不住逆选择,必须有独立的订单流毒性监控(VPIN、OFI、cross-asset signal),以及与模型无关的硬熔断(mid jump、深度蒸发、liquidation cluster)。
断言四:加密做市的特殊性集中在三件事——资金费率、跨所对冲延迟、闪崩频率;模型必须把这三件事编码成显式参数,而不是只调 γ。
断言五:监管层对「做市 vs 操纵」的区分不在于行为表面,而在于意图与归因证据链;每一次撤改都必须可归因、cancel-to-trade ratio 与挂单存活时间分布必须在合理范围内、做市义务条款必须写进策略硬约束。
附加一条:做市策略的工程难度是均匀分布在「数学、低延迟工程、风控、合规」四个维度上的——任何一个维度短板都会让其余三个白做。这不是一个研究员独立能完成的策略,必须有跨职能团队。
把这五条贴在团队墙上,比记住 AS 公式的每一个常数都更有长期价值。
十、参考文献
论文与书
- Avellaneda M, Stoikov S. High-frequency trading in a limit order book. Quantitative Finance, 2008, 8(3): 217-224.
- Glosten L R, Milgrom P R. Bid, ask and transaction prices in a specialist market with heterogeneously informed traders. Journal of Financial Economics, 1985, 14(1): 71-100.
- Kyle A S. Continuous auctions and insider trading. Econometrica, 1985, 53(6): 1315-1335.
- Stoll H R. Inferring the components of the bid-ask spread: Theory and empirical tests. Journal of Finance, 1989, 44(1): 115-134.
- Huang R D, Stoll H R. The components of the bid-ask spread: A general approach. Review of Financial Studies, 1997, 10(4): 995-1034.
- Easley D, Kiefer N M, O’Hara M, Paperman J B. Liquidity, information, and infrequently traded stocks. Journal of Finance, 1996, 51(4): 1405-1436.
- Easley D, López de Prado M, O’Hara M. Flow toxicity and liquidity in a high-frequency world. Review of Financial Studies, 2012, 25(5): 1457-1493.
- Easley D, López de Prado M, O’Hara M. The volume clock: Insights into the high-frequency paradigm. Journal of Portfolio Management, 2012, 39(1): 19-29.
- Cartea Á, Jaimungal S, Penalva J. Algorithmic and High-Frequency Trading. Cambridge University Press, 2015.
- Cartea Á, Jaimungal S. Risk metrics and fine tuning of high-frequency trading strategies. Mathematical Finance, 2015, 25(3): 576-611.
- Guéant O, Lehalle C-A, Fernandez-Tapia J. Dealing with the inventory risk: A solution to the market making problem. Mathematics and Financial Economics, 2013, 7(4): 477-507.
- Guéant O. The Financial Mathematics of Market Liquidity. Chapman and Hall/CRC, 2016.
- Ho T, Stoll H R. Optimal dealer pricing under transactions and return uncertainty. Journal of Financial Economics, 1981, 9(1): 47-73.
- Hasbrouck J. Empirical Market Microstructure. Oxford University Press, 2007.
- O’Hara M. Market Microstructure Theory. Blackwell, 1995.
- Foucault T, Pagano M, Röell A. Market Liquidity: Theory, Evidence, and Policy. Oxford University Press, 2013.
- Menkveld A J. High frequency trading and the new market makers. Journal of Financial Markets, 2013, 16(4): 712-740.
- Brogaard J, Hendershott T, Riordan R. High-frequency trading and price discovery. Review of Financial Studies, 2014, 27(8): 2267-2306.
监管与官方文件
- U.S. SEC. Concept Release on Equity Market Structure. Release No. 34-61358, 2010.
- U.S. CFTC. Antidisruptive Practices Authority. Interpretive Guidance, 2013.
- ESMA. MiFID II RTS 8: Market Making Strategies, Schemes and Agreements. 2016.
- ESMA. Guidelines on the Calibration of Circuit Breakers and Publication of Trading Halts under MiFID II. 2017.
软件与项目文档
- numpy 项目文档. https://numpy.org/doc/
- scipy 项目文档. https://docs.scipy.org/doc/scipy/
- pandas 项目文档. https://pandas.pydata.org/docs/
导航:上一篇 【量化交易】智能订单路由:场所选择、暗池、liquidity sourcing | 下一篇 【量化交易】HFT 架构:内核旁路、FPGA、共置与撮合
写到这里,从「做市的经济学」开始,到「PnL 归因 daily report」结束,构成一条完整的做市策略工程链路。AS 给的是一个解析骨架,库存惩罚、VPIN、闪崩断路器、跨所对冲、监管约束都是必须独立编码的工程层。任何一层缺失,策略都不会真正稳定地为流动性收钱——做市看似简单,实则是一项需要数学、工程、风控、合规四件事都到位的长期生意。
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【量化交易】量化交易全景:从信号到订单的工程链路
量化交易不是策略写得好就能赚钱,更难的是把数据、特征、因子、信号、组合、执行、风控、复盘这八段链路在工程上连成一条不漏数据、不串时间、不丢订单的流水线。本文是【量化交易】系列的总目录与读图,给出八段链路的输入输出、失败模式、不变量清单,并用研究流程图把从一个想法到一笔实盘订单之间所有该过的卡点串起来。
【量化交易】市场结构:交易所、做市商、暗池、ECN
系统梳理全球市场结构(Market Structure)的工程图景:从证券交易所、衍生品交易所、加密交易所,到做市商、暗池、ECN/ATS,再到 Maker-Taker 收费、PFOF、Reg NMS 与 MiFID II 的监管影响;给出量化策略选择交易场所的判断框架与基于 ccxt 的多交易所行情聚合代码。
【量化交易】市场微结构:订单簿、价差、流动性、冲击
系统讲解市场微结构的核心概念与可计算工具:限价订单簿的数据模型、报价/有效/已实现价差、Roll 模型、四维流动性度量、Kyle's lambda、订单流不平衡(OFI)、Almgren-Chriss 框架下的临时与永久冲击、PIN 与 VPIN、Hawkes 过程,并给出基于 polars 的 L2 增量处理与系数估计代码。
【量化交易】订单类型与执行语义:限价、市价、IOC、FOK、冰山
把 Limit、Market、IOC、FOK、Iceberg、Stop、MOO/MOC 这些常被混为一谈的订单类型还原为价格、数量、时效、可见性、触发五个独立维度,并对照 A 股、港股、美股、CME、Binance 五个市场的实际语义差异,给出量化系统中的订单工厂、状态机与风控前置校验的工程实现。