输入/搜索内容
2
关注
510
关注者
一个因子的生与死 ——EWY 三腿统计套利 43 天实盘全记录
帮助
创建于 2026-08-08 13:17:11  更新于 2026-08-08 13:17:29
 0
 100

经常有朋友说,量化文章大多停在理论,缺实践。今天讲点干的。

这篇不是策略推介,而是一份完整的病历:一个因子从发现、上线、四次迭代、稳定盈利,到最后失效的全过程。四个版本的代码都在,关键部分会贴,但更想说清楚的是每次改动背后的判断——特别是那些当时觉得对、后来发现只对了一半的判断,以及一次做了又主动删掉的功能。

先把时间线摆出来:

阶段版本发生了什么
💡 理论《海力士套利狂热之后,还有机会藏在 EWY 里》
🧪 原型v1.0 模拟盘回归 + z-score 状态机跑通
🩸 上实盘v1.0 币安版撞上单腿风险,重做整个执行层
🔧 改造v2.0 网格版单点进出改四级网格,行情改 WebSocket
❄️ 定型v2.1 冻结版锁定坐标系;发散收益尝试后删除
📈 辉煌亚盘、美盘各一轮,累计约 50%
⚰️ 消亡z 冲到 6,两天不回归

最后那一行才是这篇文章真正想写的。

赚钱的部分没什么可讲的,亏钱和失效的部分才有信息量。


一、这个因子是怎么被发现的

先讲一个没做成的生意

2026 年 6 月 2 日,币安上线了一批股票永续合约:SK 海力士、三星电子、现代汽车。

当时市场热议的是韩股现货与永续之间 3%—5% 的基差。看起来像送钱:一边买、一边卖,等它们靠拢。

但这条路算下来走不通,最核心的障碍是时间

韩股有开收盘,永续 24 小时不停。KRX 收盘之后,底层股票停在最后一笔成交上,而永续合约还在继续吸收隔夜的宏观数据、费半波动、汇率变化。

这时候你看到的"基差"有两种可能:永续在提前反映隔夜信息(那么现货开盘会追上去,做收敛就是逆势接刀),或者只是流动性稀薄把价格推歪了(那么做收敛才对)。

在盘口上,这两种情况长得一模一样。

分不清就下注,交易的不是收敛,而是在替市场承接一次新的价格发现。再加上换汇、通道、深度、资金费率这些摩擦,3% 的名义价差能落袋的部分很有限。

但这批合约凑齐了另一样东西

放弃跨市场之后,我注意到一件事:这批新合约让一个组合变得可以在同一个账户里执行了。

EWY 是跟踪韩国股市的 ETF。按 iShares 2026 年 7 月 17 日的持仓披露:

  • SK 海力士 ≈ 23.17%
  • 三星电子 ≈ 21.72%
  • 两家合计接近 45%

img

图 1:EWY 像一个装着多家韩国公司的篮子,三星和海力士是里面最重的两块砝码。

打个比方:EWY 是一个不透明的篮子,你不知道里面装了什么,但你知道有两块最重的砝码可以单独称重。如果篮子的总重量突然和"两块砝码推算出来的重量"对不上了,多出来的那部分是什么?

那就是要交易的东西:

text
ln(EWY) = c + β₁·ln(三星电子) + β₂·ln(SK海力士) + ε ↑ 这个才是交易对象

ε 有明确的经济含义——EWY 里三星和海力士解释不了的那部分:现代汽车、KB 金融等其余成分股,加上汇率、时段错位、资金费率基差、流动性噪声。

选这三个标的的理由不是"三条曲线长得像",而是偏离有归因。这一点后面会反复出现——最后正是这个"归因"失效了。

💡 为什么用对数价格?
β 变成弹性系数("三星涨 1%,EWY 涨 β₁%"),与绝对价位无关,可以直接换算成下单金额;残差变成百分比偏离,量纲跨时间可比,标准化出来的 z 才有意义。用原始价格,价格水平一变量纲就变了,z 值失去比较基础。

仓位怎么配

回归给出的 β 直接就是对冲比例。做空 1 单位 EWY,同时买入 β₁ 单位三星 + β₂ 单位海力士——两个共同因子被抵消,只剩下 ε 这块敞口。

javascript
var wr = -reg.betas[j] * wTarget; // 对冲腿的方向和规模,由 β 反向决定

