买卖价差 Corwin-Schultz 估计:只用每日最高最低价,把「隐性成本」算出来
你下单时看到的成交价,和真实可得的买卖价之间隔着一道隐性成本——买卖价差(bid-ask spread)。Corwin & Schultz (2012) 给出了一招极简的估计法:只靠每日最高价/最低价,对比 1 日与 2 日的高低区间,就能把价差反演出来。本文用纯 numpy 从模型底层实现这套估计,验证它能还原价差的时间变化与排序,并诚实拆穿低频数据病态条件/自相关稀释/小价差衰减/窗口主观/微观结构噪声五类真实陷阱(中阶)。
你以 100 块「市价」买入一只股票,但市场上真实的买方报价(bid)可能是 99.9、卖方报价(ask)是 100.1。中间那 0.2 块就是买卖价差(bid-ask spread)——你交易时实际付出的隐性成本。机构每天在数十只标的上进进出出,价差累积起来就是实打实的磨损。
结论先放这:
Corwin & Schultz (2012) 证明:不需要逐笔成交明细,只靠每日的最高价 H 和最低价 L,对比「单日高低区间」与「相邻两日重叠高低区间」,就能把隐藏的买卖价差反演出来。本质是解一个二元一次方程组——价差 s 和日波动率 σ 被encode在这两个区间里,对比二者即可分离。
实战里,H/L 从任意行情源(Tushare、Yahoo、交易所日线)都能直接拿到;对 A 股还要注意用「未复权」或统一「前复权」的 H/L,避免除权日的高低跳变污染区间。
实战里,H/L 从任意行情源(Tushare、Yahoo、交易所日线)都能直接拿到;对 A 股还要注意用「未复权」或统一「前复权」的 H/L,避免除权日的高低跳变污染区间。
一、为什么每日高低价里藏着价差#
1. 一个朴素的直觉#
一天之内,价格不会只停在中间价(mid)上。它在买卖价之间上蹿下跳:最高价接近当日的 ask 上沿,最低价接近 bid 下沿。于是:
其中 s 就是相对价差(spread,取对数后)。如果某天价格波动很小(低波动),但高低区间依然很宽——那宽出来的部分,大概率就是价差。
2. 关键技巧:用「两日区间」分离波动率#
难点是:单日区间同时混着「价差」和「波动率」。Corwin-Schultz 的巧思是再看相邻两日重叠的高低区间 ln(max(H_{t-1},H_t)/min(L_{t-1},L_t))。两日区间里,波动率按 √2 累积放大,但价差只加一次(两日共用同一套买卖价逻辑)。于是:
- 单日区间:
h1 = s + c1·σ - 两日区间:
h2 = s + c2·σ,其中c2 > c1
两个未知数(s, σ)、两个方程——直接可解:
c1, c2 是「区间对该波动率的敏感度常数」,由日内采样结构决定,可用蒙特卡洛标定(下文详做)。
二、从零实现:标定区间常数 c1、c2#
我们先造一个「纯噪声、无价差」的日线,统计单日与两日高低区间对波动率的敏感度,得到 c1, c2。这一步在 s=0 下做,避免价差与波动率在数据里混在一起难以分离。
import numpy as np
rng = np.random.default_rng(2026)
DAILY_VOL = 0.015 # 每日中间价漂移波动
N_INTRA = 48 # 每个交易日内的采样点数
def one_day(mid, s, sigma_d):
"""一天的高/低:中间价 ± 半价差,叠加日内波动噪声。"""
n = rng.normal(0, sigma_d, N_INTRA)
ask = mid * np.exp(s / 2 + n) # 卖价上沿
bid = mid * np.exp(-s / 2 + n) # 买价下沿
return ask.max(), bid.min()
def estimate_constants():
"""在 s=0(纯噪声)下标定 c1(单日)、c2(两日重叠)。"""
c1s, c2s = [], []
for sig in [0.004, 0.006, 0.008, 0.010, 0.015]:
h1, h2 = [], []
for _ in range(4000):
m0 = 100.0
m1 = m0 * np.exp(rng.normal(0, DAILY_VOL)) # 独立的次日中间价
A = one_day(m0, 0.0, sig)
B = one_day(m1, 0.0, sig)
h1.append(np.log(A[0] / A[1])) # 单日区间
h2.append(np.log(max(A[0], B[0]) / min(A[1], B[1]))) # 两日重叠区间
c1s.append(np.mean(h1) / sig)
c2s.append(np.mean(h2) / sig)
return float(np.mean(c1s)), float(np.mean(c2s))
c1, c2 = estimate_constants()
print(f"c1={c1:.3f} c2={c2:.3f} (c2-c1={c2-c1:.3f})")
# 输出:c1≈4.462 c2≈6.258 (c2-c1≈1.796)pythonc2-c1≈1.796 是这套方法的「对比杠杆」:两日区间比单日区间多出来的波动率敏感度。c1≈4.46 表示单日高低区间约等于 4.46 × 日波动率(这是 48 个日内采样点下的经验常数)。
三、反演估计器#
拿到 c1, c2 后,对任意一段真实日线,滚动地对比单日与两日区间即可反演出价差。
def estimate_spread(high, low, win=20):
"""滚动窗口估计价差(bps)。s = h1 - c1*(h2-h1)/(c2-c1)。"""
Tn = len(high)
h1 = np.log(high / low) # 单日区间
H2 = np.maximum(high[1:], high[:-1]) # 相邻两日重叠高
L2 = np.minimum(low[1:], low[:-1]) # 相邻两日重叠低
h2 = np.log(H2 / L2)
h2 = np.concatenate([[np.nan], h2])
s_est = np.full(Tn, np.nan)
half = win // 2
for t in range(half + 1, Tn - half):
m1 = np.nanmean(h1[t - half:t + half]) # 窗口内单日区间均值
m2 = np.nanmean(h2[t - half:t + half]) # 窗口内两日区间均值
sigma = (m2 - m1) / (c2 - c1) # 先解波动率
s = m1 - c1 * sigma # 再解价差
s_est[t] = s
return s_estpython价格路径与日内高低价差带如下——价差带越宽,隐性买卖价差越大。

