土法炼钢兴趣小组的算法知识备份

【量化交易】统计套利:协整、配对交易、PCA 残差

文章导航

分类入口
quant
标签入口
#stat-arb#cointegration#pairs-trading#pca#mean-reversion

目录

把两只走势相近的股票放在一张图上,眼睛会自动找到它们短暂分开又重新靠拢的瞬间,几乎所有第一次接触统计套利的人都是从这张图开始的。但这种「看图说话」远远不是策略,它甚至不是一个可以下注的统计假设。要把它变成可下注的东西,需要先回答一连串严肃的问题:两条价格序列「靠拢」是什么意义上的靠拢,是相关、是协整、还是某种共同因子驱动?价差的均值回归是物理意义上的回归,还是只是抽样到了一段平稳窗口?回归速度有多快、半衰期多长、应该用什么模型来量化它?把这些问题填清楚,才能从一张曲线图迈到一份能上线的统计套利策略。

本文把统计套利当作一条从理论到工程的完整链路来讲。先界定它和因子投资的边界,把「均值回归」「价差中性」「长短并举」三个核心特征讲透;然后从最朴素的距离法、相关法走到协整法和误差修正模型,给出可运行的协整检验代码;再借 Ornstein-Uhlenbeck(OU)过程把开平仓阈值、半衰期与持仓时间显式联系起来;接着把视角从两只标的扩到 N 只标的,复述 Avellaneda 和 Lee 在 2010 年那篇文章里把 PCA 残差当作均值回归信号源的整套做法,并给出一段从合成数据到组合权重的端到端模拟;之后转到加密资产场景,把跨所价差、永续与现货基差、DEX 与 CEX 价差三类常被混淆的套利分清楚;最后用 LTCM 在 1998 年的清算与 2007 年「Quant Quake」两个真实案例来回答失败模式与风险预算这两个最容易被忽视的问题。文末附上工程实现要点和参考文献。

范围与版本说明:本文涉及的统计方法以经典文献为准,包括 Engle 和 Granger(1987)、Johansen(1991)的协整理论、Avellaneda 和 Lee(2010)的统计套利原文、Gatev 等(2006)的距离法实证。代码示例使用 Python 3.11 加 numpy 1.26、pandas 2.2、statsmodels 0.14;所有数据均为本地合成或可复现仿真,不涉及真实历史行情。SVG 图位于本文同目录的 images/ 下。

风险提示:本文出现的所有策略示例只用于阐释统计套利的方法论,不构成任何投资建议。统计套利的失败大多不是来自模型计算错误,而是来自协整关系本身在结构性事件中失效。把任何示例搬到生产环境前,请先做完整的样本外验证、压力测试与流动性核算,并配置独立于策略本身的风险预算与硬止损。


一、统计套利的核心思想

统计套利(Statistical Arbitrage,常缩写为 stat arb)这个词在不同人嘴里指代不同范围。最严格的定义来自学术文献:一类长短并举、市场中性、收益主要来自资产价格相对偏离的均值回归而非整体方向暴露的策略。最宽松的定义则把任何用统计模型驱动的高频或中频策略都装进去。本文采用前者,把它的判别条件压缩成三句话。

均值回归是收益来源

统计套利押注的不是某个标的会涨或跌,而是某个线性组合会回到它的长期均值。这个线性组合可以是两只股票的价差,可以是 N 只股票相对一组主成分因子的残差累积,也可以是同一资产在两个市场上的价格差。它的关键属性是平稳(stationary):方差有限、均值不漂移、自相关在足够长的滞后下衰减到零。如果这个性质不成立,所谓的均值回归只是一段抽样幻觉,半衰期会随窗口拉长而无限放大,止损会被反复打穿。

因此统计套利的第一性原则不是「找走势像的标的」,而是「找平稳的线性组合」。两条相关性 0.99 的价格序列可能根本不协整——它们可以共同向上漂移、各自带着随机游走分量,价差越拉越大;反过来,相关性只有 0.6 的两只标的,价差却可能严格平稳,因为它们恰好被同一组共同趋势驱动。本文第二节会把相关与协整的差别用代码具体地拆开。

价差中性而非美元中性

第二个特征是「价差中性」,常被简化为「市场中性」,但二者并不等价。美元中性意味着多空市值相等,β 中性意味着多空对市场指数的暴露相互抵消,价差中性则意味着策略的盈亏只由特定线性组合(价差、残差、基差)的偏离决定,对任何外部因子的暴露都被显式扣除。

举个反例。两只股票同属白酒行业,把它们简单等市值多空,不仅会承担行业 β、还会承担行业内部的风格因子(市值、成长、波动等)。市场上行时多头一侧涨得不一定比空头一侧多——账户净值看似中性,盈亏却被行业 β 牵着走。价差中性的正确做法是先估计两只标的对市场、行业、风格因子的暴露,再用一个使价差对所有这些因子暴露为零的对冲比,剩下的部分才是真正的价差信号。第三节讲协整时会看到,Engle-Granger 估计出的对冲比 β 在数学上恰好就是「使价差对水平共同趋势中性」的那个比例。

长短并举与杠杆

第三个特征是长短并举,本质上由前两个特征推出。统计套利的预期单笔回报很小——典型的两位数 bp 量级,年化夏普率高的策略 6 至 12,但绝对收益率往往只有个位数。要把这种小信号放大到值得运营的规模,必须加杠杆。LTCM 鼎盛时杠杆率超过 25 倍,正是因为它的核心策略是国债与利率衍生品的相对价值套利,不加杠杆收益不值得做。

但杠杆放大的不仅是收益,也放大了协整破裂时的损失,并把流动性风险变成主导风险。第七节会专门讲这个事。

一个常被混淆的概念:统计套利不是无风险套利

学术上的「套利」一词在 Ross(1976)的 APT 里指的是一组完全无风险、无初始投入但有正收益的头寸。统计套利显然不满足这个严格定义——它的每一笔交易都承担风险,盈亏期望也只是「在大数定律下为正」而已。Hogan、Jarrow、Teo 与 Warachka(2004)正式给出过统计套利的可证伪定义:

满足以下四条的策略 V_t 即为统计套利:
1. 初始净投入为零;
2. 当 t→∞ 时折现期望收益为正;
3. 当 t→∞ 时折现方差趋零;
4. 在任意有限 t 内损失概率随时间趋零。

这套形式化定义在工程上很难直接验证,但它点出了一个重要事实:统计套利押注的是「在大量独立或弱相关交易上的渐进收敛」,单笔交易的胜率可能只有 55% 至 60%,靠的是数量与频率把信号放大。这个性质决定了它对样本量的极度敏感——一个一年只能交易十次的「均值回归信号」从统计意义上说基本无法验证,必须做到能反复独立交易、且交易之间相关度低,才有意义。

与因子投资的边界

统计套利经常与因子投资被放在一起讲,但二者的工程含义差别很大。

维度 因子投资 统计套利
信号来源 横截面上的预期收益差 时间序列上的价格偏离
持仓周期 月级到季级 日级到小时级
组合规模 数百到数千只 数对到数百残差
协方差估计 长样本,稳态假设 滚动样本,结构性变化
主要风险 因子失效、风格切换 协整破裂、流动性冲击
衡量指标 信息比率、年化超额 夏普率、半衰期一致性

边界并非绝对,主要差别在三处:

第一,信号的物理来源。因子投资押注的是横截面上某些可观察特征(账面市值比、过去十二个月动量、毛利率)能预测未来收益的横截面差异;统计套利押注的是某个线性组合的偏离会在时间序列上被压回去。前者依赖于「相同的特征对所有股票产生类似的相对预测力」,后者依赖于「同一组标的的相对关系在足够长时间窗口内保持平稳」。

第二,时间尺度。因子的回报来自风险溢价的累积,需要数月到数年的持仓才能显著体现;统计套利的回报来自短周期均值回归,单笔持仓往往以日或周计。这导致二者的换手率、交易成本敏感度、对实时数据基础设施的需求差几个数量级。

