你手里有 100 万股要卖,市值 1 个亿。直接一笔砸出去?盘口根本吃不下,你的卖单会把价格一路砸下去,自己把自己的成交价砸崩——这叫市场冲击。分 20 笔慢慢卖?价格是温柔了,可这 1 个亿的仓位每天都暴露在波动里,万一卖的这两天正好暴跌,你亏的是”时间风险”。
“怎么卖”本身就是一道优化题。 快与慢的权衡,早在 2000 年就被 Almgren 与 Chriss 写成了漂亮的微分方程。结论先放这里:最优执行不是匀速,也不是一股脑清仓,而是一条由”你多怕波动”决定的轨迹——越怕波动,越前置(早卖早脱手);越不在乎波动,越接近匀速。在我们的设定里(1 天内卖 100 万股、日波动 2%、分 20 笔),匀速 TWAP 的期望冲击成本是 6.5bps、波动风险高达 111bps;而最优轨迹把波动风险压到 65bps(窄了 41.5%),只多花一点点期望成本。
一、把”卖单”拆成一条库存轨迹#
设要在 N 笔内卖完总量 X。定义:
x_k:第 k 笔之后还剩下的库存(x_0 = X,x_N = 0)n_k = x_{k-1} − x_k:第 k 笔卖出的股数
于是”怎么卖”等价于决定中间那 N−1 个库存点 x_1, …, x_{N−1}。一条轨迹就对应一套执行方案。
价格怎么被你自己的卖单影响?Almgren-Chriss 用两个冲击分开建模:
- 临时冲击(temporary)
τ·n_k:只压低这一笔的成交价,之后会”恢复”。你卖得越急(单笔 n_k 越大),这一笔吃得越亏。 - 永久冲击(permanent)
η·n_k:卖完之后,中间价被永久砸低,影响后面所有笔的成交价。这是不可逆的真实损失。
再加上市场本身的随机波动,价格一步步走:
S_k = S_{k-1} − η·n_k + σ·S_0·√dt · ξ_k (永久冲击 + 噪声)
第 k 笔成交价 = S_{k-1} − τ·n_k (再扣临时冲击)plaintext二、成本的两部分:确定的冲击 + 不确定的波动#
把所有笔的”相比开盘全按 S_0 卖”的亏损加起来(这就是实现短差 Implementation Shortfall),可以拆成期望和方差两部分:
def cost_stats(x, X, eta, tau, sigma_step, N):
n = x[:-1] - x[1:] # n_k = x_{k-1} - x_k
E = 0.5 * eta * X**2 + tau * np.sum(n**2) # 期望冲击成本
Var = sigma_step**2 * np.sum(x[1:N]**2) # 持仓暴露于噪声的方差
return E, Varpython直觉:
- 期望成本
E = 0.5·η·X² + τ·Σn_k²:永久冲击是个固定账(总量定了就定了),临时冲击τ·Σn_k²惩罚”单笔卖太猛”——因为 Σn_k² 在匀速时最小,所以纯看期望成本,匀速最省。 - 方差
Var = σ_step²·Σ x_j²:你还拿着多少库存就暴露多少波动。前几笔如果迟迟不卖(x_j 大),方差爆炸;一开盘全抛(x_j=0 对 j≥1),方差直接归零。
矛盾就在这里:期望成本要你匀速,方差要你快抛。两者打架,这就是执行问题的本质。
三、最优轨迹:一个三对角系统#
目标:在期望成本和风险之间权衡,最小化 J = E + (γ/2)·Var。γ 是你对波动风险的厌恶——γ=0 纯看成本,γ→∞ 吓得赶紧全抛。
对 x_k 求偏导令为零,得到一条三对角线性方程(a=τ,b=(γ/2)·σ_step²):
−a·x_{k−1} + (2a + b)·x_k − a·x_{k+1} = 0, k=1,…,N−1
边界: x_0 = X, x_N = 0plaintext从零实现,numpy 几行就解出来:
import numpy as np
def solve_ac(X, N, tau, eta, gamma, sigma_step):
a = tau
b = 0.5 * gamma * sigma_step**2
M = np.zeros((N - 1, N - 1)); rhs = np.zeros(N - 1)
for k in range(N - 1):
M[k, k] = 2 * a + b
if k > 0: M[k, k - 1] = -a
if k < N - 2: M[k, k + 1] = -a
rhs[0] = a * X # 边界 x_0 = X 移项过来
y = np.linalg.solve(M, rhs)
x = np.concatenate([[X], y, [0.0]]) # 拼上 x_0, x_N
return xpython两个漂亮的性质:
- γ=0(风险中性):方程退化为
x_k线性 →x_k = X(1 − k/N),即匀速 TWAP。所以匀速不是”随便选的”,它是”只关心成本时的数学最优”。 - γ>0:解可以写成
x_k ∝ sinh(κ(N−k))的闭式(κ=√(γσ²/(2τ)) 量级),形状是前置衰减——头重脚轻,早卖多、晚卖少。
四、γ 越大,卖得越前置#
用具体参数跑:S0=100,X=100 万股,N=20 笔覆盖 1 天,日波动 2%,每步噪声 σ_step = 2%·100/√20 ≈ 0.447$,永久冲击 η=8e-8、临时冲击 τ=5e-7(合匀速期望成本 ~6.5bps)。
S0, X, N = 100.0, 1_000_000.0, 20
daily_vol, dt = 0.02, 1.0 / N
sigma_step = daily_vol * S0 * np.sqrt(dt)
eta, tau = 8e-8, 5e-7
for gamma in [0.0, 1e-8, 5e-8, 2e-7, 1e-6]:
x = solve_ac(X, N, tau, eta, gamma, sigma_step)python
γ=0 是那条平滑的匀速线;γ 一拉大,曲线开头陡降——头几笔把大部分货出掉,后面只是打扫战场。越怕波动,越前置,一目了然。
五、执行权衡前沿:快与慢的 Pareto 边界#
把 γ 从 0 扫到很大,每点算 (波动风险, 期望成本),就得到一条执行版的有效前沿;再摆上三个极端策略当参照:
- TWAP 匀速:(风险 111bps, 成本 6.5bps)
- 开盘全抛:(风险 0bps, 成本 54bps)——零波动但冲击爆表
- 收盘全抛:(风险最大, 成本同 54bps)——把全部时间风险扛到最后
gs = [0.0, 1e-8, 5e-8, 2e-7, 5e-7, 1e-6, 3e-6, 1e-5, 3e-5]
frontier = [cost_stats(solve_ac(X, N, tau, eta, g, sigma_step), X, eta, tau, sigma_step, N)
for g in gs]python
前沿的形状说明一切:没有任何单一策略同时最小化两者。你只能在”多花点冲击成本换低风险”和”扛着波动换低成本”之间挑一个点。开盘全抛虽然零风险,但 54bps 的冲击成本太贵,所以最优轨迹(γ 适中那段)稳稳地待在前沿的内侧——它用远低于全抛的冲击成本,把风险压到了可接受的水平。
几个沿途读数(期望成本 bps / 风险 bps):γ=5e-8 → 7.0/88.8;γ=1e-7 → 7.7/76.8;γ=3e-7 → 10.1/56.3;γ=1e-6 → 14.9/37.4。风险每压下来一截,成本就涨一点——这就是这条曲线的”价目表”。
六、蒙特卡洛:最优轨迹真的把成本分布压窄了#
公式里 Var 降了,但价格路径是随机的,真实跑出来呢?模拟 4000 条价格路径,分别用最优轨迹(γ=2e-7)和 TWAP 执行,看实现短差的分布:
def simulate(x, X, N, S0, eta, tau, sigma_step, rng):
n = x[:-1] - x[1:]; S = S0; cash = 0.0
for k in range(N):
exec_price = S - tau * n[k] # 临时冲击
cash += n[k] * exec_price
S = S - eta * n[k] + sigma_step * rng.standard_normal() # 永久冲击 + 噪声
return (S0 * X - cash) / (X * S0) * 1e4 # 实现短差 (bps)python
结果:TWAP 的实现短差均值 5.5bps、标准差 111bps;最优轨迹均值 8.7bps、标准差 64.9bps——分布窄了 41.5%。均值略高(多付了一点冲击成本),但尾巴被砍掉一大截。对一笔 1 个亿的头寸,这相当于把”卖在暴跌那两天”的尾部风险实实在在地降了下来。
七、冲击模型与成本拆解#
最后把机制画清楚:下图左是一条真实价格路径,红色尖角是每笔的成交价——你能看到中间价因永久冲击阶梯式下行(不可逆),而每笔成交价又在中间价上再凿一个临时凹坑(之后不回填)。右图把成本拆成三块:永久冲击、临时冲击、波动风险(std)。