这一步的意义在于:我不预测韩国股市涨跌。三星和海力士的共同涨跌被对冲掉了,赚的只是"篮子和砝码之间的脱节会收敛"。


二、v1.0 模拟盘:跑通不等于可行

第一版是纯模拟盘。400 USDT,3 倍杠杆,15 分钟 K 线,480 根回归窗口(约 5 个交易日)。

信号是个简单的三态机:|z| > 1.0 开仓,|z| < 0.5 平仓,48 小时强制离场。

img

图 2:残差进极端区建仓,回均值附近离场。箭头表达的是交易假设,不是必然。

💡 进出场阈值为什么不对称?
都设成 1.0 的话,z 在阈值附近抖动会反复开平,手续费先把账户吃光。0.5 的缓冲带保证每笔交易至少走完半个标准差才落袋。

跑了两天,正收益。

img

但这个正收益什么也证明不了。

模拟引擎按最新价即时全额成交,不计手续费、不计滑点、不模拟排队和部分成交,三条腿是"同时"成交的。真实执行里根本不存在这回事。

它唯一证明的是:数据链通了,z 和 β 没算飞。

第一版最有价值的部分,是那几道"不交易"的开关

新上线的合约历史短,休市时价格可能冻结,两条对冲腿又高度相关。一个异常数据就足以把 β 和 z 推到荒谬的水平,然后让系统以为发现了百年一遇的机会。

四道门槛,不负责赚钱,只负责在数据不可信时拒绝交易

门槛阈值防什么
价格冻结检测窗口内对数极差 < 1e-6休市/标记价不动,残差标准差被压到接近 0
矩阵奇异检测主元 < 1e-12两条腿完全共线,回归无解
残差标准差下限< 5e-4回归不可靠,z 的分母把噪声放大成信号
z 值合理性上限|z| > 15坏数据或病态回归产生的天文数字

请记住最后那一行的 15,后面还会遇到它。


三、上实盘的第一课:单腿风险才是套利的命门

从模拟盘转实盘,遇到的杂七杂八的 bug 不少,但那些都是工程问题,改了就好。

真正让我重做整个执行层的,只有一件事:单腿风险。

这一节想讲透,因为它不是这个策略特有的问题——任何多腿套利,成败都在这里。

3.1 问题的本质:套利组合在成交完成之前,不是套利组合

第一版的实盘下单是串行的:

text
下第一条腿 → 等 HTTP 往返 → Sleep(300ms) → 下第二条腿 → 等 → Sleep(500ms) → 下第三条腿

三次网络往返加两次休眠,累计接近 1 秒。

这 1 秒里发生了什么?第一条腿成交后,我持有的是一个裸的方向性头寸。

不是套利,是单边押注韩国半导体。所谓的 beta 中性、所谓的"只对残差敏感",在最后一条腿成交之前统统不存在。

3.2 为什么这对套利是致命的:算笔账

方向性策略也有滑点,为什么套利策略对它格外敏感?

因为目标收益的量级不一样

趋势策略吃的是几个百分点的方向性行情,0.1% 的滑点是零头。而统计套利吃的是残差的收敛,一次网格循环大约是 1 个残差标准差——这个量级本身就很小。

拿代码里的成本参数做锚:

javascript
var TAKER_FEE_RATE = 0.0005; // 币安 U 本位合约 VIP0 市价单 taker 费率 0.05% var PROFIT_FEE_MULTIPLE = 3; // 目标:一次开平至少赚回 3 倍手续费

三条腿、开平双边,光手续费就是 0.05% × 2 × 3 = 0.3% 的名义成本。按 3 倍目标算,一次完整交易的毛利目标大约在 0.9% 名义这个量级。

现在把滑点放进来。三条腿裸露接近 1 秒,如果每条腿滑 0.1%,三条腿合计 0.3%——直接吃掉三分之一的目标毛利。

行情快一点,或者赶上韩股开盘的跳空,这个数字会更难看。

⚠️ 这就是套利策略的死穴:执行误差和目标收益是同一个量级。
方向性策略输在方向判断上,套利策略输在执行上。

3.3 还有一层不对称:单腿暴露是负凸性的

假设做空价差,第一条腿(空 EWY)成交了,还差两条多头腿。

  • 价格朝不利方向走 → 补腿成本上升,这笔交易的预期收益缩水;
  • 价格朝有利方向走 → 这一秒里确实浮盈了,但仍然必须补腿,因为策略假设的是收敛,不是方向。

