Roll 协方差度量买卖成本:从逐笔成交价的「负自相关」反推隐含价差
Roll (1984) 隐含交易成本度量,是微观结构里最「反直觉却优雅」的技巧之一:它不依赖任何订单簿数据,只从一段连续的「成交价差分 Δp_t」里,用相邻两期差分的协方差 −Cov(Δp_t, Δp_{t-1}) 反推出有效(全额)买卖价差 s = 2·√{−Cov}。本文用纯 numpy 合成「随机游走中间价 + 含价差 Tick 的成交价」,滚动估计 Roll 成本,对照已知真实成本:价差主导噪声的应激段(6 tick)几乎精确还原(0.0601 vs 0.0600),噪声主导的平静段(2 tick)被向上偏置到 0.030(真实 0.020);并用一个对照实验证明——成交方向自相关 ρ 越大,Roll 越低估真实成本(ρ=0 才精确还原,ρ=0.7 时低估 −58%)。同时诚实拆穿「非负协方差被丢弃导致的低 SNR 偏置 / Tick 取整 / 趋势市失效 / 漂移污染」四类真实陷阱(中阶)。
你做高频或微观结构研究,第一件事往往是问:这笔交易的真实成本是多少? 要价差,大家本能去拉 L2 订单簿——买卖一档差多少、挂单量多少。但订单簿只在「现在」有,历史上大部分时段的逐笔成交(tape)里,只有成交价序列,没有簿。Roll (1984) 给了一个惊艳的办法:完全不看簿,只从「一段连续的成交价差分」里,把隐含的有效价差算出来。
结论先放这:如果真实价格是随机游走,而每笔成交价 = 真实价 ± 半个价差(买主动加、卖主动减),那么相邻两笔的成交价差分之间,会出现一个负的协方差,而且这个负协方差的大小,正好等于「半个价差的平方」。于是有效(全额)价差可以写成:
s = 2 · √{ −Cov(Δp_t, Δp_{t-1}) }plaintext一句话:Roll 度量的全部信息量就是——成交价序列里「相邻差分负相关」的程度,量化了市场多难成交。负相关越强(买卖来回弹得越狠),隐含价差越大、流动性越差;若差分近似不相关(真实价格剧烈波动盖过了价差弹跳),Roll 反而测不出成本,甚至会偏置。它在我们的合成里表现得很诚实:价差主导时几乎精确(应激段 0.0601 vs 0.0600),噪声主导时被向上偏置(平静段 0.030 vs 0.020),而一旦成交方向带自相关,它就系统性低估真实成本。

一、直觉:为什么「买完卖、卖完买」会让差分负相关#
设真实(有效)中间价为 ,它是个随机游走,没有自相关。每笔成交价 不是落在 上,而是落在「中间价 ± 半个价差」里——买方主动成交时加半价差,卖方主动时减半价差:
p_t = m_t + (s/2)·z_t , z_t ∈ {+1(买主动), −1(卖主动)}plaintext对 取一阶差分:
Δp_t = Δm_t + (s/2)·(z_t − z_{t-1})plaintext关键看第二项 。如果这笔和上一笔成交方向相同(都买、或都卖),,差分只含真实价变动;如果方向相反(上笔买、这笔下,或反过来),,差分被「硬拽」了一下一个半价差的量。
而相邻两期差分做协方差时, 和 不相关(游走), 序列若近似独立,唯一跨期耦合就发生在「方向相反」的那一步——它会让 和 反向联动,于是协方差为负,且大小恰好:
−Cov(Δp_t, Δp_{t-1}) ≈ (s/2)²plaintext这就是 Roll 公式 的来历。它 elegant 的地方在于:完全不需要知道谁在买、挂单在哪,只要「成交方向在相邻笔间频繁翻转」这个微观结构事实成立,价差就从差分的相关结构里自己浮出来了。
二、从零实现:纯 numpy 复刻滚动 Roll 估计#
我们用纯 numpy 合成一段数据:真实中间价随机游走 + 含价差 Tick 的成交价,再把 Roll 度量滚动跑出来,和「已知真实成本」对照(这是检验一切隐性度量的金标准——你得先有 ground truth,才知道它测没测对)。
import numpy as np
def roll_spread(price, K=50):
"""滚动 Roll 隐含全额价差估计。
price : 逐笔成交价序列
返回 : 与 price 等长、每个窗口末的估计 s_t(无效窗口为 nan)
"""
dp = np.diff(price) # Δp_t
n = len(dp)
out = np.full(n, np.nan)
for i in range(K + 1, n - 1):
a = dp[i - K:i] # Δp_t (t = i-K .. i-1)
b = dp[i - K - 1:i - 1] # Δp_{t-1}
cov = np.mean(a * b) - np.mean(a) * np.mean(b)
if cov < 0: # 只有负相关(价差弹跳)才有效
out[i] = 2.0 * np.sqrt(-cov)
return outpython两个工程上不能省的点:
- 非负协方差被丢弃:当窗口内真实价波动盖过了价差弹跳,,此时 Roll 公式「无定义」(根号里是负数)。实务里直接标
nan跳过——这步看似无害,其实正是后面「低 SNR 偏置」陷阱的根源(第五节①详拆)。 - 滚动窗口 K:K 太小→估计噪声大、抖得厉害;K 太大→跟不上价差 regime 变化(比如应激时刻价差瞬间扩大,大窗口会把它「平均掉」)。本文用 K=50 笔做演示。
合成设定:4000 笔成交,中间价波动率对应日频约 0.013,平静段价差 2 tick(=0.02 美元),应激段(第 2000~3000 笔)价差扩到 6 tick(=0.060 美元)。滚动估计结果对照真实成本:

