第 6 章 · 02 NOK:从统计套利到动量信号 本节摘要:本节是整个石油货币大项目的最高光时刻。作者本想在挪威克朗(NOK)和布伦特原油(Brent)之间做一笔统计套利——用多元回归算出 NOK 的"公允价",价格偏离就反向开仓赌回归。结果多元回归 R² 漂亮,但模型在 2017/11/15 突然失效,价格一举突破 2σ 止损线。意外的是,正是模型失效的那个瞬间,成了趋势跟随(动量)策略的最佳入场点——价格朝突破方向又猛冲了一个月。作者用一句金句概括:"We started off seeking statistical arbitrage, but ended up discovering a momentum trading entry signal.
本节摘要:本节是整个石油货币大项目的最高光时刻。作者本想在挪威克朗(NOK)和布伦特原油(Brent)之间做一笔统计套利——用多元回归算出 NOK 的"公允价",价格偏离就反向开仓赌回归。结果多元回归 R² 漂亮,但模型在 2017/11/15 突然失效,价格一举突破 2σ 止损线。意外的是,正是模型失效的那个瞬间,成了趋势跟随(动量)策略的最佳入场点——价格朝突破方向又猛冲了一个月。作者用一句金句概括:"We started off seeking statistical arbitrage, but ended up discovering a momentum trading entry signal." 本节会逐行精读多元回归、Elastic Net、协整检验、以及那个把"模型失效"封装成"动量入场信号"的滚动 50 天 OLS 策略。
涉及源码:原项目
Oil Money NOK.py(约 547 行,本项目最长脚本)、Oil Money Trading backtest.py(约 293 行,共用回测引擎)。
⚠️ 注意:本节代码量大,但思路只有一条主线——从"反向赌回归"到"顺势赌突破"的思维翻转。读的时候请始终盯住这个翻转点:它发生在价格突破 ±2σ 的那一瞬间。
挪威是全球最大的产油国之一,且汇率自由浮动。沙特、伊朗、委内瑞拉虽然也是产油大国,但汇率钉住美元(pegged),套利空间被央行挤干。所以候选名单上只剩挪威、俄罗斯、加拿大。
按第 01 节的工具变量思想,NOK 的回归元全部用日元 JPY 计价:EURJPY、GBPJPY、USDJPY、Brent(in JPY),被解释变量是 NOKJPY。日本只占挪威出口不到 2%,是干净的外生基准。
作者一开始其实是怀疑 NOK 是不是真石油货币的——挪威 60% 的 GDP 来自服务业,主权基金规模全球第一,理论上对石油的依赖没那么强。结果回归跑出来狠狠打脸:Brent 对 NOK 的影响高度显著,R² 还很高。NOK 确实还是石油货币。
模型形式:
NOKJPY = β0 + β1·USDJPY + β2·GBPJPY + β3·EURJPY + β4·BrentJPY + ε
这是最朴素的普通最小二乘(OLS)。训练区间是 2013-04-25 到 2017-04-25,测试区间是 2017-04-25 到 2018-04-25。
样本内 R² 很高,T 检验、F 检验都显著。但 OLS 摘要里有一个红灯:条件数(condition number)飙升。条件数是设计矩阵 X 的最大奇异值与最小奇异值之比,它衡量的是回归元之间是否近似线性相关。条件数大,意味着存在严重的多重共线性(multicollinearity)。
为什么 NOK 模型会有多重共线性?原因很直观:
多重共线性的危害不在于 R² 本身(它会很高),而在于系数估计极不稳定——稍微动一下样本,β 的符号和大小都可能剧变。这样的模型做样本外预测会很脆。
作者的应对是 Elastic Net。它把 Lasso(L1)和 Ridge(L2)两种正则化拼在一起:
Loss = RSS + λ·[ α·Σ|β_j| + (1−α)/2·Σβ_j² ] └──────┘ └────────────────────────┘ 残差平方和 L1(变量选择) + L2(系数收缩)
本项目用的是 sklearn.linear_model.ElasticNetCV,在给定的 α(对应公式里的 λ)和 l1_ratio(对应公式里的 α)网格里做交叉验证自动选参:
m = en(alphas=[0.0001, 0.0005, 0.001, 0.01, 0.1, 1, 10], l1_ratio=[.01, .1, .5, .9, .99], max_iter=5000).fit(x0[x0.index<'2017-04-25'], y)
得到的弹性网系数估计为:[0.00388958, 0.01992038, 0.02823187, 0.00050092](分别对应 USD/GBP/EUR/Brent)。可以看到 EUR 的系数最大(0.028),印证了挪威处于 EEA、与欧盟经济深度绑定的直觉;Brent 的系数虽小(0.0005),但因为 Brent 价格本身量级大,实际影响力依然显著。
把 NOKJPY 看成一个随机过程,可以分解为两层:
| 频率 | 主导变量 | 解释 |
|---|---|---|
| 长期趋势 | EUR | 挪威在 EEA,经济与欧盟深度绑定 |
| 短期扰动 | Brent | 石油价格波动直接驱动短线 |
这个分解很重要——它解释了为什么模型会在某个特定日期"失效":当短期扰动(Brent)的冲击大到把长期趋势(EUR)的锚也拽偏时,模型给出的"公允价"就不再有意义,价格会朝突破方向持续运行。这正是动量信号的来源。
为了验证 NOK 与 EUR 的长期关系,作者尝试了协整检验。statsmodels 当时还没有 Johansen 检验(多维),只能用 Engle-Granger 两步法:
结果 ADF 统计量 -2.56,对应 p 值 0.10,连 90% 置信区间都没达到——协整关系没有被正式确认。但可视化上两者走势明显相关(回归 R² 高达 73%),所以作者用"rule of thumb"宣布 EUR 仍是 NOK 长期趋势的主导。这其实是量化研究里很诚实的一种处理:统计检验没过,但经济直觉和可视化都支持,那就如实记录,不要硬包装成显著。
这是 Oil Money NOK.py 里最初的版本,逻辑像布林带:用 Elastic Net 算出拟合值(fitted)和残差的标准差 σ,然后:
关键代码(Oil Money NOK.py:214-270)用 signals['cumsum'] 控制只触发首次信号,然后用 break 在触及 2σ 止损线时退出整个信号生成循环:
upper = np.std(df['sk_residual'][df.index<'2017-04-25']) # 1σ 阈值 # ... signals['stop profit'] = signals['fitted'] + 2*upper # 上轨 2σ signals['stop loss'] = signals['fitted'] + 2*lower # 下轨 2σ # ... for j in range(len(signals)): if signals['nok'].iloc[j] > signals['upper'].iloc[j]: signals.iloc[j, index] = -1 # 突破上轨做空 # ...触及 2σ 止损线则 break 退出整个循环
这个版本在样本内来回振荡时确实能赚一点小钱。但作者发现了一件怪事:从 2017/11/15 开始,NOKJPY 一路向上突破 2σ 止损线,如果不止损反而顺势加仓,后面整整一个月都在大涨。止损反而是把最赚的那一段给切掉了。