第三,风险归因方式。因子投资的风险归因通常做成「因子贡献 + 特异贡献」的横截面分解;统计套利则需要「均衡贡献 + 偏离贡献 + 失效风险」这样的时间序列分解。两者的归因报告几乎不能共用一套模板。

两者并不互斥。Barra 风格因子里的「短期反转」就有统计套利的影子,AQR 等机构也把多空价值因子和统计套利信号叠加使用。但作为工程系统,它们的数据频率、回测框架、风险监控对象都不一样,不能用同一套基础设施做。

频率与品种的二维分布

把统计套利按交易频率与品种再切一刀,可以得到下面这张地图:

频率  品种 单一资产类内 跨资产类 跨地域
日级(day) 配对、PCA 残差、行业内中性 股债轮动、商品-股票均衡 A 股-港股、ADR-本地股
小时级 高换手 PCA 残差、ETF 与成分套利 期现基差、跨品种价差 跨时区 ETF 隐含价差
分钟级 ETF NAV 套利、跨所价差 三角套利、合成头寸 跨链 DEX-CEX
秒级 / 微秒级 Maker-Taker 仓位轮转、做市残差 与微观结构耦合 跨地理低延迟链路

频率越高,策略对工程基础设施(行情时延、撮合 colocation、风控决策延迟)的依赖越强;频率越低,策略对统计假设稳健性、协整长期成立、对冲精度的依赖越强。本文的篇幅集中在日级到小时级,这也是绝大多数对冲基金的统计套利仓位的频率所在。


二、配对交易

配对交易是统计套利最古老的形态,传说源自 1980 年代摩根士丹利由 Tartaglia 领导的小组。它的工程内核可以拆成三步:选对、估对冲比、定开平仓阈值。学术文献里讨论最多的是第一步「选对」的方法,下面把三种主流做法过一遍。

距离法

Gatev、Goetzmann 和 Rouwenhorst 在 2006 年那篇 Review of Financial Studies 论文里把距离法定义为:把所有候选标的的价格序列归一化为初始值为 1 的累计收益曲线,对每两条曲线计算其欧氏距离的平方和(SSD),SSD 最小的若干对作为候选配对。形式上:

SSD(i, j) = Σ_t (P̃_i,t − P̃_j,t)²

其中 P̃_i,t = P_i,t / P_i,0。这种方法的优点是不需要估计任何参数、计算量小、跨候选可比,劣势是它对「价差是否平稳」没有任何检验:两条共同上行的累计收益曲线 SSD 也可能很小,但它们的价差可能根本不平稳。距离法在实证中的胜率主要来自一个事实——把所有候选成对扫一遍,总能找到几对在过去若干个月里走得很近,未来一段时间里是否继续靠拢则没有保证。Gatev 的论文报告 1962 至 2002 年的年化超额收益约 11%,但 do Prado 在 2018 年用 2003 年后的数据复算,超额收益基本消失——这正是结构性失效的典型表现。

相关系数法

第二种是用收益率的皮尔逊相关系数排序:

ρ(i, j) = Cov(r_i, r_j) / (σ_i σ_j)

相关系数法在工程上更容易做,因为收益率比累计价格更平稳,相关系数估计更稳定。但相关系数和协整完全不是一回事。两条独立随机游走 P_i 和 P_j,加上同一个共同的漂移项 μ·t,它们的收益率仍然是独立的(除了均值都等于 μ),相关系数可能近 1,但价差 P_i − P_j 是两个独立随机游走之和,严格不平稳。这也是初学者最容易踩的坑。

协整法

正确的做法是直接检验「存在某个 β 使得 P_i,t − β · P_j,t 平稳」这一命题。Engle 和 Granger(1987)给出的两步法是:

  1. 用最小二乘估计 P_i,t = α + β P_j,t + ε_t;
  2. 对残差 ε_t 做单位根检验(增广 Dickey-Fuller,ADF),原假设是「存在单位根」即非平稳,被拒绝则说明残差平稳,配对协整。

协整法直接对应统计套利「找平稳线性组合」的第一性原则,是三种方法里最贴合策略本质的。代价是 Engle-Granger 是单方程方法、协整向量 β 在 N > 2 时不唯一;Johansen(1991)给出了向量误差修正模型(VECM)下的极大似然法,能识别 N 只标的之间的所有协整向量。第三节会给出代码。

三种方法的实证胜率对比

把三种方法在同一份样本上跑一遍,差异是显著的。Krauss(2017)那篇综述梳理了 1962 至 2014 年 NYSE 数据的实证:

方法 平均年化超额 平均夏普率 协整破裂频率
距离法(SSD) 4.3% 0.6 较高
相关系数法 3.1% 0.4
Engle-Granger 协整 5.7% 0.9 中等
Johansen + VECM 6.2% 1.0 中等
时变协整(Kalman) 6.8% 1.1 较低

但所有方法在 2003 年后的样本外都有显著的收益衰减。原因不复杂:可乐双雄那种逻辑清晰、长期协整的标的对越来越少;同时市面上做统计套利的资金越来越多,alpha 被反复套利所消耗。距离法的衰减最快,协整法略慢,时变协整法依赖更多参数估计,过拟合代价也更高。

经典案例:可口可乐与百事

可乐双雄被几乎所有教科书拿来做配对交易的标本。两家公司的产品高度同质、客户重叠、上游成本结构相似(甜味剂、铝罐、运输),逻辑上它们的相对估值应当被一组共同的现金流驱动力锚定。1990 年代到 2010 年代上半段的多数窗口里,KO 与 PEP 的价格之比围绕一个缓慢漂移的中枢震荡,这种相对稳定让它成为最早一批被反复套利、最终利润逐步消失的配对——市场里所谓的 alpha 衰减在它身上特别明显。

但 2018 年之后情况发生了变化:百事的食品业务(菲多利、桂格)占比提升到 50% 以上,可口可乐则始终聚焦饮料,二者的现金流结构开始系统性偏离,价差均值出现长期漂移。任何在样本内估计出 β 后简单延用的策略都会在 2019 至 2022 年间被反复止损。这是协整破裂的典型例子。第七节会回到这个话题。

数据预处理:被低估的工程量

配对交易在论文里常常一笔带过数据预处理,但实务中这是策略能否复现的最大变量。下面这几条是必须做、且必须按这个顺序做的:

  1. 复权处理:用后复权(adjusted close)而不是不复权价格做协整检验。配股、分红、拆股都会在原始价格里造成跳变,会被 ADF 误判为「冲击 + 漂移」从而拒绝平稳假设。
  2. 停牌填充:A 股长期停牌的标的不能简单用前值填充——这会人为压低波动率、抬高协整 t 值。正确做法是把停牌期间标记为「无效观测」,估计协整时跳过这些观测,且整段策略期内停牌占比超过 5% 的标的剔除候选池。
  3. 配股与增发:除了价格复权,还要剔除配股增发宣告日附近若干交易日(通常 ±10 日)的样本,这段时间的相对估值受供给冲击主导,破坏均衡假设。
  4. 指数调整事件:被动资金在调入调出窗口的强制买卖会在前后 5 日造成可预测的价格压力。这部分价差不是均值回归,要么作为独立的事件驱动策略来做(见下一篇),要么从配对回测样本中剔除。
  5. 货币与跨地区:跨港 A、A H、ADR 套利时所有价格必须归一到同一货币、并且把汇率波动作为独立因子在回归里扣除。直接用名义价格做协整会把汇率漂移当作均值回归信号,2015 年 8 月人民币贬值前后多数 A H 套利策略翻车都源于此。

工程上推荐建一个独立的「事件日历」表,把所有上述事件按日期、标的、影响窗口存好,回测和实盘共享同一份数据,避免回测里干净、实盘里脏的不对称。