| 区间 | 真实全额价差 | Roll 估计 | 偏差 |
|---|---|---|---|
| 平静段(2 tick) | 0.0200 | 0.0299 | +49.1% |
| 应激段(6 tick) | 0.0600 | 0.0601 | +0.2% |
这张表是全文最重要的诚实结论:Roll 度量不是「无偏金尺」。价差相对噪声越大(应激段),它越准;价差相对噪声越小(平静段、低波动标的),它越偏。为什么?下一节用对照实验把机制拆开。
三、对照实验:成交方向自相关 ρ 越大,Roll 越低估成本#
Roll 公式有个隐藏前提——相邻成交方向近似独立。但真实市场里,买方主动的单子常常是「一连串」(算法扫单、冰山单),卖单也常常成簇,于是 带自相关 。这会直接腐蚀那个负协方差:当同向单黏在一起,方向相反翻转变少,「价差弹跳」信号被稀释,算出来的 −Cov 偏小,于是 被低估。
我们用一个干净对照(固定真实价差 0.02 美元)验证:ρ 从 0 逐步加大,看 Roll 估计怎么塌。
def roll_estimate(rho, spread_dollars, n=2000, seed=7):
rng = np.random.default_rng(seed)
mid = 50.0 * np.exp(np.cumsum(rng.normal(0, 0.0008, n)))
# 带自相关的成交方向 z_t(AR(1))
u = np.zeros(n); u[0] = rng.normal()
for t in range(1, n):
u[t] = rho * u[t-1] + np.sqrt(1-rho**2) * rng.normal()
z = np.where(u >= 0, 1, -1)
obs = mid + spread_dollars/2 * z # 连续成交价
dp = np.diff(obs)
cov = np.mean(dp[1:]*dp[:-1]) - np.mean(dp[1:])*np.mean(dp[:-1])
return 2*np.sqrt(-cov) if cov < 0 else np.nanpython跑出来的对照(真实价差恒为 0.02):

