海力士最近把"套利"两个字又炒热了。
随着股票永续合约上线,SK 海力士、三星电子这些原本只在 KRX 撮合的标的,多出了一条 USDT 本位、24 小时连续报价的敞口通道。公开资料显示,币安在 2026 年 6 月 2 日上线了 SKHYNIXUSDT、SAMSUNGUSDT 和 HYUNDAIUSDT;一份 7 月发布的行业报告提到,SK 海力士韩国现货收盘价与币安合约在部分时段的基差曾达到 3%—5%,不同加密平台之间的合约价差也一度超过 2%。价差摆在屏幕上,量化团队自然会闻风而来。但我把这条路走了一遍之后,最终交易的并不是它——而是一个更冷清、但三条腿都能在同一个账户里执行的组合:EWY、三星电子、SK 海力士。
这篇文章讲清楚这套策略的原理:为什么放弃跨市场基差、这三个标的之间的关系凭什么可交易、回归式在做什么、残差为什么才是交易对象、仓位比例由什么决定、哪些数据必须拒绝交易,以及当前版本还差哪些东西才谈得上实盘。策略已在发明者量化平台以模拟盘运行,源码在文末。
首先要澄清一个常见的表述错误:股票永续合约的上线,并没有延长 SK 海力士现货的交易时间。
KRX 收盘后,底层股票停在最后一笔成交上,而以其为参考的永续合约仍在连续报价,继续吸收隔夜的宏观数据、费城半导体指数波动和汇率变化。参考资产冻结、衍生品继续定价——这种时段错配,才是近期基差的主要来源。
也正因为如此,闭市之后所谓的"公允价格"是不可观测的。永续合约的偏离可能来自两种完全相反的成因:
在盘口上,这两种情况长得一模一样。 分不清就**,交易的就不是收敛,而是在替市场承接一次新的价格发现。
即便撇开定价模糊的问题,把"韩股现货 ↔ 加密永续"这条腿真正搭起来,也要逐项穿过下面这些摩擦:

3% 的名义价差,未必有 3% 能落进口袋。而且最显眼的裂缝往往也是最拥挤的裂缝。
所以我换了个思路:保留"相对价值"的方法论,但把所有腿都放进同一个执行环境里。 这样做不能消除策略风险,但至少可以先消除掉账户、通道、时区和结算货币这几层工程摩擦,让研究聚焦在统计关系本身。

图 1:EWY 像一个装着多家韩国公司的资产篮子,三星和海力士是其中最重的两块砝码。回归试图用这两块砝码解释篮子的主要变化,橙色波动则代表暂时无法被解释的残差。
EWY 是跟踪韩国股票市场的 ETF。根据 iShares 2026 年 7 月 17 日披露的持仓:

两家合计接近 45%——足以解释 EWY 的大部分方向性波动,但远不足以完全决定它。剩下的部分,加上汇率、时段错位、资金流和各合约自身的基差,构成了可供交易的偏离。
选这个组合的理由不是"三条曲线长得像",而是偏离有经济学上的归因:两只股票本来就是这只 ETF 的核心持仓,统计关系背后有持仓结构支撑,讨论"收敛"才有意义。如果只是从几千个合约里暴力筛相关系数,很容易找到一堆漂亮但没有经济含义的伪相关,市场状态一变,关系就消失了。
研究问题因此变得具体:
当 EWY 偏离由三星和海力士给出的"隐含价格"时,这段偏离是否具有回归倾向?
策略在同一账户内交易三个 USDT 本位线性合约:
var SYMBOLS = "EWY_USDT.swap,SAMSUNG_USDT.swap,SKHYNIX_USDT.swap";
var LABELS = "EWY,三星电子,SK海力士"; // 第 1 个 = 目标腿,其余 = 对冲腿
必须说清楚:这不是无风险套利,而是押注统计关系延续的相对价值交易。
ln(EWY) = c + β₁·ln(三星电子) + β₂·ln(SK海力士) + ε
用对数价格而不是原始价格,有两个实际理由:
第一,β 变成了弹性系数。 β₁ = 0.5 的含义是"三星涨 1%,EWY 涨 0.5%",与三者各自的绝对价位无关。这样得到的对冲比例可以直接换算成名义金额,而不需要再乘以价格比。
第二,残差 ε 变成了相对偏离。 ε = 0.008 意味着"EWY 比模型隐含值贵了约 0.8%",是一个百分比。如果用原始价格回归,残差是一个绝对金额差,价格水平一变它的量纲就变了,跨时间根本没法比较,标准化出来的 z 值也失去意义。
策略的数学核心只有一个函数。它不仅返回 β₁、β₂,还把窗口内每一根 K 线的残差完整保留下来——因为交易信号来自残差序列,而不是来自系数本身:
function regressSpread(lnTarget, lnRefs) {
var k = lnRefs.length, n = lnTarget.length, dim = k + 1;
var XtX = [], Xty = new Array(dim).fill(0);
for (var i = 0; i < dim; i++) XtX.push(new Array(dim).fill(0));
function col(j, t) { return j === 0 ? 1 : lnRefs[j - 1][t]; }
// 累加正规方程 XᵀX 和 Xᵀy
for (var t = 0; t < n; t++) {
for (var i = 0; i < dim; i++) {
Xty[i] += col(i, t) * lnTarget[t];
for (var j = 0; j < dim; j++) XtX[i][j] += col(i, t) * col(j, t);
}
}
var beta = solveLinearSystem(XtX, Xty);
if (!beta) return null;
// 逐根还原残差:实际值 − 模型隐含值
var resid = [];
for (var t = 0; t < n; t++) {
var pred = beta[0];
for (var j = 0; j < k; j++) pred += beta[j + 1] * lnRefs[j][t];
resid.push(lnTarget[t] - pred);
}
return { c: beta[0], betas: beta.slice(1), resid: resid };
}
solveLinearSystem() 是带部分主元的高斯消元。当主元绝对值小于 1e-12(即两条对冲腿高度共线、正规方程近奇异)时直接返回 null,本轮信号作废——这一步不是数值洁癖:三星和海力士同属韩国半导体,共线性本来就高,一次病态求解足以把 β 推到荒谬的量级。
ε 是"EWY 的价格中,三星和海力士的同步变化解释不了的那部分"。它可能来自 ETF 内其余成分股(现代汽车、金融股),也可能来自汇率、时段错位、资金费率或流动性噪声。
这正是这套策略与方向性交易的分界线:我们不预测韩国股市涨跌,只押注"篮子与砝码之间的短期脱节会收敛"。三星和海力士的共同涨跌被对冲腿抵消掉了,剩下的敞口只有 ε。
策略取最近 480 根 15 分钟 K 线(约 5 个交易日)做滚动回归,把最新残差标准化:
z = (最新残差 − 窗口残差均值) / 窗口残差标准差
这里有一个必须承认的方法论缺陷:回归是在窗口内做全样本拟合的,最新残差本身也参与了这次拟合,因此它天然被"拉向"回归面,z 值被系统性压缩。同时窗口每根 K 线滚动一次,β 也随之变化,严格来说残差序列并不是同一模型下的可比序列。这不是致命问题(滚动 OLS 在配对交易中是常见做法),但它意味着 z 的绝对数值不能按标准正态去解读,阈值只能靠实证校准,不能拿"±2σ 对应 95%"这类课本结论直接套用。

图 2:残差进入上下极端区域时建立价差仓位,回到均值附近时退出。图中的回归箭头表达的是交易假设,不代表残差必然收敛。
信号是一个很小的状态机:空仓时只有越过进场阈值才开仓;持仓后不会因为 z 仍在同一侧而反复加仓,只等它回到离场带内,或者触发最大持仓时长。
var state = _G('bh_state') || 'FLAT';
var holdHours = state !== 'FLAT' ? (now - entryTime) / 3600000 : 0;
var forceExit = state !== 'FLAT' && MAX_HOLD_HOURS > 0 && holdHours > MAX_HOLD_HOURS;
var newState = state;
if (state === 'FLAT') {
if (zLatest > ENTRY_Z) newState = 'SHORT_SPREAD'; // EWY 偏贵 → 空 EWY、多两条对冲腿
else if (zLatest < -ENTRY_Z) newState = 'LONG_SPREAD'; // EWY 偏便宜 → 多 EWY、空两条对冲腿
} else {
if (forceExit) { newState = 'FLAT'; Log('⏰ 超过最大持仓时长,强制离场'); }
else if (Math.abs(zLatest) < EXIT_Z) { newState = 'FLAT'; Log(' z 回归至离场带内,平仓'); }
}