四、验证:估计能还原价差的时间变化吗?#
我们造一段「已知真实价差」的模拟日线:平静期 80bps、温和压力 150bps、高压期 250bps,日内波动率压低到 0.6%,让价差成为区间里「有意义的成分」(这一点很关键,见第六节陷阱)。把真实价差与估计价差摆在一起:

分区间核对(取每个区间的均值估计,比滚动噪声更稳):

| 区间 | 真实价差 | CS 估计 | 还原度 |
|---|---|---|---|
| 平静 | 80 bps | ~44 bps | 方向对、偏小 |
| 温和压力 | 150 bps | ~101 bps | 67% |
| 高压 | 250 bps | ~254 bps | 几乎精确 |
排序完美(平静 < 温和 < 高压),高压窗口几乎 1:1 还原。这说明 CS 估计最核心的价值——捕捉价差的时间变化与相对排序——是真实有效的。
滚动估计的分布也明显右移:压力期估计整体高于平静期。

五、滚动窗口越长的稳定性#
价差估计本质在用「区间差」反推,单日噪声很大。用更长滚动窗口平均,能显著压低噪声:

这也是 Corwin-Schultz 原论文的建议:实际估计时用 1 个月(约 21 个交易日)的滚动窗口做平均,而不是逐日估计。
六、五个不能忽略的真实陷阱#
别把上面的「还原」当成万能。以下每一条都会让估计翻车:
-
低频数据病态条件(最大陷阱):本文的标定里
c2-c1≈1.8,而c1≈4.46。价差 s 是从「大范围里抠出的小残差」反推的——s = h1 - 4.46·σ。一旦h2-h1(用来估 σ 的量)有 5% 误差,s 就会被放大约 4.46/1.8≈2.5 倍。当价差相对波动率极小时(比如大盘股 1–2 bps 价差 vs 1% 日波动),s 几乎被噪声淹没,估计会严重失真。这也是为什么本文用「宽价差/低波动」的较不活跃标的来演示——CS 方法在流动性差、价差占比大的资产上才稳。 -
日收益自相关稀释对比杠杆:上面的
c2-c1≈1.8假设相邻日收益近似独立。若日收益有自相关(趋势市、连续涨跌),两日区间不会按 √2 累积波动率,实际c2-c1会被压小,进一步恶化病态条件。真实实现需先检验/修正自相关。 -
小价差系统性低估:表中平静期(80 bps)只还原出 44 bps,而高压期(250 bps)近乎精确——小价差被低估、大价差还原好。这是反演法固有的非线性:残差越小,相对误差越大。应用时不能把估计值当绝对真值,应看其相对变化。
-
窗口长度主观:用 10 日、20 日还是 30 日窗口,估计水平会漂移。应报告敏感性(如第五节),而非给一个「标准答案」。
-
微观结构噪声:真实日线的高/低可能受盘中熔断、集合竞价、临时停牌污染;且若用复权价,高低区间会混入除权跳变。应用前需清洗高低价、统一复权口径。
七、小结#
Corwin-Schultz 把「看不见的买卖价差」变成了一个只用每日最高/最低价就能算的量化指标。它的优雅在于:不需要任何逐笔成交数据,对比单日与两日的高低区间,解一个二元一次方程组就分离出价差和波动率。
但它的有效是有条件的:价差必须是当日区间里「有意义的成分」——流动性越差、价差占比越大,估计越稳;而对大盘股那种「价差淹没在波动里」的场景,它本质上是病态条件的,结果只能看相对变化、不能当绝对真值。
把它当作「价差相对水平的温度计」而非「精密卡尺」——这才是工程上诚实的用法。