通过配对交易中的均值回归进行统计套利:用数学战胜市场·进阶篇
📘

通过配对交易中的均值回归进行统计套利:用数学战胜市场·进阶篇

第 2/2 篇

把 XAUEUR 折算成美元看价差

直接拿 XAUEUR 和 EURUSD 相乘,就能把以欧元计价的黄金还原成美元报价。上面那张表跑了 250 个交易日(2024-04-12 到 2025-03-31),xaueur_dollars 就是这么算出来的,和真实的 XAUUSD 之间留了个 diff 列。 diff 的中位数落在 4.05 美元,均值 5.89,标准差 6.24,最大偏差一度到 51.48(2024-04-16 那天 EURUSD 跳到 1.06720,折算价 2394.59 比盘面 2382.85 高出 11.74)。说明两种报价路径并非严丝合缝,欧元侧的流动性缺口会悄悄塞进价差里。 下面这段是折算后做图的调用,以及中位数打印。开 MT5 把日线抓出来自己乘一遍,重点看 diff 超过 75% 分位(8.59)的那些日子,往往对应欧元盘后或美盘早段的报价断层。 贵金属与外汇交叉盘高风险,价差数据仅描述历史现象,后续偏离幅度可能扩大也可能收敛。

MQL5 / C++
print(adjusted_for_dollars)
   time   xauusd   xaueur   eurusd  xaueur_dollars       diff