进场与离场阈值不对称(1.0 / 0.5)是有意的:如果两者相等,z 在阈值附近抖动就会反复开平,手续费吃掉全部利润。0.5 的缓冲带保证了每次交易至少要走完半个标准差的回归幅度才落袋。
这个状态机有两个需要明说的设计取舍:
SHORT_SPREAD 时 z 从 1.2 涨到 3),代码既不加仓也不止损,只等 |z| < 0.5 或 48 小时超时。这是均值回归策略的经典形态,也是它经典的死法——关系一旦真的破裂,亏损没有上界。SHORT_SPREAD 期间若 z 直接穿越到 −1.5,else 分支只检查 |z| < 0.5 和超时,两者都不满足,仓位会原地保持到 48 小时到期。这是明确的行为边界,不是 bug,但实盘前应重新评估。MAX_HOLD_HOURS = 48 的作用正是防止把一次短线错位熬成长期信仰——它是这个版本里唯一对"关系已经失效"这种情形的硬性认输机制。
三腿策略最容易写错的地方,是方向看对了,配比配错了。
假设信号判定 EWY 偏贵,我们做空 1 单位 EWY 的名义金额。此时组合暴露在什么风险下?根据回归式,EWY 的变化中有 β₁ 份来自三星、β₂ 份来自海力士。要把这两个共同因子抵消掉,就必须同时买入 β₁ 单位的三星和 β₂ 单位的海力士。
代码里就是这一行:
var wr = -reg.betas[j] * wTarget; // 对冲腿方向与规模,由 β 反向决定
三条腿的名义金额之比因此是 1 : β₁ : β₂,再统一归一化到总敞口预算:
var dir = state === 'SHORT_SPREAD' ? -1 : 1;
var wTarget = dir * 1.0;
var wRefs = [], gross = Math.abs(wTarget);
for (var j = 0; j < reg.betas.length; j++) {
var wr = -reg.betas[j] * wTarget;
wRefs.push(wr); gross += Math.abs(wr);
}
var scale = gross > 0 ? (GROSS_EXPOSURE / gross) : 0; // Σ|w| 归一化到 0.7
weights[0] = wTarget * scale;
for (var j = 0; j < wRefs.length; j++) weights[j + 1] = wRefs[j] * scale;
最后按 名义金额 = 权重 × LEVERAGE × 权益 换算成下单量,杠杆取 3 倍。
这一步的目的不是让组合永不亏损,而是让它只对 ε 敏感。 韩国股市整体涨跌、半导体板块共振、美元韩元波动——这些同时作用于三条腿的因子,理论上都被 β 配比抵消掉了。剩下的,才是我们想赚的那块相对价值。
每轮循环都会重算 β 和权重,但不是每轮都调仓。只有两种情况触发:
// ① 任一腿方向翻转
if ((sig.weights[i] >= 0) !== (lastWeights[i] >= 0) && (sig.weights[i] !== 0 || lastWeights[i] !== 0)) {
needRebal = true; break;
}
// ② 任一腿权重绝对值漂移超过 20%
var lw = Math.abs(lastWeights[i]);
if (lw > 1e-9 && Math.abs(Math.abs(sig.weights[i]) - lw) / lw > DRIFT_THRESHOLD) {
needRebal = true; break;
}
β 漂移正是通过 ② 被捕捉的:β 变了 → 权重变了 → 超过 20% 阈值 → 重新对冲。这层节流的意义是承认"β 的小幅抖动是估计噪声,不是信息"——如果每次微小变化都去调仓,手续费会先于残差收敛把账户耗光。
MIN_REBAL_USDT = 3 则承担了双重角色:既是"偏差太小就不动"的阈值,也是离场时的平仓开关——状态转 FLAT 后三腿权重归零,目标名义低于 3 USDT,调仓逻辑据此执行全平。
新上线的 TradFi 合约历史短、休市时段价格可能冻结,对冲腿之间又高度共线。一个异常价格就足以把 β 和 z 推到荒谬的水平,然后让系统以为发现了百年一遇的机会。
所以在信号成形之前,代码设了几道硬门槛。它们不负责创造收益,只负责在数据明显不可信时拒绝交易:
// ① 价格冻结检测:对冲腿在整个窗口内几乎无变化(休市 / 标记价格不动)
// 这会让回归极不稳定,把残差标准差压到接近 0,z 就可能被除出天文数字
if (refMax - refMin < MIN_REF_LOGRANGE) { // 1e-6
Log(' ' + LABELS[j + 1] + ' 窗口内价格几乎无变化(可能休市/冻结),本轮跳过');
return null;
}
// ② 共线检测:正规方程近奇异,高斯消元返回 null
var reg = regressSpread(lnTarget, lnRefs);
if (!reg) { Log(' 回归矩阵奇异,本轮跳过'); return null; }
// ③ 残差标准差下限:过小意味着回归不可靠,z 的分母会把噪声放大成信号
var sd = stdev(reg.resid);
if (!(sd > MIN_RESID_STD)) { return null; } // 5e-4
// ④ z 值合理性上限:极端值更可能来自坏数据而非真实机会
if (!isFinite(zLatest) || Math.abs(zLatest) > Z_SANITY_CAP) { // 15
Log(' z 值异常(' + zLatest + '),视为计算故障,本轮忽略信号');
return null;
}
第 ① 道和第 ③ 道是同一个风险的两端:韩股休市时,参考合约的价格可能长时间不动,窗口内残差标准差被压到接近零。此时 z 的分母趋近 0,任何微小的分子都会被放大成天文数字——一个看起来"千载难逢"的 z = 40,实际上只是除以了一个不该出现的小数。这类信号如果不拦,第一次触发就足以打光账户。
配套的还有整条数据链的对齐:alignData() 取三条 K 线时间戳的交集,只保留三个标的都有收盘价的时刻,并要求至少 490 根共同 K 线才允许计算。任一腿出现 0、负数或非数字价格,本轮同样作废。
所有中间量都带 [DEBUG] 前缀打日志——最新收盘价、各腿窗口内 ln 价格极差、回归系数、残差均值与标准差、原始 z 值。z 一旦异常,日志能直接定位是哪一环出的问题,不需要靠猜。
除了 z 离场和 48 小时超时,代码里唯一的主动风控是逐腿异常波动减仓:某条已持仓腿在两次检查之间出现超过 6% 的不利变动,就单独砍掉一半仓位。
var drop = pos.side === 'long' ? (last - cur) / last : (cur - last) / last;
if (drop >= EMERGENCY_DROP) { // 0.06
Log('紧急风险! [' + label + '][' + pos.side + '] 不利变动:', _N(drop * 100, 2) + '%');
// ... 减仓 50%
}
关于它的边界,有三点必须说清楚:
① 它按单腿判断,不看组合。 对冲组合里某条腿单独跌 6% 完全可能是正常的——另一条腿同时在涨,组合净敞口根本没变。这道熔断会在这种情况下误伤对冲结构,砍掉一半对冲腿之后,剩下的敞口反而不再是残差,而是裸的方向性风险。这是当前版本最需要改的一处。
② 参考价的更新是有条件的。 bh_lastCheckPrices 只在本轮没有任何腿触发时才整体更新。一旦触发,参考价保持不变,下一轮仍以旧价比较——单边行情里可能连续触发、连续减半。这是刻意的保守设计,但会让减仓比预期更激进。
③ 它是事后防线。 三条腿不可能在同一微秒、同一理想价格成交。第一条腿成交后,另外两条腿还暴露在市场里——这就是单腿风险。行情越快,原本想赚的那点残差越可能被执行误差吞掉。逐腿熔断是在损失已经发生后削减敞口,它无法替代一套真正的三腿成交协调机制。