也就是说,有利的方向你拿不满,不利的方向你全额承担。期望是负的。

暴露时间越长,这个负期望积累得越多。所以问题不是"能不能承受一次滑点",而是每一次开平仓都在交这笔税

3.4 第一层解法:串行改并发

把三条腿的下单从串行改成异步并发:

javascript
// 分两阶段: // 阶段1(并发):需要方向切换的腿,先把反向仓位并发平掉 // 阶段2(并发):所有腿的开/加/减仓,并发下单 // 阶段之间有先后(同一条腿"先平反向再开新方向"不能颠倒),阶段内部完全并发 var tasks = {}; for (...) { tasks[sym] = exchange.Go('CreateOrder', sym, cmd, -1, qty); // 异步发起,不阻塞 } for (var sym in tasks) { var orderId = tasks[sym].wait(); // 统一收割 }

暴露窗口从秒级压到了几十毫秒量级。

这是整个项目里性价比最高的一次改动。

但要说清楚:并发不等于原子。 几十毫秒仍然不是零,而且这个方案有个前提——三个订单都能立刻全额成交。深度不够的时候,市价单也会部分成交,此时组合比例失衡,持有的根本不是 beta 中性的东西。

真正的原子性只有交易所提供组合单/价差单才能做到,币安的 TradFi 永续没有这个功能。所以并发只是把问题缩小了,没有消除。

3.5 第二层解法:把执行做成幂等的循环

既然做不到原子,就退而求其次:保证最终一致

下单失败、部分成交、策略重启——不管中间出什么事,最终三条腿必须精确落在目标上。