三、协整与误差修正模型

把协整讲清楚需要一点单变量时间序列的语言。一个序列如果差分一次就平稳,记为 I(1)。一组 I(1) 序列 X_1, …, X_N 协整,意味着存在一个非零向量 β 使得 β’X 平稳即 I(0)。直觉上,这个向量描绘了诸 X 之间的长期均衡关系。短期偏离允许,但偏离幅度有边界,被一个「向均衡回归」的力量压回去——这就是误差修正模型(Error Correction Model,ECM)的来源。

ADF 检验

ADF 检验回答「这个序列是否存在单位根」。原假设 H0:存在单位根(非平稳);备择 H1:平稳。其回归形式:

Δy_t = α + β t + γ y_{t-1} + Σ_{i=1}^{p} δ_i Δy_{t-i} + ε_t

检验统计量是 γ 的 t 值,临界值由 Dickey-Fuller 分布给出(不是普通 t 分布,这是初学者的第二个坑)。p 滞后阶用 AIC 或 BIC 选。statsmodels 里 adfuller 直接给出统计量、p 值与若干临界值。

Engle-Granger 两步法

Engle-Granger 把协整检验拆成两步:先 OLS 估对冲比,再 ADF 检验残差。下面是一段可运行的代码,先合成两条协整的随机游走,再做 EG 检验。

import numpy as np
import pandas as pd
from statsmodels.tsa.stattools import adfuller, coint

rng = np.random.default_rng(42)
T = 1500
common = np.cumsum(rng.normal(0, 1.0, T))
y = common + rng.normal(0, 0.5, T)
x = 0.7 * common + rng.normal(0, 0.5, T)

# Engle-Granger 第一步:OLS 估计对冲比
beta = np.polyfit(x, y, 1)[0]
spread = y - beta * x

# 第二步:对残差做 ADF
stat, pvalue, _, _, crit, _ = adfuller(spread, autolag="AIC")
print(f"beta={beta:.4f} adf_stat={stat:.3f} pvalue={pvalue:.4f}")
print(f"critical 1%={crit['1%']:.3f} 5%={crit['5%']:.3f}")

# statsmodels 内置的 EG 检验(一步到位)
eg_stat, eg_p, eg_crit = coint(y, x)
print(f"coint_stat={eg_stat:.3f} pvalue={eg_p:.4f}")

注意 statsmodels.tsa.stattools.coint 给出的临界值经过 MacKinnon(2010)的有限样本修正,与对残差直接做 adfuller 得到的 p 值不同——coint 的零假设里多估计了一个 β,自由度低一阶。生产代码里推荐直接用 coint

Johansen 检验与 VECM

EG 法在 N=2 时简洁,N≥3 时有两个问题:协整向量不唯一(理论上可有 r 个独立的协整关系,r 称为协整秩);以哪只标的作为左手变量会影响估计。Johansen 检验解决这两个问题,思路是把 N 维 I(1) 系统写成 VECM:

ΔY_t = ΠY_{t-1} + Σ Γ_i ΔY_{t-i} + ε_t

矩阵 Π 的秩等于协整秩 r。Π = αβ’,其中 β 是 N×r 协整向量矩阵,α 是 N×r 调整速度矩阵。Johansen 用迹检验(trace)和最大特征值检验(max eigenvalue)来推断 r。

from statsmodels.tsa.vector_ar.vecm import coint_johansen, VECM

T = 1200
common1 = np.cumsum(rng.normal(0, 1.0, T))
common2 = np.cumsum(rng.normal(0, 0.5, T))
data = pd.DataFrame({
    "A": common1 + rng.normal(0, 0.3, T),
    "B": 0.8 * common1 + rng.normal(0, 0.3, T),
    "C": common2 + rng.normal(0, 0.3, T),
    "D": 1.2 * common1 + 0.5 * common2 + rng.normal(0, 0.3, T),
})

result = coint_johansen(data, det_order=0, k_ar_diff=1)
print("trace stat:", result.lr1)
print("trace 5% crit:", result.cvt[:, 1])
print("max eig stat:", result.lr2)
print("max eig 5% crit:", result.cvm[:, 1])

vecm = VECM(data, k_ar_diff=1, coint_rank=2, deterministic="ci")
fit = vecm.fit()
print(fit.beta)  # 协整向量矩阵
print(fit.alpha)  # 调整速度

VECM 给出的不只是协整向量,还有调整速度 α,它直接对应每个变量在偏离均衡时被「拉回去」的幅度。后面 OU 过程的 κ 在单变量情形下就是 −α。

何时用哪种

经验上的取舍:

VECM 输出的解读

statsmodels 给出的 fit.beta 是 N × r 的协整向量矩阵,每一列代表一个长期均衡关系;fit.alpha 是 N × r 的调整速度矩阵,第 i 行第 j 列描述「第 i 个变量对第 j 个均衡关系的回归速度」。

具体到工程:

实务中通常把 r 限制在 1 至 3 之间——超过 3 的协整结构在外样本难以稳定,估计噪声会主导信号。

最后强调一点:协整检验的样本要足够长。Hamilton(1994)的经验法则是至少 250 个观测,否则 ADF 与 Johansen 的检验功效都很低,Type II 错误(把协整误判为不协整)会非常高。日频数据下这意味着至少一年多的样本。

多重检验问题

把整个市场扫一遍找配对,会面临严重的多重检验问题。N=500 个标的就有约 12 万对候选;对每对做协整检验、按 5% 显著性筛选,理论上即使所有标的都是独立随机游走、不存在任何真协整,也会有 6000 对被误判为协整。这一类伪协整在样本外几乎全部失效。

应对方法有三类:

实务中通常组合使用:先做 FDR 5% 初筛,再做样本外验证,最后人工审核(看价差曲线形态、确认没有极端跳变、确认基本面相近)。这套流程比任何单一统计检验都稳。


四、Ornstein-Uhlenbeck 过程

协整检验告诉我们「价差平稳」,但平稳不等于可交易。要把信号变成开平仓决策,需要一个能描述价差动力学的连续时间模型。Ornstein-Uhlenbeck 过程是最常用的选择:

dX_t = κ(θ − X_t) dt + σ dW_t

参数有三个:长期均值 θ、回归速度 κ(越大回归越快)、扩散系数 σ。OU 的离散化是带常数项的 AR(1):

X_{t+Δt} = X_t e^{−κΔt} + θ(1 − e^{−κΔt}) + ξ_t

ξ_t 服从均值零、方差 σ²(1 − e^{−2κΔt})/(2κ) 的正态分布。

参数估计

把 OU 离散化后做 OLS:

def estimate_ou(x, dt=1.0):
    x = np.asarray(x, dtype=float)
    y = x[1:]
    z = x[:-1]
    n = len(y)
    Sx = z.sum()
    Sy = y.sum()
    Sxx = (z * z).sum()
    Sxy = (z * y).sum()
    Syy = (y * y).sum()

    a = (n * Sxy - Sx * Sy) / (n * Sxx - Sx ** 2)
    b = (Sy - a * Sx) / n
    sd_eps = np.sqrt((n * Syy - Sy ** 2 - a * (n * Sxy - Sx * Sy)) / (n * (n - 2)))

    kappa = -np.log(a) / dt
    theta = b / (1 - a)
    sigma = sd_eps * np.sqrt(2 * kappa / (1 - a ** 2))
    half_life = np.log(2) / kappa
    sigma_eq = sigma / np.sqrt(2 * kappa)  # 平衡时标准差
    return {
        "kappa": kappa,
        "theta": theta,
        "sigma": sigma,
        "half_life": half_life,
        "sigma_eq": sigma_eq,
    }

注意几个常踩的细节:

半衰期

