最近比较对套利感兴趣,今天我们来分享一个比较复杂的套利思路。先前的我们使用 EWY 和三星、海力士进行了套利,但是毕竟需要你对市场有一个准确判断的眼光,那么有没有办法帮助我们筛选出目标标的进行套利呢,今天我们来谈一下。
一、EWY 那笔交易,你真正付出的成本是什么
先把上次的事情讲清楚。EWY 是韩国 ETF,三星和海力士是它里面最重的两个成分。当 EWY 相对成分股显得贵了,空 EWY、多成分,等价格回到该在的位置。
这笔交易没问题。但它有一个隐含成本,很少有人算过:你为了找到这三个标的,事先付出了多少认知。
你得知道 EWY 是韩国 ETF;得知道三星海力士在里面权重最大;得知道存储周期怎么传导;得知道韩元汇率会在多大程度上污染这个价差。这些东西不在数据里,在你脑子里。它们是你多年积累的资产,但也正因为是资产,它不可复制、不可扩展、不可外包给机器。
于是就有了这个策略的全部动机:
全市场几百个标的,两两组合几万对,三个一组几百万组。你能盯几个?
配对交易的天花板不在收益率上,在你一次只能想清楚一件事上。今天我们要做的,是把"想清楚哪些标的该绑在一起"这一步,从人手里拿走,交给一个可以每天重跑一遍的流程。
二、第一步:把价格摆成矩阵
所有事情从一个非常朴素的动作开始——把价格摆成一张表,横轴时间、纵轴标的、格子里填对数价格。
python
P = np.array([[series[s][t] for s in kept] for t in ts], dtype=float)
L = np.log(P) # T×N 的对数价格矩阵
用对数不是为了好看。套利关心的永远是比例关系,而对数把比例变成了差:股价从 10 到 20 和从 100 到 200,在对数上是同一个数字。你后面所有的线性代数——加权、投影、回归——才有意义。
这个矩阵 X,T 行 N 列,就是接下来全部工作的对象。它长什么样,决定了这堆标的之间有没有可套的东西。
三、十只半导体股票,其实只有一只
把一段时间的半导体标的摆进去看:AMD、ARM、ASML、INTC、MRVL、NVDA、TSM,再加上 QQQ 和 SPY。
肉眼就能看出来:这些列高度雷同。一个涨大家涨,一个跌大家跌。
这在数学上叫低秩——矩阵有 10 列,但实际的自由度远小于 10。把它拆开的工具是 SVD:
python
_, S, Vt = np.linalg.svd(Xc, full_matrices=False)
V = Vt.T
Vf = V[:, :k] # 共同趋势方向
Vn = V[:, k:] # 零空间
k 取几,是整个策略的第一个真问题。 取小了,把真信号当噪声,套利组合里残留了方向性风险;取大了,把噪声当信号,你在赌一个不存在的结构。
不能拍脑袋,也不能用碎石图目测。这里用 Marchenko-Pastur:随机矩阵理论给出了"纯噪声情况下特征值最大能到多少"的解析上界。凡是超过这个上界的,就不可能是噪声凑出来的。
python
def mp_factor_count(returns):
T, N = returns.shape
q = float(N) / float(T)
upper = (1.0 + math.sqrt(q)) ** 2 # 纯噪声的特征值上界
ev = np.linalg.eigvalsh(C)
return int(np.sum(ev > upper)) # 超出的才算真因子
这个判据的好处是它自带样本量修正——q = N/T 直接进公式。你训练窗口越短、标的越多,上界越高、门槛越严。这正是应该的:数据少的时候本来就不该相信自己看到的结构。
跑出来的结果值得停下来看一眼:上面那 10 个标的,k = 1。
一个共同因子。十只半导体,从统计上说就是一只,加九份各自的偏离。
四、零空间:一次给你九个正交的赌注
这里是整套方法真正拉开差距的地方。
10 个标的、1 个共同因子,意味着剩下 9 个维度是"共同趋势之外"的东西。这个空间叫零空间,它的定义就是与共同趋势正交。
含义非常直接:按零空间里的任何方向配权重,共同趋势被完全抵消。
半导体整体涨 5%,你的组合不动;整体崩 8%,还是不动。留在组合里的只有一件事——谁相对偏离了。
而 EWY 那笔交易,本质就是用人脑在这个空间里手工摸到了一个方向。你猜出了一组权重,让韩国大盘因子约掉。你猜对了,所以它赚钱。
SVD 的差别在于:
它把这个空间里的 9 个正交方向一次性全给你。
正交这个词是关键。它不是"九种说法描述同一个赌注",而是九个统计上独立的赌注。一个方向回归失败,不影响另外八个。配对交易只有一个自由度,你押错就是押错;矩阵套利有 r 个,风险天然被摊开。
这就是"矩阵套利"和"配对套利"的分水岭——不是标的多了几个,是赌注的维数变了。
图 1|矩阵套利真正增加的不是标的数量,而是共同趋势之外、可以被独立监控的偏离维数。
五、这个空间能不能用,取决于奇异值间隙
但 SVD 吐出来的东西不能照单全收,这里有个足以让整套方法失效的陷阱。
SVD 按奇异值大小排序,前 k 个划给共同趋势,后面的划给零空间。问题在于,这条分界线画得稳不稳。
如果第 k 个奇异值是 100、第 k+1 个是 20,这条线稳如磐石,明天重估还是这个划分。如果是 51 和 49,那么下一次重估时,某个方向完全可能从零空间跳到因子空间去——你昨天精心算好的权重,今天指向了完全不同的组合。
这不是经验之谈,是有严格界的。Davis-Kahan / Wedin sinθ 定理给出:
text
子空间的旋转 ≤ 噪声大小 / 奇异值间隙
分母就是那个间隙。它是稳定性的定量度量,不是修辞。间隙翻倍,同样噪声下的旋转减半。
所以它是一道硬门槛:
python
gap = float(S[k - 1] / S[k])
if gap < MIN_GAP: # 默认 1.30
return None, "奇异值间隙不足 {:.2f}<{:.2f}".format(gap, MIN_GAP)
理论保证之外,还要做一次实证。方法很直接:把训练窗口劈成前后两半,各估一次零空间,量两者的主夹角。
python
half = eff // 2
angles = []
for seg in (train[:half], train[half:]):
Sc = seg - seg.mean(axis=0)
_, _, vt = np.linalg.svd(Sc, full_matrices=False)
angles.append(principal_angles_deg(Vn, vt.T[:, k:]))
if max(angles) > MAX_ANGLE_DEG: # 默认 20°
return None, "子空间旋转 {:.1f}°>{:.1f}°".format(max_angle, MAX_ANGLE_DEG)
这一问非常尖锐:前半段数据算出来的套利方向,和后半段算出来的,指的是同一个方向吗?
夹角小,说明这个结构是真实存在的经济关系;夹角大,说明你只是在给一段特定时期的噪声起了个名字。
看这一关刷掉了什么:
text
6 BMNR,COINX,CRCLX,HOODX,MSTRX,ORCLX 子空间旋转 49.6°>20.0°
6 AMZNX,GOOGLX,IBM,METAX,MSFT,PLTRX 子空间旋转 87.0°>20.0°
13 AAPLX,AMZNX,BABA,CXMT,FUTUON,GOOGLX 子空间旋转 80.0°>20.0°
87 度,几乎正交——前后两段给出的"套利方向"完全不相干。
注意这一关的性质:它拦掉的恰恰是最有说服力的那批。 大科技股相关性肉眼可见,任何一个人来手工挑篮子都会先挑它们。相关性高不等于关系稳定,而可交易的从来是后者。这一步替你省掉的钱,比它带来的机会更多。