最优轨迹(紫)相比 TWAP(红),临时冲击略升(因为前置卖得更急),但波动风险(std)那块被砍掉一大半——这正是它愿意多付一点期望成本换来的东西。
八、真实陷阱(别直接照抄)#
-
线性冲击是最大简化。真实市场的冲击是非线性的:小单近似线性,大单冲击会加速恶化(常加一个
n_k^γ,γ>1)。线性模型会低估大单的真实成本,最优轨迹因此”卖得太猛”。生产环境要用非线性/分段冲击校准。 -
σ 被当成常数。本文 σ_step 全程不变,但盘中波动有 U 形(开盘收盘大、午间小)、危机时骤升。若把波动当成常数,优化会在波动其实很大的时段还慢悠悠卖。进阶做法是用时变波动或盘口深度动态调轨迹。
-
没有流动性约束。模型假设你想卖多少就能卖多少(只是贵)。真实盘口有深度上限,单笔吃穿档位会触发更狠的冲击,甚至根本挂不进那么大的单。要加
n_k ≤ 盘口可容量约束。 -
忽略了择时信号。纯 AC 是”机械执行”,完全不判断现在该不该卖。若叠加一个方向判断(认为接下来会跌),应当前置得更狠——但那是另一层决策,别和成本优化混为一谈导致双重计数。
-
隐含”无滑点之外的对手盘”假设。本文没建模抢跑(front-running)、暗池、以及你的单本身改变他人行为。在真实微观结构里,大单的信息含量会引来逆向选择,这部分成本模型完全看不见。
-
γ 和冲击参数都要校准,且会漂移。η、τ 随标的流动性、市场状态变;γ 随你的风险预算变。用历史冲击回归定期重估,别拿一篇论文的常数用一年。
九、小结#
Almgren-Chriss 的美,在于它把”执行”从拍脑袋变成了一道有解析解的优化:永久冲击、临时冲击、持仓波动风险,三个东西写进一个目标,解出来是一条由 γ 决定的前置/后置轨迹。匀速 TWAP 只是 γ=0 的特例,绝不是天然最优。
它给交易员的不是”正确答案”,而是一张价目表——“想少冒 X 的波动风险,得多付 Y 的冲击成本”。拿着这张表,再去和你的风险预算对账,才知道该快还是该慢。
至此,我们从”怎么选权重”(鲁棒优化)讲到”怎么把仓位变成成交”(最优执行)——一个管上限,一个管落地。