半衰期 t₁/₂ = ln 2 / κ 是衡量信号衰减速度的核心指标,它直接决定持仓时间的量级。半衰期 1 天的信号要求秒级到分钟级换手;半衰期 30 天的信号可以做日频持仓;半衰期超过 60 天的信号一般不值得做配对交易,因为它对结构性变化的暴露太大、风险预算无法支撑。

把半衰期和样本长度联系起来还有一条经验:估计 κ 的样本至少要覆盖 5 至 10 个半衰期,否则置信区间会大到无法做决策。半衰期 30 天意味着至少需要 6 个月以上的样本。

MLE 与不确定性量化

OU 过程在 dt 已知时的对数似然有闭式:

log L(κ, θ, σ | x_0..x_T) = -T/2 · log(2π σ²(1-e^{-2κΔt})/(2κ))
                            - Σ (x_t - μ_t)² / (σ²(1-e^{-2κΔt})/κ)

其中 μ_t = x_{t-1} e^{-κΔt} + θ(1 - e^{-κΔt})。直接用 scipy.optimize.minimize 极大化即可。MLE 的优点是给出渐进协方差矩阵(Fisher 信息逆),可以推 κ 的标准误,进而推半衰期的置信区间。

from scipy.optimize import minimize

def ou_neg_loglik(params, x, dt=1.0):
    kappa, theta, sigma = params
    if kappa <= 0 or sigma <= 0:
        return 1e10
    a = np.exp(-kappa * dt)
    var_eps = sigma ** 2 * (1 - a ** 2) / (2 * kappa)
    mu = a * x[:-1] + theta * (1 - a)
    resid = x[1:] - mu
    n = len(resid)
    return 0.5 * n * np.log(2 * np.pi * var_eps) + 0.5 * (resid ** 2).sum() / var_eps

def fit_ou_mle(x, dt=1.0):
    init = [0.1, x.mean(), x.std()]
    res = minimize(ou_neg_loglik, init, args=(x, dt),
                   method="Nelder-Mead", options={"xatol": 1e-6})
    return res.x, res.hess_inv if hasattr(res, "hess_inv") else None

实务中,把 MLE 与 OLS 都跑一遍,结果差异超过 20% 就要警惕——多半是异常值或者样本里包含一段非平稳区段。这是检测样本污染最便宜的办法之一。

开平仓阈值

Bertram(2010)给出了 OU 过程下最优开平仓阈值的解析解:在零交易成本、对称交易、单回合盈利目标的设定下,最大化单位时间收益的开仓阈值约在均值的 1.5σ_eq 附近,平仓阈值在均值附近。但实际策略中通常把开仓阈值设在 ±2σ_eq、平仓阈值设在 ±0.5σ_eq,这个保守化主要是为了处理两件事:σ_eq 估计误差与协整破裂保护。

下图展示了一段配对价差的标准化轨迹与开平仓事件示意。

配对价差时间序列与开平仓阈值

图中的 z 值是价差减去 θ 后除以 σ_eq 得到的标准化量。当 z 越过 +2σ 时认为价差「过高」,做空价差(卖 y、买 βx);越过 −2σ 时反向操作。z 重新越过 ±0.5σ 即平仓。右侧灰红区域表示协整关系开始破裂,z 长期不回归——这是几乎所有配对交易策略的灾难场景,必须配硬止损(z 超过 ±3.5σ 强制平仓并标记该配对失效)。

持仓时间的分布

OU 过程的首次穿越时间(first passage time)分布是经典问题。对从 +2σ_eq 进入、首次回到 0 的时间,期望值约为 0.99 × t₁/₂;从 +2σ_eq 进入、首次回到 +0.5σ_eq 的时间约为 0.74 × t₁/₂。这意味着如果半衰期 10 天,那么单次交易的预期持仓时间应当在 7 至 10 天量级。如果实测分布的均值远超此值,要么是 κ 估计过大、要么是协整在期间内已经破裂。把这个一致性检验做成实时监控,是判断策略是否还在「按图施工」的最敏锐指标。

与 Vasicek 和 CIR 的关系

OU 过程在利率建模里是 Vasicek 模型;如果方差项变成 σ √x 而不是常数 σ,就是 Cox-Ingersoll-Ross(CIR)。统计套利领域里偶尔会用 CIR 替代 OU 来建模严格非负的价差(比如绝对值价差或波动率价差),但代价是似然不再是高斯型,参数估计要用更复杂的方法(似然函数包含 Bessel 函数)。除非有特别强的理由,OU 已经够用——正负价差都允许、似然简单、参数解释直接。

更值得讨论的是 OU 的扩展:


五、PCA 残差套利

把策略从「找两两配对」推广到「在数百只股票里找均值回归信号」,最有影响力的工作是 Avellaneda 与 Lee 在 2010 年发表的 Statistical Arbitrage in the U.S. Equities Market。它的核心思想可以一句话讲完:把横截面的收益矩阵做 PCA,前几个主成分代表系统性风险(市场、行业、风格),残差部分即是「特异回报」(idiosyncratic return),这些残差的累积过程在大多数时间段近似 OU 过程,可以独立建模并构造一篮子市场中性的均值回归仓位。

模型框架

记 R 为 N × T 的标准化收益矩阵(每行一只股票、每列一个交易日),假设:

R_i,t = Σ_{k=1}^K β_i,k F_k,t + ε_i,t

其中 F_k 是 K 个共同因子(在 Avellaneda-Lee 里取自 R 的前 K 个主成分)、β_i,k 是因子载荷,ε_i,t 是特异回报。把残差累积成 X_i(t) = Σ_{s≤t} ε_i,s,对每只股票拟合 OU,得到 κ_i、θ_i、σ_eq,i 与对应的 z 值 s_i = (X_i − θ_i) / σ_eq,i。开仓规则:

下图把这条信号链条画成了一个图。

PCA 残差套利流程

为什么用 PCA 而不是 Barra 风格因子

两条思路都成立,Avellaneda-Lee 在论文里两种都做了。差别在于:

工程上常见的做法是混合:先用一组稳定的行业 ETF 做行业残差化,再对行业内做 PCA 提取风格主成分,最后剩下的就是真正的特异回报。这样得到的残差比直接对全市场做 PCA 稳定得多。

k 的选择

主成分数 k 是关键超参。Avellaneda-Lee 建议 15 个左右(针对 1000 多只标普 500 成分股)。理论上的指引来自随机矩阵理论的 Marchenko-Pastur 分布:N 只标的、T 个交易日、Q = T/N,则纯噪声协方差矩阵的特征值上界 λ_+ = (1 + 1/√Q)²。超过这个上界的特征值才被视为信号。N=500、T=252(一年日频)时,Q≈0.5,λ_+ ≈ 5.83,通常前 10 至 20 个特征值会显著超过这个值。

完整模拟

下面是一段从合成数据到组合权重的端到端代码,足以展示残差套利的工程框架,不依赖任何真实行情。

import numpy as np
import pandas as pd

rng = np.random.default_rng(7)
N, T = 80, 750
n_factors_true = 5

# 合成因子收益
F = rng.normal(0, 0.01, (T, n_factors_true))
# 标的对因子的载荷
B = rng.normal(0, 1.0, (N, n_factors_true))
# 特异收益(带 OU 性质的累积过程)
kappa_true = 0.04
sigma_true = 0.012
eps = np.zeros((T, N))
for t in range(1, T):
    eps[t] = (1 - kappa_true) * eps[t-1] + sigma_true * rng.normal(size=N) - kappa_true * 0
# 拼装收益矩阵 R (T x N)
R = F @ B.T + eps + rng.normal(0, 0.005, (T, N))

# ---- PCA 残差化 ----
def pca_residual(R, k):
    R_centered = R - R.mean(axis=0, keepdims=True)
    R_std = R_centered / R_centered.std(axis=0, keepdims=True)
    U, S, Vt = np.linalg.svd(R_std, full_matrices=False)
    # 前 k 个主成分构成的因子收益
    Fk = U[:, :k] * S[:k]
    Bk = Vt[:k, :]
    R_hat = Fk @ Bk
    residual = R_std - R_hat
    return residual, Fk, Bk