图 2|奇异值间隙比负责检查因子空间与零空间的分界,主夹角负责检查这组套利方向在时间上有没有旋转。
六、两个统计量:一个找错位,一个防接刀
篮子定下来,进入实时监控。这里需要两个统计量,职责完全不同。
Q-score:结构错位有多深
把当前价格投影到零空间,用训练期的残差协方差做马氏距离:
python
rvec = xc @ Vn
q_raw = float(rvec @ np.array(model["resid_inv"]) @ rvec)
为什么必须是马氏距离而不是欧氏距离? 因为零空间里的 r 个方向波动量级完全不同,有的方向天然活跃、有的方向常年贴着零。用同一个绝对阈值去卡,等于只监控了波动最大的那一两个方向,剩下的形同虚设。马氏距离用每个方向自己的历史协方差做归一化,把 r 个方向拉到同一个尺度上。
回报是:如果协整成立,这个量服从 χ²(r)。阈值不用你调,直接查分布:
python
"q_entry": float(sps.chi2.ppf(0.995, r)), # 千五分位,报警
"q_exit": float(sps.chi2.ppf(0.80, r)), # 回到八十分位,离场
这是一个被低估的好处:参数越少,过拟合的空间越小。凡是能从分布里读出来的数,就不要留给自己去"优化"。
F-shock:整个因子空间是不是在剧烈重定价
这个统计量的存在意义只有一个:阻止你在错误的时候做正确的动作。
设想 Q-score 报警了,你冲进去做回归。但实际情况是美联储刚刚给出了超预期的表态,整个半导体板块在集体重新定价。价格不会回来,因为它本来就该去那儿。
所以要单独盯共同趋势本身的变动速度:
python
f = xc @ Vf # 因子得分
d = f - np.array(prev["f"]) # 一阶差分
fshock = float(d @ np.array(model["f_inv"]) @ d)
开仓需要两个条件同时成立:
python
"alarm": q > model["q_entry"], # 结构确实错位
"blocked": fshock > model["f_limit"], # 但因子在剧烈变动 → 不开
这两个统计量的组合,其实是在回答同一个问题的两面:
Q 高、F 低 —— 系统性因素没变,个体相对位置错了。这是可交易的偏离。
Q 高、F 高 —— 系统性因素正在变。这不是偏离,是重定价。
顺带说一句,经典的 MSPC(多变量统计过程控制)里的 T²/Q 统计量假设变量是平稳的,而对数价格是 I(1) 的,不能直接套用。这里的改造点在于:Q 建在零空间残差上(协整成立时平稳),F 建在因子的一阶差分上(差分后平稳)。两个统计量都被显式地移到了平稳的地基上,否则卡方分布的假设根本不成立,阈值也就没意义。