Oil Money NOK.py:288-298 在图上标了一个黄色五角星——"Potential Exit Point of Momentum Trading",并用一条黑色虚线 plt.axvline('2017/11/15') 标出模型失效日。
为什么是这一天?作者在 README 里给了几个候选解释(都没有定论,这正是 quantamental 的诚实):
关键不是"为什么",而是"发生了什么":模型破裂意味着基本面发生了投资者预期的根本变化,这种变化会持续相当长一段时间——而这正是 CTA(商品交易顾问)赚钱的方式。

oil_money_trading_backtest.py真正把"模型失效即信号"封装成可重复策略的,是这份共用脚本。它的核心是 signal_generation 函数(Oil Money Trading backtest.py:55-178),逻辑如下:
对每一个新数据点 i: ① 如果当前有持仓 holding != 0: - 持仓天数 counter > 10(默认)→ 清仓,模型重置 - |当前价 − 入场价| ≥ 0.5(默认 stop)→ 止盈/止损清仓 - 否则 counter += 1 ② 如果没有持仓 holding == 0: - 若模型未训练(trained=False):用过去 50 个点做 OLS 若 R² > 0.7,则 trained=True,算出 σ,铺设 ±1σ/±2σ 置信带 - 若模型已训练: 价格 > +2σ → 做多(breakout 上轨) 价格 < −2σ → 做空(breakout 下轨) 一旦入场:把所有置信带 reset 回 forecast 线(避免视觉混淆)
这里有四个关键参数,都做成函数参数:
def signal_generation(dataset, x, y, method, holding_threshold=10, # 持仓上限(天) stop=0.5, # 止盈止损点数 rsquared_threshold=0.7, # R² 有效性门槛 train_len=50): # 滚动训练窗口
🎯 反直觉发现(全项目最高光):请特别注意上面 ② 里的入场规则——价格突破 +2σ 是做多、突破 −2σ 是做空。这和第一版的统计套利完全相反!第一版里"突破上轨"是做空赌回归,这里"突破上轨"是做多赌延续。换句话说,作者把同一个 ±2σ 触发器,从反转信号翻成了顺势信号。
这就是 README 里那句金句的代码化身:
We started off seeking statistical arbitrage, but ended up discovering a momentum trading entry signal.
模型失效(价格突破 2σ)的瞬间,本来是统计套利的"出逃点",却被作者重新定义为趋势跟随的入场点。一次思维翻转,把一个失败策略变成了盈利策略。
为什么 reset 置信带(Oil Money Trading backtest.py:163-175)这一步不能省?因为入场后,模型其实已经"破裂"了,继续画 ±2σ 会让看图的人误以为模型仍然有效、误以为"价格还在置信带里为什么不开仓"。把置信带塌缩回 forecast 线,是在视觉上明确告诉读者:从这里开始,模型不再给预测,我们在跟趋势走。
作者对默认参数不满意——只赚 2%,还不如存银行吃挪威央行 0.75% 的无风险利率。于是做了一次暴力搜索(Oil Money NOK.py:478-489):
dic = {} for holdingt in range(5, 20): # 持仓 5~19 天 for stopp in np.arange(0.3, 1.1, 0.05): # 止损 0.30~1.05 点 signals = om.signal_generation(dataset, 'brent', 'nok', om.oil_money, holding_threshold=holdingt, stop=stopp) p = om.portfolio(signals, 'nok') dic[holdingt, stopp] = p['asset'].iloc[-1] / p['asset'].iloc[0] - 1
双循环跑完,把所有参数组合的收益率收集到 profile DataFrame,然后用两种图可视化:
oil-nok profit distribution.png):平均 2%,但极值能到 −6% 和 +6%。oil-nok profit heatmap.png,用 sns.heatmap):横轴止损点数、纵轴持仓天数、颜色代表收益率。
热力图给出的结论非常清晰,也是本节第二个反直觉发现:收益率与持仓天数强相关,与止损止盈点数几乎无关。最优持仓天数是 9 天,止损可以在 0.6~1.05 的宽区间里任选。
这背后的直觉是:这是一个动量策略,动量的强弱取决于"趋势能持续多久",而不是"单次波动有多大"。所以持仓周期(捕捉趋势)是关键,止损(控制单次波动)反而不痛不痒。