# ---- OU 参数估计(向量化)----
def estimate_ou_batch(X):
    # X 形状 (T, N),逐列估计 OU
    y = X[1:]
    z = X[:-1]
    n = y.shape[0]
    z_mean = z.mean(axis=0)
    y_mean = y.mean(axis=0)
    cov = ((z - z_mean) * (y - y_mean)).sum(axis=0) / (n - 1)
    var = ((z - z_mean) ** 2).sum(axis=0) / (n - 1)
    a = cov / var
    b = y_mean - a * z_mean
    resid = y - (a * z + b)
    sd_eps = resid.std(axis=0, ddof=2)
    a = np.clip(a, 1e-6, 0.999)  # 数值保护
    kappa = -np.log(a)
    theta = b / (1 - a)
    sigma_eq = sd_eps / np.sqrt(1 - a ** 2)
    half_life = np.log(2) / kappa
    return kappa, theta, sigma_eq, half_life

# 用 60 日窗口估 PCA 残差与 OU,做 RolL 出滚动信号
window = 252
k = 5
positions = np.zeros((T, N))
s_open, s_close = 1.25, 0.5
hl_min, hl_max = 5, 60

for t in range(window, T - 1):
    R_win = R[t-window:t]
    resid, Fk, Bk = pca_residual(R_win, k)
    # 残差累积过程
    X_cum = np.cumsum(resid, axis=0)
    kappa, theta, sigma_eq, hl = estimate_ou_batch(X_cum)
    s = (X_cum[-1] - theta) / sigma_eq
    valid = (hl >= hl_min) & (hl <= hl_max) & (sigma_eq > 0)

    pos = np.zeros(N)
    open_short = (s > s_open) & valid
    open_long = (s < -s_open) & valid
    pos[open_short] = -1.0
    pos[open_long] = +1.0
    # 平仓带:从前一日继承的仓位若 |s| < s_close 则归零
    held = positions[t-1]
    keep = (np.abs(s) >= s_close)
    pos = np.where(pos != 0, pos, held * keep)
    positions[t] = pos

# 估算策略收益(市场中性化)
ret_strategy = (positions[:-1] * R[1:]).sum(axis=1) / np.maximum(np.abs(positions[:-1]).sum(axis=1), 1)
sharpe = ret_strategy.mean() / ret_strategy.std() * np.sqrt(252)
print(f"Sharpe (synthetic): {sharpe:.2f}")

代码里有几个生产环境要注意的点:

  1. 主成分的方向每个窗口重新估计后会反号——SVD 不保证 V 的列方向,必须用一个一致化规则(比如让每列第一个非零元素为正)来稳定符号,否则残差累积过程会出现伪反转。
  2. kappahalf_life 必须做合理性筛选。半衰期小于交易日(例如 5 天)说明信号衰减过快、噪声主导;半衰期大于 60 天说明回归速度太慢、不值得做配对。Avellaneda-Lee 的论文里实测最佳区间是 5 至 30 个交易日。
  3. 仓位归一化要按 Σ |w| = 1 而不是 Σ w = 0;前者保证总杠杆受控,后者只保证美元中性,二者都要做。
  4. 在合成数据上看到的夏普率不能外推到真实市场。真实市场的 PCA 残差套利在 2003 至 2007 年实测夏普率约 1 至 1.5,2008 年后由于太多机构同时做、收益逐步衰减到 0.5 以下。

与配对交易的关系

PCA 残差是配对交易的高维推广。把 N=2、k=1 代入:第一主成分就是 (P_i + P_j)/√2 这种共同方向,残差就是 (P_i − P_j)/√2 这种相对方向,正是协整意义下的价差。换句话说,配对交易是 PCA 残差套利在 N=2 的特例。

Avellaneda-Lee 的 s-score 与改进

原论文里 Avellaneda 和 Lee 用一个叫 s-score 的归一化量做信号:

s_i = (X_i(T) - m_i) / σ_eq,i
m_i = θ_i + a_i / κ_i  (非零 drift 修正)

其中 a_i 是把残差累积视作带漂移的 OU 时拟合出来的漂移项。论文中给出的开仓阈值是 ±1.25 σ_eq,平仓阈值 ±0.5 σ_eq;并加了「半衰期必须在 5 至 30 个交易日之间」的硬筛选。这套阈值在 2003 至 2007 年实证里表现极佳,年化夏普率约 1.4,最大回撤约 8%。

后续学界对它做了若干改进,工程上值得借鉴的有:

与机器学习方法的关系

把 PCA 替换成自编码器(autoencoder)、变分自编码器(VAE)或更复杂的非线性降维方法,是 2018 年后学界的尝试方向。Gu、Kelly、Xiu(2020)的 Empirical Asset Pricing via Machine Learning 是这一方向的代表,他们用神经网络估计因子载荷,再把残差作为均值回归信号。结论是:在样本外,神经网络方法比 PCA 大约高 0.2 至 0.3 的夏普率,但模型黑盒、过拟合风险高、对超参敏感。

工程上的折中做法是「PCA 作为基线、神经网络作为残差补充」:先用 PCA 提取主成分作为风险模型,再用神经网络对 PCA 残差做进一步建模(比如预测残差累积过程的下一步),把神经网络当成「对 OU 模型残差的进一步残差化」。这样既保留了 PCA 的稳定性,又获得了少量的非线性增量。


六、加密资产中的统计套利

加密市场把统计套利的工程难度推向新的极端。市场全天候运行、无熔断、撮合规则碎片化、数十家交易所之间没有 NMS 类的最优执行约束、合约与现货的清算与保证金机制各家不同,使得「同一资产在不同地方价差是否存在」这件事不再是一句套话,而是日常会发生的现实。下面把三类最常见的加密统计套利分清楚。

跨所价差

同一币种在不同中心化交易所(Binance、OKX、Coinbase、Kraken 等)之间的价差。在一般行情下价差小到滑点和手续费就能吃光,但在两类事件中价差会显著放大:

跨所价差的工程难点不在于发现价差——价差摆在订单簿上人人可见——而在于资金调度。要做跨所价差,必须在多个交易所同时持有可用余额,且能在分钟到小时级别完成跨链转移。这件事在风险预算上的代价被很多策略低估:等价于持有「已经分散在 N 个对手方」的资金池,对手方风险随交易所数量线性放大。FTX 倒闭直接清零了所有把头寸放在 FTX 上做跨所套利的策略账户。

永续合约与现货基差

币安、OKX、Bybit 上的永续合约(perp)通过资金费(funding rate)锚定现货,每 8 小时结算一次。结构上:

「现货多 + 永续空」(cash-and-carry)在 perp 长期溢价的市场中是几乎稳定盈利的:现货价格波动通过对冲抵消,盈利来自累积资金费。2021 年牛市顶部 BTC 永续年化资金费率一度超过 60%,不少机构靠 cash-and-carry 拿到接近无风险的两位数年化收益。

但这条策略也有失效场景:

工程上,这类策略的关键监控指标不是基差本身,而是预估资金费的稳定性、现货端杠杆率、以及永续端保证金缓冲(margin buffer)。生产系统里通常对每个交易对维护一个「最小保证金 / 当前持仓」比例,跌破阈值就降仓而不是补保证金。

DEX 与 CEX 价差

Uniswap 等 AMM 上的代币价格由储备比例决定,存在可观察的瞬时价差与 CEX 中心限价簿上的价格之间。理论上 MEV 搜寻者不断套利会把这个差价压缩到接近 gas 成本水平。但对中等规模的代币(市值 1 亿到 10 亿美元、流动性集中在某 DEX 池)来说,DEX 与 CEX 价差经常打开到 30 至 100 bp,足以容纳手动套利策略。

工程上的复杂度集中在:

DEX-CEX 套利不属于本文意义上的「均值回归型」统计套利,更接近无风险套利(arbitrage in the strict sense);但它常被一并归入统计套利的工程范畴,因为同样需要快速扫描、稳定监控、风险预算约束。

资金费率的简单计算

为了让永续基差套利的工程含义具体,下面给一段计算预期 cash-and-carry 收益的代码。

def expected_cash_carry_yield(funding_rates, fee_taker=0.0004, fee_maker=0.0002,
                               periods_per_year=365 * 3, hedge_ratio=1.0):
    """
    funding_rates: 历史每 8 小时资金费率序列(如 0.0001 表示 0.01%)
    返回年化预期收益率,扣除单次开仓的双边手续费
    """
    avg_funding = np.mean(funding_rates)
    annualized = avg_funding * periods_per_year * hedge_ratio
    # 一次开仓手续费摊销到一年(假设持有一整年)
    open_cost = fee_taker + fee_maker
    return annualized - open_cost

# 假设 BTC 永续过去三年平均资金费 0.01% 每 8 小时
hist = np.full(1000, 0.0001)
yld = expected_cash_carry_yield(hist)
print(f"期望年化收益: {yld*100:.2f}%")

实际系统中要把这个数字按月份滚动估,在资金费滑落到接近手续费水平时主动减仓——cash-and-carry 不是「开了就放着」的策略,必须有对资金费的实时监控与自动调仓。

跨所价差的执行模型

跨所价差的工程难点在于 执行:理论上看到 A 所比 B 所贵 30 bp 即可在 A 卖、B 买锁定收益,但实际过程中:

这些综合下来,名义 30 bp 的价差落到口袋里通常只剩 5 至 15 bp。能不能稳定盈利,靠的是基础设施延迟、撮合通道质量、与多账户资金调度,而不是数学模型的精度。


七、风险与失败模式

统计套利在过去四十年里出过三次大事故:1998 年 LTCM 清算、2007 年 8 月的 Quant Quake、2020 年 3 月的去杠杆。三件事的共同结构都是「相关性突变 + 杠杆放大 + 流动性枯竭」三重叠加。这一节按失败模式归纳。

协整破裂

最朴素的失败:策略上线后,被套利的协整关系在样本外不再成立。原因可能是:

工程对策不是「想办法识别基本面」——这超出了统计套利的能力范围——而是配置硬止损与配对淘汰机制。一对策略连续触发 2 至 3 次最大止损、或者其残差的滚动 ADF p 值连续 30 个交易日大于 0.10,就把它从策略池中移出,等待至少 60 天再重新检验。

结构性变化

比协整破裂更普遍、更隐蔽的失败模式。它指的不是某一对协整突然消失,而是整个市场的横截面相关结构发生持续变化,导致 PCA 残差套利那类全市场模型整体收益衰减。2003 至 2007 年是统计套利的黄金期,标普 500 横截面相关性大约 0.3,残差套利空间大;2008 年后相关性长期上升到 0.5 以上,特异回报占比下降,整类策略的夏普率随之被压缩。

监控这一类风险的指标不是策略层面的盈亏,而是横截面残差方差占总方差的比例——可以理解为「市场里到底有多少特异回报可供套利」。这个比例是一个慢变量,月度统计就足够。如果连续 6 个月明显下降,应当主动降低整类策略的资金分配。

流动性冲击与拥挤交易

2007 年 8 月 6 日至 9 日的「Quant Quake」事件是几乎所有量化机构的噩梦。它的结构是:某家大机构因外部原因(据后续披露,是子基金被赎回)被迫平仓其统计套利组合,由于市场上其他多家机构持有高度相关的因子暴露和残差头寸,这家机构的卖出引发别人的止损,止损又引发别人的止损,三天内全行业的市场中性策略集体亏损 20% 到 30%,多年累积的收益化为乌有。

Khandani 和 Lo(2007)在事后复盘里给出了关键发现:策略本身没有出错,错的是「同一类信号被太多机构同时持有」。这就是 crowding(拥挤)。统计套利的拥挤度可以从两个角度估:

监控拥挤的工程做法是定期估计「我的组合若以日均成交量的 5% 速度平仓需要多少个交易日」。这个数字一旦超过 5,就说明组合规模相对市场流动性已经偏大,必须主动降仓——不是在风险事件发生时降,而是事先就把规模控制在临界以下。

LTCM 事件回顾

Long-Term Capital Management 在 1994 至 1998 年间是华尔街的明星,团队包含两位诺贝尔奖得主(Merton 与 Scholes)和原所罗门兄弟的核心交易员。它的核心策略不是股票统计套利,而是利率与信用利差的相对价值套利——但失败逻辑完全可以套到统计套利上。

简化版:LTCM 押注美国国债 on-the-run(最新发行)与 off-the-run(早期发行)之间的流动性溢价会向均值回归,杠杆率高峰时达到 25 倍以上。1998 年 8 月俄罗斯主权违约后,全球投资者抛售低流动性资产、买入最高流动性的 on-the-run 美债,原本应当回归的利差被进一步打开而不是收敛。LTCM 的对手方逐步压缩信贷额度并要求追加保证金,进入死亡螺旋:

  1. 利差扩大 → 浮亏增加;
  2. 浮亏触发追加保证金;
  3. 追加保证金需求超过现金储备,被迫部分平仓;
  4. 平仓加剧利差扩大,回到 1。

最终美联储召集 14 家大银行注入 36.25 亿美元接管 LTCM 仓位,避免了一次连锁清算。

LTCM 在巅峰时管理约 1300 亿美元的总头寸,但本金只有约 47 亿美元——杠杆率约 28 倍,再加上通过场外衍生品(互换、远期、期权)放大的隐含杠杆率,总名义敞口估计接近 1.25 万亿美元。Lowenstein(2000)那本 When Genius Failed 是这段历史最详尽的一手记录,对工程师而言它最值得反复读的部分不是策略本身,而是「为什么所有人都看到了风险,但谁都没能阻止」——这是关于风险治理的故事,而不是关于模型的故事。

LTCM 事件给统计套利的三条工程教训:

三次大事故的横向对比

把 1998、2007、2020 三次大事件的关键参数对照一下:

维度 LTCM 1998 Quant Quake 2007 Covid 抛售 2020-3
触发事件 俄罗斯主权违约 单一基金被迫平仓 全球封城与流动性紧张
主要受损策略 利率相对价值 股票市场中性 多品种相对价值、基差
损失幅度 LTCM 净值 -92%、行业链锁震荡 头部基金 3 日 -25% 至 -30% 多策略基金 -8% 至 -15%
持续时间 约 6 周 约 5 个交易日 约 4 周
拥挤度因素 衍生品对手集中 多机构同因子敞口 杠杆 + ETF 抛售
央行介入 美联储协调注资 无直接干预 美联储设立 PMCCF/SMCCF

共同模式相当清晰:「外部冲击 + 拥挤头寸 + 杠杆约束 + 流动性枯竭」。这四件事独立出现都不致命,叠加在一起就会触发死亡螺旋。

黑天鹅之外的灰天鹅

除了上面这些极端事件,统计套利在日常运营中还会被一些「灰天鹅」反复折磨:

工程上推荐建一个「事件日历」表(与第二节提到的复权事件表合并),在策略层做事件感知的仓位调整:对临近事件的标的暂停开仓、提前平仓或缩仓。


八、工程实现

把前面所有内容收拢到工程系统,可以画成一条 pipeline:

原始行情 → 数据清洗 → 候选筛选 → 协整 / 残差化 → OU 估计 →
信号生成 → 风险约束 → 组合构建 → 执行 → 实时监控 → 离线再估计

下面只挑工程上最容易出问题的几个环节展开。

多组合并行扫描