| 方向自相关 ρ | Roll 估计 | 相对真实偏差 |
|---|---|---|
| 0.0 | 0.0205 | +2.6% |
| 0.1 | 0.0180 | −10.0% |
| 0.3 | 0.0167 | −16.7% |
| 0.5 | 0.0123 | −38.4% |
| 0.7 | 0.0084 | −58.2% |
结论很硬:ρ=0(独立方向,Roll 假设成立)时几乎精确(+2.6%,就是采样噪声);ρ 每升一档,低估就加深一档,ρ=0.7 时真实成本被砍掉近六成。这意味着——凡是「大单黏性、算法扫单、做市商刷量」明显的市场(加密、小盘股、盘中最活跃时段),Roll 度量会系统性低估你的真实摩擦成本。用它做「交易成本归因」前,务必先估一下成交方向自相关,或者用后面的修正方案。
四、它到底能干嘛:三个落地场景#
- 历史 tape 的成本重建:没有 L2 存档的老数据,用 Roll 从成交价序列反推「那段市场大概多贵」——比如比较 2015 股灾前后、或某币上线做市商前后的隐含成本变化。
- 流动性监控:滚动 Roll 估计如果突然跳升,往往是价差扩大 / 流动性枯竭的先行信号,比等盘口数据更及时(因为 tape 永远有)。
- 策略成本归因:一个高频策略「理论赚 3 bps、实盘只赚 1 bps」,用 Roll 估计出这段的隐含成本,就能把缺口拆成「理论滑点 vs 真实摩擦」,而不是笼统说「执行不好」。
五、诚实拆穿:四类真实陷阱#
① 低 SNR 偏置:非负协方差被丢弃,导致「平静段高估、应激段才准」。 本文平静段 Roll 测出 0.030 而真实仅 0.020(+49%)。机制:窗口里只要真实价波动盖过价差弹跳,Cov 就非负、被丢成 nan;而留下来的「负相关窗口」恰恰是价差信号相对强的少数,对它们求均值,自然把估计往上拽。这解释了为什么「价差越大越准、越小越偏」——它不是 bug,是 estimator 的固有偏置。修正:用 Hasbrouck (2009) 的「把非负协方差也纳入、用全样本二阶矩」的改进版,或改用 Corwin-Schultz(见同专栏)这种基于高低价的度量,对低波动段更稳。
② Tick 取整污染:本文主图用了连续成交价,真实逐笔是离散取整的。 真实世界成交价落在 tick 网格上(0.01 美元一档),当价差只有 1~2 tick 时,取整会让 的 ±半价差被「量化噪声」吃掉,Roll 估计进一步失真。本文为贴合 Roll 的连续假设用了连续价,若换真实逐笔,平静段偏差只会更大。实务:价差 < 3 tick 的标的,Roll 基本不可用,直接上 L2 或 Corwin-Schultz。
③ 趋势 / 漂移市失效。 Roll 假设真实价是「无漂移随机游走」。若中间价有趋势( 带符号),趋势项会和价差弹跳项混进协方差,把负协方差「填平」甚至反号,估计直接变 nan 或严重偏小。所以 Roll 只适合「短期近似平稳」的微观窗口,不能拿去测一段有方向的行情。
④ 它不是「执行成本」的全集。 Roll 测的是价差(做市商报价的静态摩擦),不含你的市场冲击(price impact)——你下一笔大单把价格推走的那部分。对机构大单,impact 往往比价差大一个量级。Roll 低估了真实总成本,这一点和「ρ 自相关低估」叠加,意味着实盘摩擦成本永远 ≥ Roll 估计值,用它做容量规划必须留安全垫。
六、落到量化:你的日常工作流#
- 先有 ground truth 再谈度量:像本文这样,先用已知价差合成数据验证你的 Roll 实现——很多「Roll 测出来是 0」的 bug,其实是窗口 K 太小、或数据里混了分红拆细导致的假跳。
- 永远配合「方向自相关」一起看:跑 Roll 前先算 ,ρ>0.2 就标记「此段 Roll 低估」,别直接当真值填进成本模型。
- 低波动 / 亚 3-tick 标的换工具:用 Corwin-Schultz(基于日高低价)或干脆拉 L2;Roll 在这类场景是装饰品。
- 把它当「流动性温度计」而非「成本标尺」:监控滚动 Roll 的跳变比盯绝对值更有用——跳升 = 流动性警报。
真实落地时,逐笔成交价序列(券商 / 交易所 tape)直接喂进
roll_spread即可;若只有分钟级 OHLC,Roll 基本失效(差分里价差信号被时间聚合冲掉),改用 Corwin-Schultz。本文纯 numpy 合成只为把「差分负相关 → 隐含价差」的每一步摊开。记住:Roll 度量优雅,但它是把「尺子」——只量得准价差、量不准冲击,且偏好高波动高流动性的应激段;平静段和自相关段,它会温柔地骗你。