javascript
function strictOpenUntilFilled(targets, regCoef) { while (true) { cancelAndConfirmNoPendingOrders(); // 先确认没有在途订单 var pq = queryLivePositionsStrict(true); // 再查交易所的真实持仓 if (!pq.ok) { Sleep(ORDER_SETTLE_MS); continue; } if (positionsExactlyMatchTargets(pq.positions, targets)) { // 二次复核通过后才认为开仓完成 _G('bh_execState', 'HOLDING'); return true; } submitOpeningDifferenceBatch(pq.positions, targets); // 只补差额,不重下全量 } }

三个要点:

① 撤单在查仓之前。 有在途订单时查到的持仓是不完整的,据此补差额会重复下单。

② 只补差额。 每轮循环重新计算"目标 − 实际",只提交缺口。这样即使某次提交部分成交,下一轮自然会补上剩余部分。

③ 达标后二次复核。 第一次查询满足条件不代表真的完成了,可能还有刚成交未结算的部分。撤单 → 再查 → 再确认,两次都满足才置为 HOLDING

执行状态机是 RECOVERING → FLAT_CLEAN → OPENING → HOLDING → CLOSING,状态持久化。策略重启后先进 RECOVERING:撤掉所有挂单、核对交易所的真实仓位,再决定继续持有还是清理。

🔧 多腿实盘的最低标准
任何时候策略被杀掉、机器重启、网络断开,重新拉起来之后必须能从交易所的真实状态恢复,而不是相信自己内存里记的东西。

单腿策略这样做是加分项,多腿策略这样做是及格线——因为单腿策略最差是"仓位不对",多腿策略最差是"以为自己对冲了,其实没有"。

3.6 第三层:离场时的单腿风险,比入场时更致命

这一点很多人会忽略。

入场时如果一条腿没成交好,最坏的结果是这笔交易的预期收益变差。

离场时如果一条腿平不掉,你会从"套利组合"直接变成"裸敞口"。 而且这种情况偏偏最容易发生在行情剧烈、流动性变差的时候——正是最不想要裸敞口的时候。

所以平仓函数用了和开仓完全对称的死循环写法:撤单 → 查真实仓位 → 确认全部为 0 → 二次复核 → 才允许置为 FLAT_CLEAN

javascript
function strictCloseUntilFlat(reason) { while (true) { cancelAndConfirmNoPendingOrders(); var pq = queryLivePositionsStrict(true); if (!pq.ok) { Sleep(ORDER_SETTLE_MS); continue; } if (positionsAreCompletelyFlat(pq.positions)) { // 二次复核 → 清除所有状态,包括冻结的回归模型 ... return true; } submitCloseBatch(pq.positions); } }

这个死循环不是代码洁癖,是刚需。 一个"差不多平掉了"的多腿组合,是账户里最危险的东西。

3.7 还没解决的部分

说清楚:单腿风险我只处理到"缩小 + 最终一致"这一层。真正完整的方案还差三件事:

  • 入场前的深度检查:三条腿盘口深度不足时应当直接放弃这次信号,而不是硬下;
  • 限价单 + 超时回滚:市价单保成交但不保价格,限价单保价格但可能腿不齐,需要一套超时后撤单并回滚已成交腿的机制;
  • 时段过滤:只在三条腿流动性都好的窗口开仓。

这三条都写进了待办,一直没做完。没做完的待办最后都会来找你。


四、v2.0:从"开关"改成"网格"

执行层稳定之后,信号层的问题浮上来了:单点进出太粗糙。

|z| > 1.0 一次性开满仓,|z| < 0.5 一次性平光。于是经常出现这种情况:z 刚过 1.0 就满仓了,然后它继续走到 2.0、2.5——既不能加仓摊低成本,也没有止损,只能干看着。等它回到 0.5 平仓,中间最好的位置完全没吃到。

v2.0 改成四级收敛网格

text
z +2.5 ├──── 满仓 ────┐ │ │ 这段可以 +1.5 ├──── 半仓 ────┘ 反复循环 │ +0.5 ├──── 清仓 ────────┼──────────── 0 -0.5 ├──── 清仓 │ -1.5 ├──── 半仓 ────┐ │ │ 镜像 -2.5 ├──── 满仓 ────┘

正向 z 穿过 +1.5 开半仓、穿过 +2.5 加满仓;回落到 +1.5 减回半仓、回到 +0.5 全平。负向完全镜像。

±1.5 与 ±2.5 之间可以反复循环——这是收益的主要来源,一次错位能吃好几个来回。

三个设计细节,事后看每一个都很关键:

① 判断"穿越",而不是"当前位置"

javascript
if (prevZ < GRID_HALF_Z && z >= GRID_HALF_Z) { ... } // 必须是真实穿越

如果只看当前位置,策略重启一次就会立刻按当时的 z 建仓——而那个位置根本不是想要的入场点。用穿越判断,脚本在区间中途启动时不会凭空开仓。

② ±2.5 之外禁止继续加仓

硬规则,没有例外。极端偏离里无限摊平是网格策略最经典的死法。

这条规则后来救了我一命。

③ 满仓数量在开仓时冻结

每轮先算出"满仓状态下三条腿各是多少数量",存下来,之后的档位切换都是对这个固定基数做缩放。

不这么做的话,每次切换都按当时的权益和 β 重算,三条腿的比例会漂——来回几次之后,对冲结构就散了。 这本质上仍然是执行层那个问题:任何让三条腿比例失衡的机制,都在制造隐性的单腿暴露。

顺带把行情层换成了 WebSocket

网格和单点进出有一个本质区别:它必须时刻盯着 z。

单点进出只关心"z 有没有越过 1.0",晚几秒问题不大。网格有四条边界,还要判断穿越方向,prevZz 之间隔得越久,判断越不准——极端情况下 z 在两次采样之间穿过两条线又折返,你完全看不见。

REST 轮询 10 秒一次,对这个需求太慢了。

所以换成币安 U 本位合约的 WebSocket 推送,REST 只在启动时装一次历史 K 线,运行中全靠推送更新最新价和当前 K 线。轮询间隔从 10 秒压到 200 毫秒。

配套的守护逻辑必须跟上:行情超过 5 秒没更新视为失效、断线自动重连、重连失败回落到 REST、私有接口异常退避。单独看都很琐碎,但少任何一个,实盘跑几天都会出事。


五、一次做了又删掉的功能:发散方向的收益

这一节讲一个主动放弃的决定。它比前面任何一次改进都更值得写。

动机

均值回归策略在趋势里挨打,这是常识。

网格跑了一段时间后,我盯着日志里那些"z 走到 2.5 之后又继续走到 3.5、4.0"的记录,很自然会想:既然它经常继续走,为什么不顺着做一把?

于是加了手动发散开仓通道——正向开仓 / 负向开仓 两个按钮,仓位用独立的 positionOrigin = 'MANUAL' 标记,与自动网格仓位完全隔离,避免自动逻辑接管人工判断的仓位。

javascript
function weightsForDirection(betas, dir) { var wTarget = dir * 1.0; var wRefs = [], gross = Math.abs(wTarget); for (var j = 0; j < betas.length; j++) { var wr = -betas[j] * wTarget; wRefs.push(wr); gross += Math.abs(wr); } var scale = gross > 0 ? (GROSS_EXPOSURE / gross) : 0; // ... 归一化后返回 }

代码写完了,也跑通了。然后我把它删了。

为什么删

第一,发散没有离场依据。

收敛交易有一个客观的目标:z → 0。你知道自己在等什么,也知道等到什么程度该走。

发散呢?z 会走到 3 还是 8?没有任何东西告诉你。一个没有目标价的交易,本质上是在裸赌方向,只不过换了个统计学的外衣。

第二,开仓后很难达到预期。

实盘跑下来的体感很清楚:真正的发散行情是跳跃式的。等你判断出"这是发散不是噪声",最好的位置已经过去了。剩下的部分要么幅度不够覆盖成本,要么正好赶上反转。

第三,也是最根本的一条——收敛和发散用的是同一个信号。

z = 2.5 到底意味着什么?

  • 可能是"偏离到了极端,即将回归"→ 应该做收敛;
  • 也可能是"这个关系正在断裂"→ 应该做发散。

这两种情况在 z 值上完全不可分辨。 同一个数字,两个相反的解读,而且在下单那一刻没有任何办法区分。

那么在同一个策略里同时放收敛和发散两套逻辑,等于承认自己不知道该信哪一个——最后的结果一定是:该收敛的时候在做发散,该发散的时候在做收敛。

💭 取舍的结论
一个策略只应该有一个假设。

想做发散,就应该另起一套独立的信号体系去识别"关系断裂",而不是复用收敛策略的 z。

删掉这个功能,是这个项目里唯一一次做减法。

当时我把"z 无法区分极端偏离与关系断裂"当成了一个功能取舍的理由,没有再往下想一步。这个判断后来被证明是这个策略最根本的风险,而不只是一个功能能不能做的问题。


六、v2.1:冻结坐标系

网格上线一段时间后,对账时发现一个说不通的现象:

  • 有几次 z 走完了完整的收敛路径,从 ±2.5 一路回到 ±0.5 全部平掉,结算下来却基本没赚
  • 反过来,有几次账面浮盈已经相当可观,z 却还在 1.8、1.9 徘徊,迟迟不到离场线,只能眼看着行情回吐。

信号和盈亏对不上。

排查结果:尺子本身在变

根子在滚动回归。

β、残差均值、残差标准差每根 K 线都在更新。也就是说,建仓时的 z = 2.0 和一小时后的 z = 2.0,根本不是同一个东西——同样一组三腿价格,因为尺子换了,算出来的 z 就不一样。

于是出现了两种脱钩:

现象真实情况
z 回到离场线,但没赚钱残差其实没怎么收敛,是 β 和均值的漂移把 z 拉回去的。我平掉的是一个"看起来收敛了"的仓位
赚了钱,z 却不到位残差确实收敛了,但窗口内波动率变小、均值移动,把 z 撑在了高位。该落袋的时候,网格不让走

盈亏由实际价差变动决定,离场由 z 决定。坐标系一漂移,这两件事就脱钩了。

再往极端想:价格根本没动,仅仅因为 β 更新,z 就可能跨过 0.5 触发全部平仓——一笔完全由计算引起的交易,还要付三条腿的手续费。

解法:开仓那一刻,把整把尺子冻结

javascript
function captureAndFreezeBetas(betas, c, mean, sd) { _G('bh_frozenRegCoef', JSON.stringify({ c: c, betas: betas, mean: mean, sd: sd })); Log('❄️ 已冻结回归模型(HOLDING期间坐标系锁定)'); }

冻结的是四个量:截距 c、系数 betas、残差均值 mean、残差标准差 sd。少冻结任何一个,坐标系都不完整——特别是 sd,它是 z 的分母,直接决定所有边界在真实价格空间里的宽度。

持仓期间所有决策用的 z 都基于冻结模型重算,直到完全平仓才解冻。改完之后,信号和盈亏终于对上了:z 从 2.5 走到 0.5,对应的就是实实在在的价差收敛。

同时保留实时回归做监控

这是关键的一步:实时回归继续算,但只用来对比冻结模型,不参与决策。

javascript
// β 漂移监控 var drift = Math.abs(reg.betas[b] - frozenRegCoef.betas[b]) / Math.abs(frozenRegCoef.betas[b]); if (drift > DRIFT_THRESHOLD) { Log('⚠️ β' + (b+1) + '漂移' + _N(drift*100,1) + '% (冻结:' + _N(frozenRegCoef.betas[b],4) + ' → 新:' + _N(reg.betas[b],4) + ') | 需人工检查是否重新武装'); } // 波动率漂移监控 var sdDrift = Math.abs(sd - frozenRegCoef.sd) / frozenRegCoef.sd; if (sdDrift > DRIFT_THRESHOLD) { Log('⚠️ 波动率(sd)漂移' + _N(sdDrift*100,1) + '% | 市场波动性可能改变,需人工检查'); }

设计意图是对的:冻结坐标系保证决策稳定,实时回归负责告诉我"这把尺子还准不准"。

但两个监控我都设成了 只告警、不操作,理由是"不能让自动逻辑推翻人工判断"。

❄️ 冻结坐标系,是这个策略赚到钱的直接原因。
它让 z 重新代表真实的价差偏离,网格的四条边界终于变成了真正固定的边界。

而"只告警、不操作",是它最后死掉时亏损被放大的直接原因。


七、辉煌期:亚盘一轮,美盘一轮

冻结版定型之后,策略进入了一段很舒服的日子。

规律性非常强:亚盘一轮,美盘一轮。

原因不难理解——KRX 开盘和美股开盘是两个信息注入点,EWY 和两只韩股永续之间的错位主要在这两个窗口形成,然后在流动性恢复后收敛。网格在 ±1.5 和 ±2.5 之间来回吃,一天能走好几个来回。

累计下来大约 50%

这个数字必须配三个注释,否则是误导:

  1. 杠杆是 10 倍。 这不是低风险产品。
  2. 样本只有 40 多天,而且集中在同一种市场状态里。
  3. 它证明的是"这个因子在这段时间存在",不是"这个因子稳定存在"。

赚钱的时候,所有护栏看起来都是多余的。这就是最危险的时候。


八、死亡:z = 6,两天不回归

某天开始,日志里出现了一个没见过的状态。

z 值冲到 6 附近,然后就不动了。整整两天。

但没有触发任何一道护栏。回头看前面那四道门槛:

门槛当时的实际情况结果
价格冻结对冲腿在剧烈波动✅ 通过
矩阵奇异求解正常返回✅ 通过
残差标准差不小,反而在变大✅ 通过
z ≤ 15z = 6通过

Z_SANITY_CAP = 15 是设计来拦截计算故障的——除零、坏数据、病态回归产生的天文数字。它拦得住 z = 400,拦不住 z = 6。

而 z = 6 恰恰是最危险的区间:数值上完全合理,经济上完全荒谬。

按网格规则,z 穿过 2.5 时已经满仓,之后被"±2.5 之外禁止加仓"的硬规则挡住——这条规则救了我,否则会一路摊平到爆仓。 但仓位就那么挂着,等一个不会来的收敛。48 小时超时后强制平仓,在最深的位置。

尸检:β₂ 变成了负数

停机之后去查 β:

text
β₁ (三星电子) = 0.93 β₂ (SK海力士) = -0.24 ← 这里

海力士占 EWY 约 23% 的权重,回归系数却是负的——这在经济上完全说不通。

而且后果是直接的。对冲腿方向由 决定,β₂ 变负意味着海力士腿翻到了和 EWY 同一侧:做空 EWY 的时候,同时在做空海力士。

那不是对冲,是在韩国半导体上加了一笔方向性空头。

顺着这条线查下去,原因是三层叠加的。

第一层:共线性压垮了回归

三星和海力士这段时间近乎同步:8 月 4 日两者分别跌 8.76% 和 8.79%(Gate 新闻);7 月 31 日更夸张,两只股票单日同涨近 30%,创下各自历史单日最大涨幅(KED Global)。

两个解释变量高度同步时,回归无法把方差归因给谁。系数方差爆炸,符号随机翻转。

注意 β₁ + β₂ = 0.69,这个合计弹性完全合理——只是拆分毫无意义。

原来那道共线护栏是无效的:它只在"完全共线"时才拒绝。高度共线但没到完全共线,会顺利算出一组有限的、垃圾的系数,然后一路通过后面所有检查。

第二层:板块轮动打断了经济归因

EWY 有约 55% 是非半导体成分。而这段时间韩国市场恰好在剧烈轮动:KOSPI 权重股拖累指数,但上涨家数远多于下跌,KOSDAQ 连续三个交易日触发买入 sidecar(KED Global)。

结果是 EWY 跌幅远小于海力士——过去一个月海力士 −35%、三星 −23%(高盛,via 新浪财经)。

这是最本质的一层。

这个因子的全部前提是"篮子和砝码的短期脱节会收敛"。而板块轮动恰恰是让脱节持续下去的力量——残差不再是噪声,它变成了一个持续的结构性偏离。

第三层:杠杆型 ETF 放大了海力士的特异波动

韩国监管正在调查外资高频交易是否放大了市场波动,起因是挂钩三星和海力士的杠杆 ETF 上线后程序化交易量近乎翻倍;当局已将单只个股杠杆 ETF 的个人投资上限设为总资产的 20%(Reuters via KED)。

解释变量的特异方差被人为抬高,回归系数会被向零衰减,在共线配对下就容易压过零点变负。

冻结坐标系在这里起了反作用

冻结的本意是"持仓期间尺子不能变",这在正常市况下完全正确。

但当关系发生结构性断裂时,冻结意味着我一直在用一把"崩盘前的尺子",度量"崩盘后的世界"。

而且 z = 6 这个数字,是分子分母两头同时推出来的:

text
残差(分子)── 结构断裂,真实偏离急剧变大 z = ───────────── sd(分母)── 冻结的是平静期的小值,市场已进入高波动状态

分子在涨,分母被锁死在一个过期的小数上。z 被系统性放大了。

更要命的是,旧坐标系下的残差永远不会回到旧的均值——因为那个均值所描述的关系已经不存在了。

z = 6 卡住两天,不是市场没回归,是我在用一个已经作废的参照系测量。

告警其实都打过

DRIFT_THRESHOLD = 0.20。β₂ 从正值翻到 −0.24,漂移率远超 20%;崩盘期间的实际波动率相对冻结的 sd,漂移同样远超 20%。

这两条告警一定都打过,而且大概率打了很多遍。

但它们被设计成了"只告警、不操作"。


九、真正的教训:漂移监控才是因子的生命体征

复盘下来,最核心的一条不是"某个阈值设错了",而是:

我把 β 漂移和波动率漂移当成了运维告警,而它们其实是这个因子的生命体征。

心电图不是用来提醒医生看一眼的,是用来触发抢救的。

9.1 β 漂移:分级处置,而不是一刀切

DRIFT_THRESHOLD = 0.20 一个阈值管所有情况,这是设计缺陷。漂移 25% 和符号翻转,是完全不同性质的两件事。

正确的做法是分级:

级别判据含义处置
一级漂移 > 20%关系在变化,但结构还在告警 + 禁止加仓(允许持有和减仓)
二级漂移 > 50%,或两条腿 β 相对比例大幅改变对冲比例已经不可信强制平仓退出,重新回归
三级β 符号翻转对冲腿变成了同向腿,组合不再是套利立即平仓 + 停止开新仓,人工介入

三级是硬熔断。因为符号翻转之后,你持有的东西在结构上已经不是你以为的那个组合了——继续持有等于在裸奔,而且自己不知道。

这里必须说清楚一点:这和"最大开仓值"这类仓位限制不是一回事。

仓位上限管的是"最多亏多少",漂移熔断管的是"我现在持有的还是不是我以为的东西"。前者限制损失幅度,后者判断策略假设是否还成立。仓位上限拦不住结构失效——它只会让你在一个已经变质的组合上,亏得慢一点。

9.2 波动率漂移:它决定了网格边界值多少钱

sd 漂移这条我当初写进去了,但完全没想明白它的分量。

z 的分母就是 sd。冻结 sd,等于把网格边界在真实价格空间里的宽度也冻结了。

  • 市场进入高波动状态(真实 sd 变大)→ 冻结的小 sd 使 z 被放大 → 明明是正常波动,却显示为极端偏离;
  • 市场进入低波动状态(真实 sd 变小)→ 冻结的大 sd 使 z 被压缩 → 真正的机会被压到阈值以下,看不见。

这次死亡就是第一种情况的极端版本。

所以 sd 漂移的处置逻辑应该和 β 分开:

sd 漂移超过阈值 → 当前冻结模型的所有 z 阈值都已失真 → 平仓解冻,用新的波动率水平重新武装。

而不是继续拿着一把过期的尺子等收敛。

9.3 最该补而一直没补的:残差半衰期

上面两条都是事后指标——它们告诉你"关系已经变了"。

有没有事前指标?有,就是残差半衰期:对残差拟合均值回归过程,估计它回到均值需要多久。

半衰期的价值在于它直接对应策略参数:

  • 半衰期 ≪ 最大持仓时限 → 策略健康;
  • 半衰期开始拉长 → 收敛变慢,这是因子衰减的早期信号
  • 半衰期 > 最大持仓时限 → 策略在系统性地"未收敛先强平",此时应该自动降杠杆或停机

这一条在第一篇文章的"未实现清单"里就写了,43 天里一直没补。

清单上写着的东西,市场迟早会来收账。

9.4 一条没被看见的线

回过头看,放弃发散功能时遇到的问题,和最后杀死策略的问题,其实是同一个。

当时因为"z 无法区分极端偏离与关系断裂",我否掉了发散功能。判断是对的。

但我只把它当成一个功能取舍,没有意识到:这个无法区分性,同时也是收敛策略本身的根本风险

当 z = 6 出现时,它恰恰就是"关系断裂"而不是"极端偏离"。而这个策略,只会按后者去理解它。


十、还有优化方向吗?有

这个因子暂时死了,但思路没死。留几个方向给大家继续挖:

① 把两条腿合成一条,从根上消灭共线性。

按 23.17 : 21.72 构造一个固定权重的合成腿,只回归一个 β。共线问题彻底消失,而且更贴合"用成分股复制 ETF"的经济叙事。代价是丢掉两者之间的相对信息。

② 或者干脆换个配对,绕开 ETF。

直接做三星 vs 海力士两腿配对。这一步同时解决三个问题:只有一个解释变量,谈不上共线;两只都是半导体,不受板块轮动干扰(EWY 那 55% 的非半导体成分正是这次的杀手);而且从三条腿减到两条腿,单腿风险的暴露面直接少了三分之一。

这可能是最实际的一条路。

③ 带约束的回归。

坚持保留两条腿的话,用非负最小二乘或岭回归来稳定拆分,而不是裸 OLS。β 被约束在合理区间,符号翻转不会发生——这比事后检测符号更根本。

④ 把平稳性检验做成"因子存活开关"。

滚动检验残差是否平稳,不平稳就不开新仓。这比任何 z 阈值都更根本——z 有意义的前提是残差平稳,而这个前提本身需要被持续验证。

⑤ 独立的"关系断裂"识别体系。

这是发散功能留下的作业。如果能用一套独立于 z 的信号(比如成分股之间的相关结构变化、行业内部轮动强度)判断"关系正在断裂",那么它既可以作为收敛策略的熔断开关,也可以作为发散策略的入场信号。

这一件事做成了,前面所有问题都能一并解决。


写在最后

从发现到消亡,43 天。四个版本,赚了大概 50%,最后在一个没预料到的地方还回去一部分。

这不是一个失败的故事,也不是一个成功的故事。

这就是因子的正常生命周期——发现、优化、盈利、环境改变、失效。区别只在于,有的人在失效时知道发生了什么,有的人不知道。

如果这篇东西有什么用,大概是这四句:

模拟盘只能验证代码没写错,验证不了策略能不能赚钱。

多腿套利的成败在执行层,不在信号层——执行误差和目标收益是同一个量级。

一个策略只应该有一个假设。

β 漂移和波动率漂移不是运维告警,是因子的生命体征——它们该触发的是抢救,不是提醒。

四个版本的源码都在,欢迎继续挖。有做出改进的,回来交流。


⚠️ 风险提示
本文为个人实盘记录与复盘,不构成任何投资建议。文中策略使用 10 倍杠杆,属高风险交易,历史收益不代表未来表现。统计套利不等于无风险套利——本文记录的正是一次协整关系失效的完整过程。任何人在实盘运行前,请务必完成文中提到的验证项。

相关推荐
评论
全部评论 (0)
暂无数据
暂无数据
  • 1
社区
回测系统
APP 下载
iPhone 下载
© 2015 - ∞ INVENTOR PTE LTD (SG)