候选配对的数量是 O(N²),N=1000 时有近 50 万对;如果还要对每对做协整检验、估 OU、滚动窗口验证,朴素实现的算力会爆炸。工程优化通常分两层:

实际生产环境里,PCA 残差路线比配对枚举更适合大 N,因为它的算力是 O(N²T) 级 SVD,而不是 O(N²) 个独立小问题。

稳定性监控

监控不是事后查,而是把每个策略的「健康度」做成实时指标。建议至少配六个:

指标 目的 触发动作
滚动 ADF p 值 协整是否仍然成立 连续 30 日 > 0.10:暂停
半衰期估计 OU 假设稳定性 偏离训练值 50% 以上:标记
实际持仓时间 vs 理论 信号衰减节奏 偏差超 100%:复盘
单日最大回撤 / 历史 95% 分位 极端事件早期信号 突破:触发降仓
横截面残差方差占比 整类策略空间 月度连续下降 6 月:缩资金
平仓所需流动性 拥挤度 估值 > 5 日:硬性降仓

这些指标都是能在每天收盘后增量更新的。把它们画成单页 dashboard,关键阈值用红绿灯展示,是统计套利运营的最低基础设施。

风险预算

统计套利的风险预算应该按多个维度同时设上限,而不是只盯单一夏普率或最大回撤:

class RiskBudget:
    def __init__(self):
        # 每对配对的最大总美元持仓
        self.per_pair_notional = 5_000_000
        # 单只标的在所有策略合并下的最大持仓
        self.per_symbol_notional = 20_000_000
        # 单一行业最大暴露
        self.per_sector_notional = 100_000_000
        # 单一对手方(交易所、券商)最大资金
        self.per_counterparty = 50_000_000
        # 总杠杆率
        self.gross_leverage = 6.0
        # 总美元中性偏差
        self.net_leverage = 0.05
        # 流动性约束:组合 95% 平仓所需交易日
        self.liquidation_days = 3.0

    def check(self, portfolio):
        # 每个上限单独 check,任一违反返回 False
        ...

每条约束都对应一个独立的真实风险来源——per_pair_notional 控制单一配对失效的损失;per_counterparty 控制 FTX 类事件;liquidation_days 控制拥挤度。这些约束彼此不可替代,不能用风险归因模型「净化」掉其中任何一条。

离线再估计与样本污染

PCA 残差与 OU 参数都是滚动估计的,但「滚动」本身有两个常见错误:

测试体系

最后一条工程要求:每个策略上线前必须通过三层测试。

  1. 单元测试:协整检验、OU 估计、信号计算函数的代码层正确性,包括边界条件(数据缺失、NaN、单位根边界)。
  2. 历史回测:至少跨越一个完整周期(含上行、下行、震荡),夏普率、最大回撤、换手率、单笔成交占比都要看,不能只看最终曲线。
  3. 样本外仿真盘(paper trade):3 至 6 个月的实盘信号但不下单,对比仿真盈亏与回测预期的偏差。偏差超过 30% 就要停下来排查。

跳过任何一层测试直接上线的策略,几乎都会在头一年内出事。

一段最小可用的回测骨架

下面把前面散落的代码片段整合成一段最小可用的回测骨架,足以承载从协整到下单决策的完整链路。生产系统当然要远比这复杂,但所有要素都在这里。

import numpy as np
import pandas as pd
from dataclasses import dataclass
from typing import Dict, List

@dataclass
class PairConfig:
    symbol_a: str
    symbol_b: str
    beta: float
    theta: float
    sigma_eq: float
    half_life: float
    s_open: float = 2.0
    s_close: float = 0.5
    s_stop: float = 3.5

class PairBook:
    def __init__(self):
        self.pairs: Dict[str, PairConfig] = {}
        self.positions: Dict[str, int] = {}  # +1 / -1 / 0 表示价差方向

    def add(self, key: str, cfg: PairConfig):
        self.pairs[key] = cfg
        self.positions[key] = 0

    def evaluate(self, prices: Dict[str, float]) -> List[dict]:
        orders = []
        for key, cfg in self.pairs.items():
            pa = prices.get(cfg.symbol_a)
            pb = prices.get(cfg.symbol_b)
            if pa is None or pb is None:
                continue
            spread = pa - cfg.beta * pb
            z = (spread - cfg.theta) / cfg.sigma_eq
            pos = self.positions[key]

            # 硬止损
            if abs(z) > cfg.s_stop and pos != 0:
                orders.append({"key": key, "action": "stop", "z": z})
                self.positions[key] = 0
                continue

            # 平仓
            if pos != 0 and abs(z) < cfg.s_close:
                orders.append({"key": key, "action": "close", "z": z})
                self.positions[key] = 0
                continue

            # 开仓
            if pos == 0:
                if z > cfg.s_open:
                    orders.append({"key": key, "action": "short_spread", "z": z})
                    self.positions[key] = -1
                elif z < -cfg.s_open:
                    orders.append({"key": key, "action": "long_spread", "z": z})
                    self.positions[key] = +1
        return orders

class StatArbBacktester:
    def __init__(self, book: PairBook, slippage_bp: float = 2.0,
                 fee_bp: float = 1.0):
        self.book = book
        self.slippage_bp = slippage_bp
        self.fee_bp = fee_bp
        self.pnl = []
        self.entries: Dict[str, dict] = {}

    def run(self, price_panel: pd.DataFrame):
        # price_panel: index 是日期,列是各 symbol 收盘价
        for ts, row in price_panel.iterrows():
            prices = row.to_dict()
            orders = self.book.evaluate(prices)
            day_pnl = self._mark_to_market(prices)
            day_pnl -= self._apply_orders(orders, prices)
            self.pnl.append({"ts": ts, "pnl": day_pnl})
        return pd.DataFrame(self.pnl).set_index("ts")

    def _mark_to_market(self, prices):
        total = 0.0
        for key, pos in self.book.positions.items():
            if pos == 0 or key not in self.entries:
                continue
            cfg = self.book.pairs[key]
            entry = self.entries[key]
            curr = prices[cfg.symbol_a] - cfg.beta * prices[cfg.symbol_b]
            total += pos * (curr - entry["spread"])
        return total

    def _apply_orders(self, orders, prices):
        cost = 0.0
        for o in orders:
            cfg = self.book.pairs[o["key"]]
            spread = prices[cfg.symbol_a] - cfg.beta * prices[cfg.symbol_b]
            notional = abs(prices[cfg.symbol_a]) + abs(cfg.beta * prices[cfg.symbol_b])
            cost += notional * (self.slippage_bp + self.fee_bp) / 1e4
            if o["action"] in ("long_spread", "short_spread"):
                self.entries[o["key"]] = {"spread": spread}
            else:
                self.entries.pop(o["key"], None)
        return cost

这段骨架已经把回测的主要陷阱避开了:信号生成与执行分离、滑点和手续费按 notional 折算、止损独立于信号、组合层与单对层解耦。它的问题在于没有处理:保证金、做空借券费、配对失效后的重新筛选、风险预算检查。这些都是把骨架变成生产系统所必须补的部分,但已经超出了一篇文章的篇幅。

实盘上线的最后一公里

策略在回测里夏普率好看不等于上线就能运行。最容易被忽视的「最后一公里」工程清单:

  1. 撮合差异:回测假设以收盘价成交,实盘是以收盘前若干分钟的 VWAP 或开盘的 TWAP 成交,价差大约 5 至 15 bp。这个差异会把回测里勉强盈利的策略直接打回原形。
  2. 借券约束:A 股做空必须借券(融券),并不是所有标的都能借到,借券利率波动大。回测里假设无成本做空、上线时借不到券或借券利率超过预期收益的情况比比皆是。
  3. 资金占用与杠杆约束:券商的两融保证金比例不是常数,会因标的、市场状态变化。回测里假设的杠杆率上线后可能根本无法达到。
  4. 税收与费用:印花税、过户费、券商佣金、ECN 费、监管费汇总起来通常比模型假设高 30% 至 50%。
  5. 延迟与抢单:实盘里下单后到撮合之间的延迟有概率被其他算法抢先一步,导致预期成交量不足。
  6. 回报送达延迟:交易所回报到达策略层的延迟从几毫秒到几百毫秒不等,对秒级策略影响显著。
  7. 盘中熔断与停牌:A 股、港股都有此机制;策略下单时标的可能临时停牌、订单被退回,必须有兜底逻辑。
  8. 节假日、半日盘、调整时段:每个市场都有自己的特殊交易日历,回测如果用错日历会算错累积收益。