源码中
POLL_MS的注释写的是"5 分钟",实际值是1 * 60 * 1000(1 分钟)。1 分钟的轮询配合 15 分钟 K 线,意味着同一根未收盘 K 线会被反复计算,DRIFT_THRESHOLD因此承担了比设计预期更重的节流职责。实盘前应当统一。
原型在发明者量化的模拟盘运行,初始资金 400 USDT。两天测试下来,账户取得了一定的正收益。

这是一个让人愿意继续研究的结果,但远不能作为盈利能力的证明:
正收益在这个阶段的正确读法是:流程跑通了,没有触发异常熔断,z 和 β 处于合理范围。 仅此而已。
把"公式能跑"和"资金可托付"分开的,就是下面这份清单。这些都不在当前代码里,实盘前需要逐项补齐:
统计有效性
MAX_HOLD_HOURS = 48 目前是拍脑袋定的,本应由半衰期反推。ENTRY_Z = 1.0 和 LOOKBACK = 480 都还没经过样本外校准。成本与执行
stepSize 与最小名义值取整。同样的框架并不局限于韩国市场:
模型也不必固定三条腿,标的池可以扩展成"一个目标 + 多个解释因子"。但组合越大不代表越高级:每增加一条腿,就多一份盘口、成交、资金费率和故障风险,而每条腿的执行误差都会直接侵蚀你想赚的那点残差。
一个真正可交易的组合应当同时满足三件事:
少一项,都只能算研究素材,不能算可交易机会。
海力士的基差提醒了市场:新的交易载体会制造新的价格裂缝。但套利机会从来不只存在于最热闹的那一道裂缝里。指数与成分股、ETF 与龙头篮子、同一主题里的 leader 与 follower,都可能短暂失去同步。
公式负责描述偏离,工具负责发现和执行,风控负责让我们有机会等到下一次偏离。至于眼前这点模拟盘收益,最合适的态度不是庆祝,而是把它当成下一步优化清单的起点。