Type/to search
2
Follow
503
Followers
从配对到矩阵:让机器自己找出可以套的那一篮子
Discussions
Created 2026-08-04 17:50:08  Updated 2026-08-05 10:33:02
 0
 3

img

最近比较对套利感兴趣,今天我们来分享一个比较复杂的套利思路。先前的我们使用 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 个,风险天然被摊开。

这就是"矩阵套利"和"配对套利"的分水岭——不是标的多了几个,是赌注的维数变了

img

图 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 度,几乎正交——前后两段给出的"套利方向"完全不相干。

注意这一关的性质:它拦掉的恰恰是最有说服力的那批。 大科技股相关性肉眼可见,任何一个人来手工挑篮子都会先挑它们。相关性高不等于关系稳定,而可交易的从来是后者。这一步替你省掉的钱,比它带来的机会更多。
img

图 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 建在因子的一阶差分上(差分后平稳)。两个统计量都被显式地移到了平稳的地基上,否则卡方分布的假设根本不成立,阈值也就没意义。
img

图 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

十一、整条流水线

img

图 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 模式,不产生真实委托。先把样本外统计跑够,再谈钱。

Related Recommendations
Comment
All comments (0)
No data
No data
  • 1
Forums
PINE Language
Get the app
iPhone Download
© 2015 - ∞ INVENTOR PTE LTD (SG)