基于预测的统计套利(基础篇)
用统计套利做跨品种预测
统计套利不靠方向猜顶底,而是抓两个高相关品种价差的均值回归。MT5 上这类系统通常先算协整关系,再在 z-score 偏离阈值时反向建仓,外汇与贵金属配对(如 XAUUSD 与 XAGUSD)在高波动期价差扩张,回归概率倾向随流动性恢复而上升,但杠杆品种回撤可能超 20%,属高风险操作。 原文作者 Javier Santiago Gaston De Iriarte Cabrera 于 2024 年 11 月 19 日发布该思路,社区展示阅读量 1361,说明这类非方向性策略在零售圈有稳定关注。 落地时先在 MT5 符号列表里挑相关系数 >0.8 的品种对,用内置相关系数工具验证 90 天窗口,再决定阈值。别把正态当圣经——价差在事件驱动下会长期偏离,需加波动率过滤。
「统计套利底层的三个统计量」
统计套利不靠方向判断吃饭,它赚的是相关金融工具之间价格低效回归的钱。模型要成立,先得啃下相关性、协整、皮尔逊系数这三块基石,否则配对标的选出来也是假关联。 相关性只回答一件事:两个证券价格变动贴得多紧。正相关的俩标的通常同向走,负相关的反过来。但光看相关性有个坑——它不保证关系稳定,一段行情里同涨同跌,下一段可能就拆伙。 协整比相关性更刁钻。它检验的是若干时间序列的线性组合能否长期保持平稳。白话讲:两只票各自乱走,但它们的价差受某条均衡线拽着,偏向了就会被拉回。配对交易的核心假设就立在这,标的历史上黏在一起才值得盯。 皮尔逊系数给相关性加了把尺:值域锁死在 -1 到 1,1 是完全同向往,−1 是完全反向,0 是没线性关系。实战里若两资产绝对值偏高,且你判断它会回长期均值,才算露出交易口子。外汇与贵金属跨品种统计套利属高风险,突发政策或流动性断裂会让均衡暂时失效。 真正跑这套的人靠算法和高频系统扫数据、抓偏离、自动下单。模型假设价格终将收敛历史均值,但经济冲击或情绪踩踏能撕开最稳的关系,所以人工重判不可省。
◍ 用生活场景拆开相关性与协整
相关性只是描述两件事是否同步发生。你和你朋友总在周六去同一家影院,这就是正相关:你出现,他大概率也在;若为负相关,你去了他反而不去;相关性为 0 则两者毫无同步痕迹。 协整比相关性更硬核,它管的是长期约束。把两个泳者用绳子拴住,短期各自乱游、距离时远时近,但绳长限定了他们不会彻底散开——这就是协整:变量短期可背离,长期必回到共同均衡。外汇与贵金属跨品种价差交易常依赖这种约束,但杠杆环境下短期背离就可能触发强平,属高风险操作。 皮尔逊相关系数给相关性一个数值刻度。接近 +1 是同涨同跌,接近 -1 是反向,0 是无线性关联。实测温度与冷饮销量会得到显著正系数,这类线性度量在 MT5 里用 iPearson 类函数即可批量算跨品种历史样本,用于筛候选配对。
用相关系数筛掉噪声货币对
想找出真正值得盯的联动品种,先得把弱相关和伪协整的过滤掉。一段 Python 脚本直连 MT5 终端,拉取名称含 EUR/USD 或结尾为 USD 的日线品种,取最近 365 根 D1 收盘,做两两 Pearson 相关系数与协整检验。 门槛设得狠一点:相关系数绝对值大于 0.8 才算强相关,协整 p 值小于 0.05 才算统计显著。跑完一轮,从全市场 EUR/USD 系里筛出 54 对同时满足两项条件的组合。 几个典型结果有:EURUSD 与 USDHRK 相关系数 -0.9972、p 值 0.0000;GBPUSD 与 GBXUSD 相关系数 0.9998、p 值 0.0000;AUDUSD 与 USDCNH 相关系数 -0.8074、p 值 0.0390。外汇与贵金属杠杆高、点差跳变频繁,这类数字仅代表历史样本的统计倾向,实盘须以 MT5 重新验算。 下方是脚本原文,复制进本地 Python 环境(需装 MetaTrader5、pandas、scipy、statsmodels)即可复跑,改 symbols 过滤规则就能换自己的观察池。
class="kw">import MetaTrader5 as mt5 class="kw">import pandas as pd from scipy.stats class="kw">import pearsonr from statsmodels.tsa.stattools class="kw">import coint class="kw">import numpy as np # Connect with MetaTrader class="num">5 if not mt5.initialize(): print("Failed to initialize MT5") mt5.shutdown() # Get the list of symbols symbols = mt5.symbols_get() symbols = [s.name for s in symbols if s.name.startswith(&class="macro">#x27;EUR&class="macro">#x27;) or s.name.startswith(&class="macro">#x27;USD&class="macro">#x27;) or s.name.endswith(&class="macro">#x27;USD&class="macro">#x27;)] # Filtrar símbolos por ejemplo # Download historical data and save in dictionary data = {} for symbol in symbols: rates = mt5.copy_rates_from_pos(symbol, mt5.TIMEFRAME_D1, class="num">0, class="num">365) # Último año, diario if rates is not None: df = pd.DataFrame(rates) df[&class="macro">#x27;time&class="macro">#x27;] = pd.to_datetime(df[&class="macro">#x27;time&class="macro">#x27;], unit=&class="macro">#x27;s&class="macro">#x27;) data[symbol] = df.set_index(&class="macro">#x27;time&class="macro">#x27;)[&class="macro">#x27;close&class="macro">#x27;] # Close connection with MT5 mt5.shutdown() # Calculate the Pearson coefficient and test for cointegration for each pair of symbols cointegrated_pairs = [] for i in range(len(symbols)): for j in range(i + class="num">1, len(symbols)): if symbols[i] in data and symbols[j] in data: common_index = data[symbols[i]].index.intersection(data[symbols[j]].index) if len(common_index) > class="num">30: # Asegurarse de que hay suficientes puntos de datos corr, _ = pearsonr(data[symbols[i]][common_index], data[symbols[j]][common_index]) if abs(corr) > class="num">0.8: # Correlación fuerte score, p_value, _ = coint(data[symbols[i]][common_index], data[symbols[j]][common_index]) if p_value < class="num">0.05: # P-valor menor que class="num">0.05 cointegrated_pairs.append((symbols[i], symbols[j], corr, p_value)) # Filter and show only cointegrated pairs with p-value less than class="num">0.05 print(f&class="macro">#x27;Total pairs with strong correlation and cointegration: {len(cointegrated_pairs)}&class="macro">#x27;) for sym1, sym2, corr, p_val in cointegrated_pairs: print(f&class="macro">#x27;{sym1} - {sym2}: Correlación={corr:.4f}, P-valor de Cointegración={p_val:.4f}&class="macro">#x27;)
「跨品种协整配对的实际读数」
下面这组跨市场配对,是把 Pearson 相关系数与协整 P 值同时跑出来的结果。P 值小于 0.05 通常意味着两序列在样本期内存在统计上显著的长期均衡关系,但外汇与贵金属本身杠杆高、跳空频繁,协整随时可能被宏观事件打断,实盘只能当作概率参考。 EURJPY 与 USDTRY 的相关性达到 0.9564,协整 P 值 0.0102;EURCZK 与 USDCLP 相关性 0.9655,P 值仅 0.0001。这类高相关且低 P 值的组合,在 MT5 里适合做价差回归类的观察,而不是单向押注。 也有反向案例值得记:EURJPY 与 EURMXN 相关性 -0.8785(P 值 0.0172),EURNZD 与 NZDUSD 相关性 -0.8505(P 值 0.0455)。负向配对在价差拉大到历史带宽外时,倾向触发均值回归,但必须带止损。 USDBGN-USDHRK 与 USDHRK-USDDKK 的相关系数分别 0.9944 和 0.9954,P 值都是 0.0000,基本是钉住汇率的镜像;这种组合在真实交易里几乎没有价差空间,列出只是提醒你别被高相关误导成可套利机会。
◍ 把跨品种皮尔逊相关做成副图指标
想在 MT5 副窗里直接看两个品种的相关强度,可以把皮尔逊相关系数写成自定义指标。下面这段初始化只开一个缓冲、画红线,短名自动拼成「主图品种 & GBPUSD」,加载后一眼能认出配对。 参数上默认取 GBPUSD 作第二品种、回看 100 根 K 线算相关。BarsBack 设太小,相关系数会被近期噪声明暗摆动带偏;外汇与贵金属跨品种相关本就随宏观切换,属高风险观察量,数值只代表历史窗口内的线性耦合,不代表未来必然延续。 OnCalculate 里先卡 rates_total < BarsBack 直接返 0,避免样本不足算出 nonsense;随后把两个价格序列各扩到 BarsBack 长度,准备拷历史收盘价。这套骨架你拷进 MetaEditor 就能编译,把 Symbol2 换成 XAUUSD 便能量黄金与美指代理品种的相关漂移。
class="macro">#class="kw">property copyright "Javier S. Gastón de Iriarte Cabrera" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#class="kw">property indicator_separate_window class="macro">#class="kw">property indicator_buffers class="num">1 class="macro">#class="kw">property indicator_color1 Red input class="type">class="kw">string Symbol2 = "GBPUSD"; class=class="str">"cmt">// Second financial instrument input class="type">int BarsBack = class="num">100; class=class="str">"cmt">// Number of bars to include in correlation calculation class="type">class="kw">double CorrelationBuffer[]; class="type">int OnInit() { SetIndexBuffer(class="num">0, CorrelationBuffer, INDICATOR_DATA); PlotIndexSetInteger(class="num">0, PLOT_DRAW_TYPE, DRAW_LINE); PlotIndexSetString(class="num">0, PLOT_LABEL, "Pearson Correlation"); IndicatorSetString(INDICATOR_SHORTNAME, "Pearson Correlation(" + Symbol() + " & " + Symbol2 + ")"); class="kw">return INIT_SUCCEEDED; } class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">class="kw">datetime &time[], class="kw">const class="type">class="kw">double &open[], class="kw">const class="type">class="kw">double &high[], class="kw">const class="type">class="kw">double &low[], class="kw">const class="type">class="kw">double &close[], class="kw">const class="type">long &tick_volume[], class="kw">const class="type">long &volume[], class="kw">const class="type">int &spread[]) { if (rates_total < BarsBack) class="kw">return class="num">0; class=class="str">"cmt">// Ensure enough bars are present class="type">class="kw">double prices1[], prices2[]; ArrayResize(prices1, BarsBack); ArrayResize(prices2, BarsBack); class=class="str">"cmt">// Copy historical data for primary symbol