买卖价差(bid-ask spread)是金融市场里最被低估的”免费午餐”——你每笔交易实际付出的隐性成本。但它不是一个单一的东西。一个经典的拆解把价差写成三种经济成本的叠加:
- 逆向选择成本(adverse selection):你这笔交易对面,可能站着比你更懂行的”知情交易者”。做市商挂单被知情单吃掉时要亏钱,所以必须在价差里预先收一笔”信息税”。坏消息越多、信息不对称越严重,这项越高。
- 库存成本(inventory):做市商刚跟你成交完,手里头寸就偏了。为了把库存调回中性、承担临时持有风险,他也要在报价里加价。
- 指令处理成本(order processing):撮合、清算、监管等近似固定的运营成本,通常最稳定。
把这三块从一条观测到的价差序列里拆开,是微观结构研究的硬功夫。本文用状态空间模型 + 卡尔曼滤波做这个分解——关键技巧是:把价差当成观测,把逆向选择与库存当成两个动态不同的隐状态,靠它们”慢 vs 快”的节奏差异把彼此认出来。
一、为什么能从一个观测拆出两个隐状态#
直觉上,一个方程 有两个未知数,解不出来。但状态空间模型能解,靠的是结构识别(identification via dynamics):
- 逆向选择成分是”慢变量”——信息不对称是黏性的,今天的信息优势会持续好多笔,所以建模成高持续性 AR(1):。
- 库存成分是”快变量”——做市商几笔之内就把库存调平了,所以建模成低持续性 AR(1):。
观测方程只有一条,但两个隐状态的**时间签名(temporal signature)**不同:慢成分贡献的是低频漂移,快成分贡献的是高频抖动。卡尔曼滤波(连同 RTS 平滑)正是干这个的——它从单条噪声观测里,用动态结构把两个 latent 分离出来。

二、状态空间模型设定#
把第 笔(或第 分钟)的隐状态记为 ,观测是报价价差 。
状态转移(两个独立的 AR(1) 偏离项):
观测方程:
bps 是指令处理成本(常数),作为观测截距。下面给出完整的卡尔曼滤波器(2 维线性高斯,自己实现,不依赖黑箱):
import numpy as np
def kalman_smoother(y, gamma, phi_info, sig_info, phi_inv, sig_inv, R):
F = np.diag([phi_info, phi_inv])
Q = np.diag([sig_info**2, sig_inv**2])
H = np.array([[1.0, 1.0]])
I = np.eye(2)
# 用无条件方差初始化
v_info = sig_info**2 / (1 - phi_info**2)
v_inv = sig_inv**2 / (1 - phi_inv**2)
x_filt = np.zeros((len(y), 2)); P_filt = np.zeros((len(y), 2, 2))
x0, P0 = np.array([0.0, 0.0]), np.diag([v_info, v_inv])
xprev, Pprev = x0.copy(), P0.copy()
for t in range(len(y)):
# 预测
xp = F @ xprev; Pp = F @ Pprev @ F.T + Q
# 更新
S = H @ Pp @ H.T + R
K = (Pp @ H.T) / S
innov = y[t] - (H @ xp + gamma)
xf = xp + K.flatten() * innov
Pf = (I - K @ H) @ Pp
x_filt[t], P_filt[t] = xf, Pf
xprev, Pprev = xf, Pf
# RTS 平滑(逆向回扫)
x_sm = x_filt.copy()
for t in range(len(y) - 2, -1, -1):
Pp_next = F @ P_filt[t] @ F.T + Q
C = P_filt[t] @ F.T @ np.linalg.inv(Pp_next)
x_sm[t] = x_filt[t] + C @ (x_sm[t + 1] - F @ x_filt[t])
return x_smpython注意几个工程细节:观测噪声方差 要设得比隐状态过程噪声大,否则滤波会把噪声当信号;初始化用无条件方差能让前几十步更稳;RTS 平滑比单纯滤波在样本内复原上更干净,因为它利用了”未来”信息。
三、复原质量:滤波器真的拆开了吗#
我们用已知的真实成分合成数据(逆向选择均值 3 bps、库存均值 1.4 bps、指令处理 2 bps,加观测噪声),跑上面的滤波器,把平滑估计和真实值比:
- 逆向选择成分复原相关 0.756,库存成分复原相关 0.675;
- 复原 RMSE 均为 ≈1.36 bps(相对 6.76 bps 的均值价差,误差约 20%,主要来自两个成分在单观测下的固有混淆)。