条件数是多重共线性的免费预警。OLS 摘要里那一行 Cond. No. 经常被忽略,但它一旦飙升(>30 就要警惕,>100 基本确认),就要切到 Elastic Net/Ridge。这是看 OLS 摘要的肌肉记忆。
L1 做变量选择、L2 做系数收缩。Elastic Net 的混合正则项让它比纯 Lasso(共线时挑一个扔其他)和纯 Ridge(全留但不归零)都更适合金融数据——金融变量本来就高度共线。
置信带 reset 回 forecast 线。这个细节看似只是画图,实则是策略逻辑的一部分:它标志着"模型破裂,进入趋势跟随模式"。视觉诚实也是策略诚实。
把失效点重新定义为入场点。这是本节最值钱的一招。当一个统计模型的预测被价格击穿时,第一反应不要是"模型错了赶紧修",而是问一句"这个击穿本身是不是信号?"。
参数搜索看热力图,不看平均值。平均值 2% 看着寒酸,但热力图告诉你极值能到 6%,而且对哪个参数敏感一目了然。直方图 + 热力图是参数搜索的标准可视化组合。
引用 Nick Patterson 的话做策略祛魅。冷战的密码学家、文艺复兴雇员,他说"Renaissance 用的就是简单一元回归,简单但有效,前提是知道如何避开那些坑"。这不是炫耀复杂模型,而是提醒:识别(找对变量)远比估计(调对参数)重要。本项目的 ±2σ 突破看似简陋,但因为识别对了"模型失效=趋势起点",反而有效。
下一节(第 03 节)把同样的方法套到 RUB 和 CAD 上——结果一个被制裁摧毁,一个被 dollar effect 揭穿。两个都是"证伪"案例,但证伪本身比确认更有教学意义。