图 3|真正想要的是左上角:结构错位已经够深,但共同因子仍然平静。右上角更像事件驱动的重新定价。
七、归因:残差是组合的属性,不是标的的属性
Q-score 报警只告诉你"这个组合错位了"。它没有、也不可能告诉你是谁的问题——残差是投影出来的,它属于整个组合。
定位需要另一套东西:leave-one-out 回归。
python
for i in range(N):
others = [j for j in range(N) if j != i]
A = np.column_stack([np.ones(eff), train[:, others]])
b, _, _, _ = np.linalg.lstsq(A, train[:, i], rcond=None)
res = train[:, i] - A @ b
loo.append({"i": i, "others": others,
"beta": b.tolist(), "sd": float(res.std(ddof=1))})
思路是:用其余 N−1 个标的去重构标的 i。如果重构得好,说明 i 没有特有偏离;如果重构残差异常大,偏离就落在 i 身上。
实时监控时每个标的算一个 z:
python
pred = b[0] + float(b[1:] @ x[rec["others"]])
z = (x[rec["i"]] - pred) / rec["sd"]
实盘的一次输出:
text
ARM:-5.604 NVDAX:-3.099 INTC:2.548 ASML:1.564
ARM 偏离 5.6 个标准差,方向为负——其余九个标的共同给出的定价明显高于 ARM 当前的价格。
仓位就据此展开:多 ARM,其余九条腿按回归系数 β 反向配平。
python
sgn = 1.0 if top["z"] > 0 else -1.0
w = {top["symbol"]: -sgn}
for pos_j, j in enumerate(rec["others"]):
w[model["members"][j]] = sgn * float(b[1 + pos_j])
这里有个容易被忽略的性质:权重不是你选的,是回归系数给的。而 OLS 的正规方程保证了残差与所有回归元正交——也就是说,这组权重天生对共同因子中性。你不需要额外去做 beta 中性化,它是从构造里掉出来的。
这就是自动化版本的 EWY 三角。只不过它有十条腿,而且每天重估一次。
八、三个坑:样本内完美,样本外稳定亏钱
上面是方法论。下面是我认为更值钱的部分——三个会让这套东西在实盘里持续失血的具体陷阱。它们的共同特征是:回测漂亮,实盘阴跌。
其一:杠杆 ETF 的波动率衰减
SOXL 是 3 倍做多半导体,TQQQ 是 3 倍做多纳指。它们和标的的相关系数能到 0.99,看上去是天赐的套利对手。
但杠杆 ETF 每日再平衡,这导致一个数学上必然的结果:
text
ln(3x ETF) ≠ 3 × ln(标的)
差额是一个确定性向下的漂移,量级由标的波动率决定。你把它配进协整组合,样本内拟合会非常好(因为漂移在样本内也被 μ 吸收了一部分),上线之后每天亏一点,稳定地亏。这不是运气问题,是它的产品设计决定的。
识别方式:相关性极高但波动率显著更大的,判为杠杆版本。
python
if abs(C[i, j]) >= DEDUP_RHO: # ρ > 0.97
hi, lo = (i, j) if vol[i] > vol[j] else (j, i)
if vol[hi] / vol[lo] >= DEDUP_VOL_RATIO: # 波动比 > 1.40
drop_idx.add(hi)
##剔除 SNXX,保留 SNDK,相关系数 0.9813,波动比 2.15x
但这只抓得住成对的。SOXL 是 3 倍指数,它和池子里任何单个成分的相关性都到不了 0.97,会大摇大摆走进篮子。所以篮子内部需要第二道:
python
med_vol = float(np.median(vols))
out = [i for i in range(len(kept)) if vols[i] > VOL_OUTLIER_MULT * med_vol]
日志里的处理结果:
text
6 AMD,ARM,INTC,MRVL,SOXL,TSM 剔除波动异常成员['SOXL']后规模不足
整个篮子作废,也不让它进来。这个取舍是对的——一个被污染的篮子的期望收益是负的,而放弃它的成本只是少一个候选。
其二:财报跳空,与"错位"在形态上完全一致
美股财报盘后发布,而这些永续 24 小时交易。
那一刻的画面是:整个板块几乎不动,单一标的瞬间跳 8%。
在 Q-score 和 LOO 归因看来,这与教科书级的套利机会在数据上无法区分:组合残差飙升,单腿 z 值拉到 6 以上,其余腿纹丝不动。
但这是永久性重估。价格不会回来,你做的不是均值回归,是接飞刀。
区分点不在幅度,在速度。流动性失衡是逐步拉开的,事件重定价是一步到位的:
python
if q - prev["q"] > model["q_entry"] * JUMP_MULT: # 单轮跳变超过报警线 60%
jump_until = now + JUMP_COOL_S # 冷却 4 小时
这个判据不完美——它会误伤一部分真实的快速偏离。但错过一次机会的成本,和接一次财报刀的成本,不在一个量级上。在这种非对称下,宁枉勿纵是理性的。
其三:日内季节性,白天和深夜不能共用一把尺
永续 24 小时运行,但真实美股只在特定时段开盘。美股盘中时段的残差波动,和亚洲深夜时段可以差一个数量级。
如果混在一起估阈值,结果是双向失准:盘中太松,漏掉真机会;深夜太紧,满屏假报警。
有个诱人的懒办法是直接把非交易时段的数据丢掉。但这恰恰丢掉了最值钱的部分——财报就发在那个时候,那是"整队不动、单腿跳出去"最纯粹的形态。
正确做法是给每个时段配一把自己的尺子:
python
Q_train = np.einsum("ij,jk,ik->i", Rn, resid_inv, Rn)
slots = ((ts[-eff:] // 3600000) % VOL_SLOTS).astype(int)
for h in range(VOL_SLOTS):
sel = Q_train[slots == h]
if len(sel) >= MIN_SLOT_BARS:
slot_scale[str(h)] = float(np.median(sel)) / q_med_all
监控时用当前时段自己的因子归一化,效果直接体现在面板上:
text
Q(季节归一) 10.17 Q(原始) 14.78 时段因子 1.453
原始值 14.78,但该时段基准波动本就高 45%,归一后 10.17,未触及 18.55 的报警线。不做这一步,这就是一次假信号,而且是会反复出现的那种。
九、样本内的均值回归,一个信息量为零的现象
这一节我想说得直白一些,因为它推翻的东西比它建立的更多。
你在样本内看到的所有漂亮回归图,都不构成任何证据。
z-score 的构造方式保证了 Σz = 0。这是一个代数恒等式,不是统计发现。把纯粹的随机游走喂进去,画出来的图一样会优美地围绕零轴穿越。
均值回归在样本内是被定义出来的,不是被观察到的。
唯一有信息量的检验只有一个:
冻住训练窗口估出的参数,看之后真实发生的偏离会不会回来。
所以这套系统把样本外统计放在了面板的第一屏,压在账户资金上面:
python
def validation_stats(store):
done = [a for a in store["alerts"] if a.get("resolved") is not None]
ok = [a for a in done if a["resolved"]]
return {
"rate": len(ok) / len(done), # 报警后是否真的回归
"median_min": times[len(times) // 2], # 回归耗时
"mae": np.mean([a["peak_q"] / a["q0"] for a in done]), # 回归前最坏偏到几倍
}
三个数各有各的用处:
| 指标 | 读什么 | 不达标意味着 |
|---|---|---|
| 回归率 | 报警后 Q 回落的比例 | 低于 50%,协整前提不成立,整套逻辑作废 |
| 回归耗时中位数 | 资金要被占用多久 | 过长,资金费和机会成本会吃掉全部利差 |
| 最大不利偏移 | 回归前最坏走到几倍 | 这个数直接决定止损位和杠杆上限 |
第三个尤其容易被忽略。它衡量的是"你在正确的判断上能不能活到收敛那一刻"——一个回归率 80% 但 MAE 高达 3 倍的策略,配上不合适的杠杆,照样会在收敛前爆掉。方向对和活下来是两件事。
篮子的 gap 多漂亮、ADF 的 t 值多显著,在这张表面前都不算数。它们是前置条件,不是结论。
十、最后一公里:信号正确,订单可能是错的
还有一个非常不"数学"、但足以毁掉一切的问题。
合约按张成交,只能取整。
假设某条腿的目标是 3.7 张,floor 之后是 3 张,19% 的误差。十条腿各来一次,你实际持有的组合已经不是那个中性组合了。
后果比"少赚一点"严重得多:β 一旦失真,头寸里就残留了方向性市场风险。你以为在赌"ARM 会向组合回归",实际上你同时押了半导体的方向。那不是这个策略要承担的风险,也不是它有优势的风险。
所以下单前必须量一次实际权重与目标权重的夹角:
python
tgt = np.array([w.get(sy, 0.0) for sy in w])
act = np.array([legs[sy]["notional"] * (1 if long else -1) ...])
fidelity = float(tgt @ act / (norm(tgt) * norm(act)))
if fidelity < MIN_HEDGE_FID: # 默认 0.97
return # 配不准,就不开
顺着这个约束可以反推出一个很实用的数字:这个篮子最少需要多少敞口才配得动。
python
needs.append(float(np.max(FID_TARGET_CT * gran[nz] * aw.sum() / aw[nz])))
min_gross = float(np.median(needs))
面板上直接标出来:
text
BB136E8 最小敞口 $11,938 当前可交易 ✅
B90094A 最小敞口 $4,597 当前可交易 ✅
资金不够就不该做这个篮子。 强行做的结果不是"小一点的套利",而是"取整误差主导的方向性头寸"——性质完全变了。
另外一项常被漏掉的成本是资金费率。多空组合的 carry 不会自动抵消,各腿费率不同;而 TradFi 永续因为无法与现货套利,基差由持仓结构决定,往往有系统性偏离。这是 spread 上一项确定性漂移,不显式记账就会系统性高估收益:
python
for sym, leg in pos["legs"].items():
rate = mk["fundingRate"] * dt / mk["fundingInterval"]
acc += notional * rate if leg["side"] == "long" else -notional * rate
pos["funding_paid"] += acc
十一、整条流水线
图 4|筛选、验证、监控、执行和样本外统计构成闭环;前面所有门槛,最终都要接受样本外结果的裁决。
text
全市场数百个合约
↓ 流动性闸门(持仓量、多空用户数)
↓ 排除 pre-IPO 合成品(无外部价格锚,协整无从谈起)
约 120 个候选标的
↓ 层次聚类,距离用 √(2(1−ρ)),不引入任何行业先验
约 150 个候选篮子
↓ 剔除杠杆版本
↓ MP 定因子数 k
↓ 奇异值间隙 ≥ 1.30
↓ 前后半窗子空间夹角 ≤ 20°
↓ ADF 检验 + OU 半衰期
2~6 个通过的篮子
↓ Q-score / F-shock 实时监控
↓ 跳空过滤、日内季节性归一
↓ LOO 归因定位
↓ 整数张取整 + 对冲保真度检查
下单
↓
样本外验证统计 ← 唯一的裁判
对照实盘输出读一遍:
text
ID N k因子 r零空间 σ间隙 子空间夹角 ADF最优t 半衰期 稳定评分
BB136E8 10 1 6 4.476 11.96° -5.704 17.8bar 0.807
AMD, ARM, ASML, INTC, MRVL, NVDA, QQQ, SPY, TSM
这一行的意思是:这 10 个标的由 1 个共同因子驱动,剩余 6 个方向通过了平稳性检验;奇异值间隙 4.5 倍,分界稳固;前后半窗夹角 12°,结构在时间上一致;偏离后约 **18 根 K 线(4.5 小时)**衰减一半。
这就是机器版的 EWY 三角——十条腿,六个独立赌注,全程没有一个字来自人的判断。
十二、必须说清楚的三件事
第一,它没有强制收敛机制。 真正的套利之所以是套利,靠的是最终的物理交割:ETF 能申赎、期货能交割。而这些 TradFi 永续无法与真实美股实物交割。约束价格的只有资金费率、做市商库存和相对报价习惯——比真套利弱一个量级。所以它是统计套利,收敛是概率性的,不是保证。
第二,样本长度是当前最大的软肋。 这些合约上线不久,可能还没完整走过一个财报周期。协整关系在财报季前后会不会漂移,现有数据回答不了。所以样本外统计必须持续累积,而不是跑几天就下结论。
第三,前面所有的定理、门槛、检验,加起来只做了一件事——提高样本外成立的概率。 它们不提供保证。奇异值间隙、子空间夹角、ADF、MP 判据,全部都是必要条件而非充分条件。
所以整套东西最终只落在一张表上:
text
🔬 样本外验证
累计报警 已了结 回归率 回归耗时中位数 最大不利偏移 结论
1 0 — — — 样本不足,继续积累
回归率站上 60%,前提成立,可以谈仓位。
回归率长期低于 50%,前提不成立,关掉,别修参数。
中间没有第三种选项。修参数把回归率修上去这件事,本身就是过拟合的定义。
策略源码:TradFi 矩阵统计套利
策略默认运行在GateIo交易所,其他需要根据不同交易所进行切换;
默认paper 模式,不产生真实委托。先把样本外统计跑够,再谈钱。
- 1