这个”≈0.7 的相关 + ≈1.3 bps 误差”是诚实的结果:单观测下两个隐状态不可能 100% 分清,但经济结构(慢/快节奏)足以把大部分信息还原出来。如果你想更准,可以加第二个观测(比如同时用报价价差和成交价冲击),识别度会显著提升。
四、均值价差的成分构成#
把整段序列平均,得到这张”价差切片图”:

在我们设定的参数下,逆向选择 ≈ 3.30 bps(约 49%)、库存 ≈ 1.46 bps(约 22%)、指令处理 = 2.00 bps(约 30%)。这说明:对流动性一般的标的,信息不对称的隐性税往往比很多人以为的更重——它才是价差的大头。这也是为什么”被知情交易者逆向选择”是做市商和被动挂单者最该防范的风险。
五、分解有经济学含义:信息事件窗口#
分解不是数学游戏,它有可解释的经济含义。我们在序列中段人造了一个”信息事件窗口”(比如财报、重大新闻),给逆向选择成分叠了一道冲击,库存成分保持平稳。看窗口放大图:

这正是我们想要的分离效果:逆向选择随信息事件跳升,库存成分稳如磐石。如果你在真实 tick 数据上看到”某只股票价差突然放大但库存指标没动”,基本可以判定是信息驱动而非流动性挤兑——这对事件驱动策略和做市风控是直接可用的信号。
六、真实数据怎么落地(不用假设 DGP)#
本文为了演示可控,直接用了生成数据的真实参数做滤波。真实落地时你不会知道 ,而且更常见的是反过来用交易数据估计成分,而不是先假设状态空间:
经典做法是 Huang–Stoll (1997) / MRR (1997) 的协方差分解。给定带符号的成交流水(买 = +1,卖 = −1)和价格变动 ,估计:
其中 就是逆向选择成分(信息),其余项对应库存与指令处理。另一种是从 Roll (1984) 有效价差出发,用 的负自协方差反推隐性有效价差。无论哪条路,思路一致:用”成交方向”和”价格冲击”的协方差结构反解成分,而不是凭空假设两个 AR(1)。
七、已知偏差与边界#
- 识别靠的是动态结构:如果逆向选择和库存的真实持续性其实很接近,单观测分解会显著劣化——这正是为什么真实数据通常要引入第二个观测(成交价冲击)或多标的联合滤波。
- 状态空间是线性高斯近似:极端行情下成分可能非平稳(比如闪崩时库存成本瞬时爆炸),这时要换 regime-switching 或 Student-t 噪声。
- 本文结果是方法演示:用合成数据证明”能拆开”,真实幅度要按第六节的方法在你自己的 tick 数据上重估,不能直接搬 3.30/1.46/2.00 这几个数。
- 指令处理成本设为常数:真实中它随成交量、交易所规则缓慢变化,精细建模应让它也随时间走。
结论#
买卖价差拆成”逆向选择 + 库存 + 指令处理”三块后,交易成本和做市风险才真正可读。状态空间 + 卡尔曼滤波提供了一条不靠强假设、可解释、可落地的分解路径:慢/快动态差异让单观测也能认出两个隐状态。对你而言,最实用的两点是——(1)逆向选择往往是价差大头,挂单者和做市商要优先防知情单;(2)价差在信息事件下的”只跳逆向选择、不跳库存”是可直接监控的微观结构信号。