把这些都做对,统计套利策略才有可能稳定上线。否则回测里 1.5 的夏普率到实盘只剩 0.5 是常态。


九、本文小结

统计套利不是某个具体策略,而是一类有明确数学结构的策略族:长短并举、价差中性、收益来自平稳线性组合的均值回归。从配对交易开始,距离法和相关系数法只是粗略筛选,真正能支撑生产策略的是协整与误差修正模型;OU 过程把「价差平稳」翻译成可量化的开平仓阈值与持仓时间;PCA 残差把同一个思路推广到几百只标的的全市场版本。

加密资产把这套框架的边界推得更远——24/7 运行、跨所流动性碎片化、永续与现货基差带来新维度的套利,但也带来对手方风险与流动性风险的指数级放大。LTCM 与 Quant Quake 的两次事故告诉我们,统计套利的失败极少来自模型计算错误,多数来自「相关性突变 + 杠杆放大 + 流动性枯竭」的三重叠加。工程系统的核心任务不是把模型做得更精,而是把这三类风险的早期信号变成可监控的实时指标,并配置独立于策略本身的风险预算。

全文核心要点回顾

按章节顺序提炼:

这是一类对统计直觉、工程可靠性、风险纪律三者要求都很高的策略。它没有银弹,没有可以一招通吃的因子,没有跨时间稳定的最优参数。能在十年时间尺度上持续运行的,几乎都是把上面这些工程细节做到极致的团队。

给工程师的几条务实建议

如果你刚开始接触这个方向,下面这几条踩坑笔记可能比任何论文都有用:

  1. 从两两配对开始,不要一上来做 PCA 残差。两两配对的失败模式简单可识别,PCA 残差的故障定位极其困难——某个残差异常你都不知道是因子方向反转、协方差崩坏、还是数据污染。
  2. 把回测框架与信号研究分离。研究阶段允许用 in-sample 拟合、超参扫描;回测框架要严格按时间因果,并对所有滚动统计量加 lookback 截断。两者用同一份代码会反复在前视污染上栽跟头。
  3. 做基础设施投资比做模型优化更值钱。一台干净的、能在 5 分钟内跑完 5 年滚动协整扫描的实验机器,比一个夏普率高 0.1 的复杂模型对长期产出的影响更大。
  4. 风险监控用红绿灯而不是连续指标。值班的人不需要知道某指标精确数值,只需要知道哪些指标进入了警戒区。这要求每个指标都有明确的阈值,并按阈值上色。
  5. 把每次失效写进事后笔记。哪个配对怎么失效的、失效前是否已有信号、止损是否及时、停用流程是否走对,每一项都写下来。半年后回看,会发现很多失效有共同的早期征兆。

这个领域接下来会怎么走

近年统计套利领域有几个值得关注的方向:

这些方向都没有彻底改变本文描述的核心框架,但它们改变了信号源约束集——而这正是统计套利在过去四十年里反复演化的方向。框架不变,叶子变化。

一句话总结

如果只能记住一句话:统计套利不是关于赚钱,而是关于在协整关系成立的窗口内反复执行;它的工程难度全部都在「识别协整成立的窗口」与「在窗口外快速止损」上。所有更复杂的数学只是这两件事的展开。把这句话刻在监控大屏的页眉上,比读十本书都有用。

十、参考文献

  1. Engle R F, Granger C W J. Co-Integration and Error Correction: Representation, Estimation, and Testing. Econometrica, 1987, 55(2): 251-276.
  2. Johansen S. Estimation and Hypothesis Testing of Cointegration Vectors in Gaussian Vector Autoregressive Models. Econometrica, 1991, 59(6): 1551-1580.
  3. Avellaneda M, Lee J H. Statistical Arbitrage in the U.S. Equities Market. Quantitative Finance, 2010, 10(7): 761-782.
  4. Gatev E, Goetzmann W N, Rouwenhorst K G. Pairs Trading: Performance of a Relative-Value Arbitrage Rule. Review of Financial Studies, 2006, 19(3): 797-827.
  5. Khandani A E, Lo A W. What Happened to the Quants in August 2007? Journal of Investment Management, 2007, 5(4).
  6. Bertram W K. Analytic Solutions for Optimal Statistical Arbitrage Trading. Physica A, 2010, 389(11): 2234-2243.
  7. Lowenstein R. When Genius Failed: The Rise and Fall of Long-Term Capital Management. Random House, 2000.
  8. Hamilton J D. Time Series Analysis. Princeton University Press, 1994.
  9. MacKinnon J G. Critical Values for Cointegration Tests. Queen’s Economics Department Working Paper No. 1227, 2010.
  10. do Prado M L. Advances in Financial Machine Learning. Wiley, 2018.
  11. Vidyamurthy G. Pairs Trading: Quantitative Methods and Analysis. Wiley, 2004.
  12. Pole A. Statistical Arbitrage: Algorithmic Trading Insights and Techniques. Wiley, 2007.
  13. Tsay R S. Analysis of Financial Time Series. 3rd ed. Wiley, 2010.
  14. statsmodels documentation. tsa.stattools.coint, tsa.vector_ar.vecm. https://www.statsmodels.org/
  15. Avellaneda M. Lecture Notes on Statistical Arbitrage. NYU Courant Institute, 2011.

导航:上一篇 【量化交易】因子动物园:价值、动量、质量、低波 | 下一篇 【量化交易】事件驱动策略:并购、财报、指数调整

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-05-01 · quant

【量化交易】量化交易全景:从信号到订单的工程链路

量化交易不是策略写得好就能赚钱,更难的是把数据、特征、因子、信号、组合、执行、风控、复盘这八段链路在工程上连成一条不漏数据、不串时间、不丢订单的流水线。本文是【量化交易】系列的总目录与读图,给出八段链路的输入输出、失败模式、不变量清单,并用研究流程图把从一个想法到一笔实盘订单之间所有该过的卡点串起来。

2026-05-01 · quant

【量化交易】市场结构:交易所、做市商、暗池、ECN

系统梳理全球市场结构(Market Structure)的工程图景:从证券交易所、衍生品交易所、加密交易所,到做市商、暗池、ECN/ATS,再到 Maker-Taker 收费、PFOF、Reg NMS 与 MiFID II 的监管影响;给出量化策略选择交易场所的判断框架与基于 ccxt 的多交易所行情聚合代码。

2026-05-01 · quant

【量化交易】市场微结构:订单簿、价差、流动性、冲击

系统讲解市场微结构的核心概念与可计算工具:限价订单簿的数据模型、报价/有效/已实现价差、Roll 模型、四维流动性度量、Kyle's lambda、订单流不平衡(OFI)、Almgren-Chriss 框架下的临时与永久冲击、PIN 与 VPIN、Hawkes 过程,并给出基于 polars 的 L2 增量处理与系数估计代码。

2026-05-01 · quant

【量化交易】订单类型与执行语义:限价、市价、IOC、FOK、冰山

把 Limit、Market、IOC、FOK、Iceberg、Stop、MOO/MOC 这些常被混为一谈的订单类型还原为价格、数量、时效、可见性、触发五个独立维度,并对照 A 股、港股、美股、CME、Binance 五个市场的实际语义差异,给出量化系统中的订单工厂、状态机与风控前置校验的工程实现。


By .