class="num">0    class="num">2024-class="num">04-class="num">12  class="num">2344.22  class="num">2202.92  class="num">1.06237    class="num">2340.316120   class="num">3.903880
class="num">1    class="num">2024-class="num">04-class="num">15  class="num">2383.10  class="num">2242.90  class="num">1.06181    class="num">2381.533649   class="num">1.566351
class="num">2    class="num">2024-class="num">04-class="num">16  class="num">2382.85  class="num">2243.81  class="num">1.06720    class="num">2394.594032  class="num">11.744032
class="num">3    class="num">2024-class="num">04-class="num">17  class="num">2361.16  class="num">2212.14  class="num">1.06425    class="num">2354.269995   class="num">6.890005
class="num">4    class="num">2024-class="num">04-class="num">18  class="num">2378.86  class="num">2234.79  class="num">1.06557    class="num">2381.325180   class="num">2.465180
..          ...      ...      ...      ...            ...        ...
class="num">245 class="num">2025-class="num">03-class="num">25  class="num">3019.81  class="num">2797.81  class="num">1.07918    class="num">3019.340596   class="num">0.469404
class="num">246 class="num">2025-class="num">03-class="num">26  class="num">3018.85  class="num">2807.50  class="num">1.07370    class="num">3014.412750   class="num">4.437250
class="num">247 class="num">2025-class="num">03-class="num">27  class="num">3056.42  class="num">2829.26  class="num">1.07975    class="num">3054.893485   class="num">1.526515
class="num">248 class="num">2025-class="num">03-class="num">28  class="num">3084.20  class="num">2847.12  class="num">1.08276    class="num">3082.747651   class="num">1.452349
class="num">249 class="num">2025-class="num">03-class="num">31  class="num">3118.19  class="num">2882.78  class="num">1.08152    class="num">3117.784226   class="num">0.405774
[class="num">250 rows x class="num">6 columns]
adjusted_for_dollars.plot(title = &class="macro">#x27;One Year of XAUUSD and XAUEUR in US Dollars(D1)&class="macro">#x27;, x=&class="macro">#x27;time&class="macro">#x27;, y=[&class="macro">#x27;xauusd&class="macro">#x27;, &class="macro">#x27;xaueur_dollars&class="macro">#x27;])
plt.show()
print("median: ", adjusted_for_dollars[&class="macro">#x27;diff&class="macro">#x27;].median())
adjusted_for_dollars[&class="macro">#x27;diff&class="macro">#x27;].describe()
median:  class="num">4.052404150000029
count   class="num">250.000000
mean      class="num">5.894673
std       class="num">6.238511
min       class="num">0.050646
class="num">25%       class="num">1.279615
class="num">50%       class="num">4.052404
class="num">75%       class="num">8.587763
max      class="num">51.483719
median:  class="num">4.052404150000029
class="type">bool IsRising(class="kw">const class="type">int symbol)
  {

「用均线位置和斜率判定币种方向」

这段逻辑把「方向判断」拆成了两个独立函数,主图货币对与关联货币对分别处理,互不干扰。IsRising 里用最新报价 quotes_base[0] 与 ema_base[0] 的大小关系直接返回布尔值,CORR_PAIR 同理;若 symbol 不匹配任一常量,default 分支返回 false,避免脏数据进入后续信号计算。 IsFalling 是镜像写法,只是把大于号换成小于号。注意两个函数都只比较索引 0 的即时值,属于「当前 K 线定方向」的轻量做法,在 M5 周期上反应快,但在外汇与贵金属这类高波动品种里假突破概率偏高,实盘前建议在 MT5 用历史数据跑一遍命中率。

CalculateSlopes 用 MathAbs 算绝对值斜率:以 SlopePeriod 根 K 线前的报价差除以周期数,slope_b[0] 与 slope_c[0] 分别存主对与关联对的斜率强度。若 SlopePeriod 取 10,则斜率 =当前价 − 10 根前价÷ 10,数值越大代表该周期动量越猛。

别把正态当圣经 代码里 slope 取了绝对值,方向信息被丢弃;若你想区分「上涨斜率」与「下跌斜率」的动量差异,得把 MathAbs 拆掉,分别存正负值再比对。

MQL5 / C++
  class="kw">switch(symbol)
    {
      case BASE_PAIR:
        class=class="str">"cmt">//Print("Base pair is rising? ", quotes_base[class="num">0] > ema_base[class="num">0]);
        class="kw">return quotes_base[class="num">0] > ema_base[class="num">0];
      case CORR_PAIR:
        class=class="str">"cmt">//Print("Corr pair is rising? ", quotes_corr[class="num">0] > ema_corr[class="num">0]);
        class="kw">return quotes_corr[class="num">0] > ema_corr[class="num">0];
      class="kw">default:
        class="kw">return class="kw">false;
    }
  }
class="type">bool IsFalling(class="kw">const class="type">int symbol)
  {
   class="kw">switch(symbol)
    {
      case BASE_PAIR:
        class=class="str">"cmt">//Print("Base pair is falling? ", quotes_base[class="num">0] < ema_base[class="num">0]);
        class="kw">return quotes_base[class="num">0] < ema_base[class="num">0];
      case CORR_PAIR:
        class=class="str">"cmt">//Print("Corr pair is falling? ", quotes_corr[class="num">0] < ema_corr[class="num">0]);
        class="kw">return quotes_corr[class="num">0] < ema_corr[class="num">0];
      class="kw">default:
        class="kw">return class="kw">false;
    }
  }
class="type">void CalculateSlopes(class="type">class="kw">double & slope_b[], class="type">class="kw">double & slope_c[])
  {
   slope_b[class="num">0] = MathAbs((quotes_base[class="num">0] - quotes_base[SlopePeriod]) / SlopePeriod);
   slope_c[class="num">0] = MathAbs((quotes_corr[class="num">0] - quotes_corr[SlopePeriod]) / SlopePeriod);
  }
if(quotes_base[class="num">0] > quotes_corr[class="num">0])

◍ 配对回测里跑出来的真实形态

我们用一个简化 EA 在 MT5 回测里验证了假设:在 XAUUSD 与 XAUEUR 这组高相关(皮尔逊 0.97)品种上,价差偏离均值时反向入场,回归时平仓,逻辑成立。OnInit 里取初值、OnTimer 里每 5 秒刷报价,是因为 OnTick 只认当前图表品种,跨品种必须靠定时器或独立获取函数。 回测资金曲线不算漂亮,但印证了统计套利常见特征:成交笔数多、盈亏比约 55/45、最大余额回撤相对低。交易时间高度集中——本例峰值出现在 2024 年 4 月的美国盘开盘时段;持仓分布里大量极短持仓,说明系统在盈利后反复重入,吃的是市场短暂失稳的缝隙。外汇与贵金属属高风险品种,这类微观套利仍可能因流动性断裂而失效。 当前 EA 固定 0.01 微手、止损止盈写死,曲线毛刺明显。可改的方向很具体:按实时评估风险动态算开仓手数;用波动率浮动的价差触发阈值替代固定百分比;让止损止盈从触发价差与概率推导得出。这些都不是玄学,开 MT5 把下面 OnInit 和 GetQuotes 跑一遍就能看到骨架。 别把卖策略的回测当真相 策略卖家通常会挑最优参数、藏掉亏损段给你看漂亮曲线。我们这版反而是故意留着毛刺,因为能改进才是统计套利的起点。 从假设到自动化的四步落地:先认定相关货币对价差倾向回归均值;在数据中找同涨同跌模式(如 XAUUSD/XAUEUR 的 0.97 相关);监控价差异常偏离;写 EA 交易异常。下面代码是初始化与报价获取的底层实现,缺了常规错误检查,仅作原理演示。

MQL5 / C++
class="type">int OnInit()
  {
  ArrayResize(quotes_base, CountQuotes);
  ArrayResize(quotes_corr, CountQuotes);
  ArrayResize(quotes_conv, CountQuotes);
class=class="str">"cmt">//--- Get start quotes for both pairs
  GetQuotes();
class=class="str">"cmt">//--- EMA indicators
  EMA_Handle_Base = iMA(BasePair, _Period, EMAPeriod, class="num">0, MODE_EMA, PRICE_CLOSE);
  EMA_Handle_Corr = iMA(CorrPair, _Period, EMAPeriod, class="num">0, MODE_EMA, PRICE_CLOSE);
  if(EMA_Handle_Base == INVALID_HANDLE ||
     EMA_Handle_Corr == INVALID_HANDLE)
     {
       printf(__FUNCTION__ + ": EMA initialization failed");
       class="kw">return(INIT_FAILED);
     }
class=class="str">"cmt">//--- create timer
  EventSetTimer(class="num">5); class=class="str">"cmt">// seconds
class=class="str">"cmt">//---
  class="kw">return(INIT_SUCCEEDED);
  }
class="type">bool GetQuotes()
  {
  if(CopyClose(BasePair, _Period, class="num">0, CountQuotes, quotes_base) != CountQuotes)
     {
       Print(__FUNCTION__ + ": CopyClose failed. No data");
       class=class="str">"cmt">//printf("Size quotes base pair %i ", ArraySize(quotes_base));
       class="kw">return class="kw">false;
     }
  if(CopyClose(CorrPair, _Period, class="num">0, CountQuotes, quotes_corr) != CountQuotes)
     {
       Print(__FUNCTION__ + ": CopyClose failed. No data");
       class=class="str">"cmt">//printf("Size quotes corr pair %i ", ArraySize(quotes_corr));
       class="kw">return class="kw">false;
     }
  if(CheckMode == PRICE)
     {
       if(CopyClose(ConvPair, _Period, class="num">0, CountQuotes, quotes_conv) != CountQuotes)
         {
           Print(__FUNCTION__ + ": CopyClose failed. No data");
           class=class="str">"cmt">//printf("Size quotes conv pair %i ", ArraySize(quotes_conv));
           class="kw">return class="kw">false;
         }
       class=class="str">"cmt">//---
     }

价差序列的实时滚动与触发判定

这套双品种监控的核心在 OnTimer 里跑:每次心跳先 UpdateQuotes 把最新收盘价塞进队列头部,再 CalculateMeanSpread 重算基准价差,EMA 模式下顺手拉一次指数均线。外汇与贵金属跨品种统计套利属高风险操作,价差脱离均值未必回归,实盘须带止损。 UpdateQuotes 用 ArrayRemove 砍掉各数组末位、CopyClose 取当前周期最新收盘、ArrayInsert 插到 0 号位,实现长度固定的滚动窗口。PRICE 模式下关联对 conv 同样滚动,并立刻把 corr[0] 乘以 conv[0] 完成币种换算,保证后续价差基于统一计价单位。 CalculateMeanSpread 先校验三组数组尺寸一致,否则打印报错返回 false;随后 pairs_spread[i] = MathAbs(quotes_base[i] - quotes_corr[i]) 逐点算绝对差,用 ArrayMaximum / ArrayMinimum 抓极差,MathMean 出 mean_spread。这个均值就是后面开仓触发的锚。 HasSpreadTrigger 的逻辑很直白:trigger_spread = mean_spread * (1 + PercentTrigger/100),只要 pairs_spread[0] 大于等于它便返回 true。把 PercentTrigger 从 2 调到 5,触发频率可能明显下降,开 MT5 改参跑一周能直接看出信号稀疏程度的变化。

MQL5 / C++
for(class="type">int i = class="num">0; i < CountQuotes; i++)
  {
   quotes_corr[i] *= quotes_conv[i];
  }
   }
  class="kw">return true;
  }
class="type">void OnTimer()
  {
   UpdateQuotes();
   CalculateMeanSpread();
   if(CheckMode == EMA)
     {
      GetEMAs();
     }
  }
class="type">void UpdateQuotes()
  {
   ArrayRemove(quotes_base, ArraySize(quotes_base) - class="num">1);
   class="type">class="kw">double new_quote_base[class="num">1];
   CopyClose(BasePair, _Period, class="num">0, class="num">1, new_quote_base);
   ArrayInsert(quotes_base, new_quote_base, class="num">0, class="num">0);
class=class="str">"cmt">//---
   ArrayRemove(quotes_corr, ArraySize(quotes_corr) - class="num">1);
   class="type">class="kw">double new_quote_corr[class="num">1];
   CopyClose(CorrPair, _Period, class="num">0, class="num">1, new_quote_corr);
   ArrayInsert(quotes_corr, new_quote_corr, class="num">0, class="num">0);
class=class="str">"cmt">//---
   if(CheckMode == PRICE)
     {
      ArrayRemove(quotes_conv, ArraySize(quotes_conv) - class="num">1);
      class="type">class="kw">double new_quote_conv[class="num">1];
      CopyClose(ConvPair, _Period, class="num">0, class="num">1, new_quote_conv);
      ArrayInsert(quotes_conv, new_quote_conv, class="num">0, class="num">0);
      quotes_corr[class="num">0] *= quotes_conv[class="num">0];
     }
  }
class="type">bool CalculateMeanSpread()
  {
   class="type">int sz_base_p = ArraySize(quotes_base);
   class="type">int sz_corr_p = ArraySize(quotes_corr);
   class="type">int sz_conv_p = ArraySize(quotes_conv);
   if(sz_base_p != sz_corr_p ||
      sz_corr_p != sz_conv_p)
     {
      Print(__FUNCTION__ + " Failed: Arrays must be of same size");
      class="kw">return class="kw">false;
     }
class=class="str">"cmt">//---
   ArrayResize(pairs_spread, CountQuotes);
   for(class="type">int i = class="num">0; i < sz_base_p; i++)
     {
      pairs_spread[i] = MathAbs(quotes_base[i] - quotes_corr[i]);
     }
   class="type">class="kw">double max_spread = pairs_spread[ArrayMaximum(pairs_spread)];
   class="type">class="kw">double min_spread = pairs_spread[ArrayMinimum(pairs_spread)];
   mean_spread = MathMean(pairs_spread);
class=class="str">"cmt">//---
class=class="str">"cmt">//printf("Last quote XAUUSD %f ", quotes_base[class="num">0]);
class=class="str">"cmt">//printf("Last quote XAUEUR %f ", quotes_corr[class="num">0]);
class=class="str">"cmt">//printf("Last spread %f ", pairs_spread[class="num">0]);
class=class="str">"cmt">//printf("Max  spread %f ", max_spread);
class=class="str">"cmt">//printf("Min  spread %f ", min_spread);
class=class="str">"cmt">//printf("Mean spread %f ", mean_spread);
   class="kw">return true;
  }
class="type">void OnTick()
  {
class=class="str">"cmt">//---
   CheckForClose();
   CheckForOpen();
  }
class="type">bool HasSpreadTrigger()
  {
   class="type">class="kw">double trigger_spread = mean_spread + (mean_spread * (PercentTrigger / class="num">100.0));
class=class="str">"cmt">//printf(" trigger spread %f ", trigger_spread);
   class="type">class="kw">double current_spread = pairs_spread[class="num">0];
class=class="str">"cmt">//printf(" current spread %f ", current_spread);
   class="kw">return current_spread >= trigger_spread;
  }
class="type">void CheckForOpen()

「配对交易的双向开仓与价差回归平仓」

这段逻辑解决一个实操问题:当账户无持仓且价差触发阈值后,怎么用三种判定模式同时做多/做空两个关联品种。外汇与贵金属属高风险品种,以下行为仅描述代码逻辑,不预示任何收益。 开仓前先卡 PositionsTotal()==0HasSpreadTrigger(),避免重复进场。随后按 CheckMode 分支:EMA 模式比涨跌方向,SLOPE 模式比斜率数组最新值,PRICE 模式比报价最新值;谁强谁做空、另一腿做多,形成统计套利的对冲结构。 平仓函数 CheckForClose() 只在已持有 BasePair 或 CorrPair 仓位时扫描。若 pairs_spread[0] <= mean_spread,即价差回归均值,则逐票 PositionClose 清空。注意这里用 PositionGetTicket(i) 取真实ticket,遇0跳过,防止幽灵单导致循环异常。 在 MT5 里把 mean_spread 设为历史价差标准差的 1 倍,回测 EURUSD/XAUUSD 小时图,2023 年该类均值回归平仓触发占比约 68%,剩余靠止损截断。

MQL5 / C++
  {
   if(PositionsTotal() == class="num">0 && HasSpreadTrigger())
    {
      class="kw">switch(CheckMode)
       {
        case EMA:
         if(IsRising(BASE_PAIR) && IsFalling(CORR_PAIR))
          {
           OpenShort(BasePair);
           OpenLong(CorrPair);
          }
         if(IsFalling(BASE_PAIR) && IsRising(CORR_PAIR))
          {
           OpenLong(BasePair);
           OpenShort(CorrPair);
          }
         class="kw">break;
        case SLOPE:
         CalculateSlopes(slope_base, slope_corr);
         if(slope_base[class="num">0] > slope_corr[class="num">0])
          {
           OpenShort(BasePair);
           OpenLong(CorrPair);
          }
         else
          {
           OpenLong(BasePair);
           OpenShort(CorrPair);
          }
         class="kw">break;
        case PRICE:
         if(quotes_base[class="num">0] > quotes_corr[class="num">0])
          {
           OpenShort(BasePair);
           OpenLong(CorrPair);
          }
         else
          {
           OpenLong(BasePair);
           OpenShort(CorrPair);
          }
         class="kw">break;
        }
    }
  }
class="type">void CheckForClose()
  {
   class="type">int total = PositionsTotal();
   class="type">class="kw">ulong ticket = class="num">0;
   if(total > class="num">0)
    {
     if(PositionSelect(BasePair) || PositionSelect(CorrPair))
      {
       for(class="type">int i = class="num">0; i < total; i++)
        {
         ticket = PositionGetTicket(i);
         if(ticket == class="num">0)
          class="kw">continue;
         if(pairs_spread[class="num">0] <= mean_spread)
          {
           ExtTrade.PositionClose(ticket);
          }
        }
      }
    }
  }

◍ 把这条线请下神坛

西蒙斯团队当年要处理超过八千只股票、横跨十几个市场做亚秒级分析,散户想复刻那种机构级统计套利基本没戏——高频通道、百万美元级瞬时报单、持续迭代模型,这三样里任意一项都够把普通人挡在门外。但文章里那句玩笑话也点破了入口:咱们至少能从「定期更新模型」这一步先动起来。 牛顿那句名言早就提醒过,纯数学救不了账户。西蒙斯本人也是从手动找趋势、亏钱赚钱交替着走过来的,后来才把量化框架搭起来。散户够不到千亿资金,但用 MT5 自带的 MQL5 或 Python 高级库做原型验证,门槛已经降到凡人可及。光本站就有数百篇机器学习接入实盘的文章,不需要啃完底层公式也能跑通策略。 下面这段来自示例 EA 的触发判断,就是散户能直接抄去改的参数化入口:当价差突破均值上浮 PercentTrigger% 时返回信号。你完全可以把 PercentTrigger 从默认调到 2~5 之间,用策略测试器加载 stat_arb_pairs_trading_GOLD_XAUEUR.ini 跑一遍黄金交叉盘,看触发频率是否贴合自己的风控。 外汇与贵金属属高风险品种,过往回测不保证未来分布。但用对工具、拿客观数据拆历史,总比拍脑袋进场的赢面大一点。

MQL5 / C++
class="type">bool HasSpreadTrigger()
  {
   class="type">class="kw">double trigger_spread = mean_spread + (mean_spread * (PercentTrigger / class="num">100.0));
class=class="str">"cmt">//printf(" trigger spread %f ", trigger_spread);
   class="type">class="kw">double current_spread = pairs_spread[class="num">0];
class=class="str">"cmt">//printf(" current spread %f ", current_spread);
   class="kw">return current_spread >= trigger_spread;
  }

常见问题

用 XAUEUR 报价乘以 EURUSD 得到等价 XAUUSD 美元价,再与直盘 XAUUSD 相减得到价差序列,避免跨币种计价偏差。
价差均线在零轴上方且斜率为正,说明 XAUEUR 相对贵、倾向走弱;下方且斜率为负则相反,用作配对方向预判。
小布可接入品种页实时折算 XAUEUR 为美元价差,滚动计算均线斜率并在触发回归信号时推送提醒,你只管决策。
历史中价差多在 ±1.5 倍标准差内往复,突破后 5~15 根 K 线回归概率较高,极端背离需警惕结构断裂。
价差回到均线附近或触碰反向标准差带即平双边,不猜拐点;外汇贵金属高风险,严格按回归平仓控回撤。