基于预测的统计套利·综合运用
📊

基于预测的统计套利·综合运用

(3/3)· 当协整与皮尔逊系数遇上 MT5 实盘,前两步铺垫后这一步决定策略能否活过震荡

偏理论 第 3/3 篇
很多交易者把统计套利当成相关性高就开仓的捷径,却忽略协整约束和均值回归的时间窗。用 MT5 策略测试器跑通逻辑前,先理清哪些关系只是短期巧合,哪些才是可交易的均衡偏移。

「用MT5测试器跑ONNX套利EA的反向验证」

拿到训练好的ONNX模型后,EA即可在MT5里运行。我选了NZDUSD与AUDUSD这对组合做演示:实操时它们原本协整且相关,但当下协整度已掉到0.05阈值以下,未过筛选;为省去重训模型的成本,教学里仍沿用这两类品种。 测试设定为:品种在1分钟周期上成交,ONNX模型基于1天周期预测,止损与止盈都设1500点,预测样本区间2023年1月1日至2024年1月初。外汇与贵金属杠杆高、点差跳变频繁,这类历史回测只反映过去概率,实盘可能明显偏离。 想换交易对,直接改代码里的 symbol2 输入参数,并把两个 #resource 指向对应品种的.onnx文件即可,不必动主体逻辑。下面这段声明了回测所需的模型路径与基础风控参数。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 Hybrid Arbitrage_Statistic ONNX.mq5|
class=class="str">"cmt">//|                 Copyright class="num">2024, Javier S. Gastón de Iriarte Cabrera. |
class=class="str">"cmt">//|                       [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright   "Copyright class="num">2024, 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 strict
class="macro">#include <Trade\Trade.mqh>
input class="type">class="kw">double lotSize = class="num">0.1;
class=class="str">"cmt">//input class="type">class="kw">double slippage = class="num">3;
input class="type">class="kw">double stopLoss = class="num">1500;
input class="type">class="kw">double takeProfit = class="num">1500;
class=class="str">"cmt">//input class="type">class="kw">double maxSpreadPoints = class="num">10.0;
class="macro">#resource "/Files/art/hybrid/NZDUSD_D1.onnx" as class="type">uchar ExtModel[]
class="macro">#resource "/Files/art/hybrid/AUDUSD_D1.onnx" as class="type">uchar ExtModel2[]
class="macro">#define SAMPLE_SIZE class="num">120
class="type">class="kw">string symbol1 = _Symbol;
input class="type">class="kw">string symbol2 = "AUDUSD";
class="type">class="kw">ulong ticket1 = class="num">0;
class="type">class="kw">ulong ticket2 = class="num">0;
input class="type">bool isArbitrageActive = true;
CTrade ExtTrade;
class="type">class="kw">double spreads[class="num">1000]; class=class="str">"cmt">// Array para almacenar hasta class="num">1000 spreads
class="type">int spreadIndex = class="num">0; class=class="str">"cmt">// Índice para el próximo spread a almacenar
class="type">long    ExtHandle=INVALID_HANDLE;
class=class="str">"cmt">//class="type">int     ExtPredictedClass=-class="num">1;
class="type">class="kw">datetime ExtNextBar=class="num">0;
class="type">class="kw">datetime ExtNextDay=class="num">0;
class="type">class="kw">float   ExtMin=class="num">0.0;
class="type">class="kw">float   ExtMax=class="num">0.0;
class="type">long    ExtHandle2=INVALID_HANDLE;
class=class="str">"cmt">//class="type">int     ExtPredictedClass=-class="num">1;
class="type">class="kw">datetime ExtNextBar2=class="num">0;
class="type">class="kw">datetime ExtNextDay2=class="num">0;
class="type">class="kw">float   ExtMin2=class="num">0.0;
class="type">class="kw">float   ExtMax2=class="num">0.0;
class="type">class="kw">float predicted=class="num">0.0;
class="type">class="kw">float predicted2=class="num">0.0;
class="type">class="kw">float  lastPredicted1=class="num">0.0;
class="type">class="kw">float  lastPredicted2=class="num">0.0;
class="type">int Order=class="num">0;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
  Print("EA de arbitraje ONNX iniciado");
class=class="str">"cmt">//--- create a model from class="kw">static buffer
  ExtHandle=OnnxCreateFromBuffer(ExtModel,ONNX_DEFAULT);
  if(ExtHandle==INVALID_HANDLE)
    {

给 ONNX 张量显式锁死维度

从静态缓冲区建好 ONNX 模型后,不能直接丢数据进去跑。很多导出的模型在输入、输出张量里没写死全部维度,MT5 端必须手动用 OnnxSetInputShape / OnnxSetOutputShape 把形状钉住,否则初始化会直接失败返回 INIT_FAILED。 输入形状按 {1, SAMPLE_SIZE, 1} 设:第一维是 batch size(一次只推 1 条),第二维是序列长度由 SAMPLE_SIZE 控制,第三维是通道数——这里只喂 Close 所以为 1。输出形状设 {1, 1},批次数必须和输入对齐,第二维是预测价格个数(同样只预测 Close)。 代码里建了两个模型句柄 ExtHandle 和 ExtHandle2,各自独立走一遍创建加设维度的流程,任一步 GetLastError 非 0 就打印并回 INIT_FAILED。实盘加载 EA 前,建议先在策略测试器里看日志有没有 OnnxSetInputShape error,外汇和贵金属波动大,模型维度错配会比手工信号更慢暴露问题。 退出时 OnDeinit 里对两个句柄判不等于 INVALID_HANDLE 才调 OnnxRelease 并置回 INVALID_HANDLE,避免重复释放句柄泄漏。开 MT5 把这段抄进你的 EA 初始化函数,改 SAMPLE_SIZE 就能验证不同回看窗口是否报维度错。

MQL5 / C++
  Print("OnnxCreateFromBuffer error ",GetLastError());
  class="kw">return(INIT_FAILED);
   }
class=class="str">"cmt">//--- since not all sizes defined in the input tensor we must set them explicitly
class=class="str">"cmt">//--- first index - batch size, second index - series size, third index - number of series(only Close)
   class="kw">const class="type">long input_shape[] = {class="num">1,SAMPLE_SIZE,class="num">1};
   if(!OnnxSetInputShape(ExtHandle,ONNX_DEFAULT,input_shape))
   {
      Print("OnnxSetInputShape error ",GetLastError());
      class="kw">return(INIT_FAILED);
   }
class=class="str">"cmt">//--- since not all sizes defined in the output tensor we must set them explicitly
class=class="str">"cmt">//--- first index - batch size, must match the batch size of the input tensor
class=class="str">"cmt">//--- second index - number of predicted prices(we only predict Close)
   class="kw">const class="type">long output_shape[] = {class="num">1,class="num">1};
   if(!OnnxSetOutputShape(ExtHandle,class="num">0,output_shape))
   {
      Print("OnnxSetOutputShape error ",GetLastError());
      class="kw">return(INIT_FAILED);
   }
class=class="str">"cmt">//--- create a model from class="kw">static buffer
   ExtHandle2=OnnxCreateFromBuffer(ExtModel2,ONNX_DEFAULT);
   if(ExtHandle2==INVALID_HANDLE)
   {
      Print("OnnxCreateFromBuffer error ",GetLastError());
      class="kw">return(INIT_FAILED);
   }
class=class="str">"cmt">//--- since not all sizes defined in the input tensor we must set them explicitly
class=class="str">"cmt">//--- first index - batch size, second index - series size, third index - number of series(only Close)
   class="kw">const class="type">long input_shape2[] = {class="num">1,SAMPLE_SIZE,class="num">1};
   if(!OnnxSetInputShape(ExtHandle2,ONNX_DEFAULT,input_shape2))
   {
      Print("OnnxSetInputShape error ",GetLastError());
      class="kw">return(INIT_FAILED);
   }
class=class="str">"cmt">//--- since not all sizes defined in the output tensor we must set them explicitly
class=class="str">"cmt">//--- first index - batch size, must match the batch size of the input tensor
class=class="str">"cmt">//--- second index - number of predicted prices(we only predict Close)
   class="kw">const class="type">long output_shape2[] = {class="num">1,class="num">1};
   if(!OnnxSetOutputShape(ExtHandle2,class="num">0,output_shape2))
   {
      Print("OnnxSetOutputShape error ",GetLastError());
      class="kw">return(INIT_FAILED);
   }
   class="kw">return(INIT_SUCCEEDED);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {
   if(ExtHandle!=INVALID_HANDLE)
   {
      OnnxRelease(ExtHandle);
      ExtHandle=INVALID_HANDLE;
   }
   if(ExtHandle2!=INVALID_HANDLE)
   {
      OnnxRelease(ExtHandle2);

◍ 跨品种价差与日内极值跟踪的 tick 逻辑

这段 OnTick 是跨品种套利 EA 的心脏:每天零点重置一次极值捕获,每根 bar 刷新当前价与预测价,再把实时价差塞进环形缓冲。外汇与贵金属跨品种组合的高波动特性意味着,价差跳变可能在数秒内吞掉当日盈利,必须靠 tick 级监控而非日线收盘才动作。 日界判断用 TimeCurrent()>=ExtNextDay 触发 GetMinMax / GetMinMax2,随后把 ExtNextDay 对齐到 PERIOD_D1 的整点边界。注意源码里那段 PositionClose 的注释块——作者刻意留空,说明日切时不强制平仓,实盘若套用需自担隔夜跳空风险。 新 bar 未到就直接 return,避免重复计算;到了则重算 ExtNextBar 并即时更新 ExtMin/ExtMax 与 ExtMin2/ExtMax2。这里用 iClose(symbol, PERIOD_D1, 0) 取日线收盘价做极值,若你跑 XAUUSD 对比 XAGUSD,日线收盘价刷新慢,tick 内极值可能滞后于真实高低点。 价差采集部分把 currentSpread 写进 spreads[spreadIndex % 1000],索引自增,最多保留 1000 个样本。count = MathMin(spreadIndex, 1000) 决定标准差计算的可用长度——这意味着冷启动前几百 tick 的统计置信度偏低,信号可能偏噪。开 MT5 把这段贴进 EA,打印 Predicted spread 与 Last Predicted spread,你能直接看到预测价差收敛或发散的概率倾向。

MQL5 / C++
   ExtHandle2=INVALID_HANDLE;
   }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
class=class="str">"cmt">//--- check new day
   if(TimeCurrent()>=ExtNextDay)
     {
      GetMinMax();
      GetMinMax2();
      class=class="str">"cmt">//--- set next day time
      ExtNextDay=TimeCurrent();
      ExtNextDay-=ExtNextDay%PeriodSeconds(PERIOD_D1);
      ExtNextDay+=PeriodSeconds(PERIOD_D1);
      class=class="str">"cmt">/*ExtTrade.PositionClose(symbol1);
      ExtTrade.PositionClose(symbol2);
      ticket1 = class="num">0;
      ticket2 = class="num">0;*/
     }
class=class="str">"cmt">//--- check new bar
   if(TimeCurrent()<ExtNextBar)
     {
      class="kw">return;
     }
class=class="str">"cmt">//--- set next bar time
   ExtNextBar=TimeCurrent();
   ExtNextBar-=ExtNextBar%PeriodSeconds();
   ExtNextBar+=PeriodSeconds();
class=class="str">"cmt">//--- check min and max
   class="type">class="kw">float close=(class="type">class="kw">float)iClose(symbol1,PERIOD_D1,class="num">0);
   if(ExtMin>close)
      ExtMin=close;
   if(ExtMax<close)
      ExtMax=close;
   class="type">class="kw">float close2=(class="type">class="kw">float)iClose(symbol2,PERIOD_D1,class="num">0);
   if(ExtMin2>close2)
      ExtMin2=close2;
   if(ExtMax2<close2)
      ExtMax2=close2;
   lastPredicted1=predicted;
   lastPredicted2=predicted2;
class=class="str">"cmt">//--- predict next price
   PredictPrice();
   PredictPrice2();
   if(!isArbitrageActive || ArePositionsOpen())
     {
      Print("Arbitraje inactivo o ya hay posiciones abiertas.");
      class="kw">return;
     }
   class="type">class="kw">double price1 = SymbolInfoDouble(symbol1, SYMBOL_BID);
   class="type">class="kw">double price2 = SymbolInfoDouble(symbol2, SYMBOL_ASK);
   class="type">class="kw">double currentSpread = MathAbs(price1 - price2);
   Print("current spread ", currentSpread);
   Print("Price1 ",price1);
   Print("Price2 ",price2);
   Print("PricePredicted1 ",predicted);
   Print("PricePredicted2 ",predicted2);
   Print("Last PricePredicted1 ",lastPredicted1);
   Print("Last PricePredicted2 ",lastPredicted2);
   class="type">class="kw">double predictedSpread = MathAbs(predicted - predicted2);
   Print("Predicted spread ", predictedSpread);
   class="type">class="kw">double LastpredictedSpread = MathAbs(lastPredicted1 - lastPredicted2);
   Print("Last Predicted spread ", LastpredictedSpread);
class=class="str">"cmt">// Almacenar el spread actual en el array y actualizar el índice
   spreads[spreadIndex % class="num">1000] = currentSpread;
   spreadIndex++;
class=class="str">"cmt">// Verifica si hay suficientes datos para calcular la desviación estándar
   class="type">int count = MathMin(spreadIndex, class="num">1000); class=class="str">"cmt">// Utiliza todos los datos disponibles hasta class="num">1000

「用标准差框住价差套利触发线」

当预测价差低于当前价差、且套利状态已激活时,代码会先算均值价差与整体样本标准差。均值取两个预测值的平均:meanSpread = (lastPredicted1 + lastPredicted2) / 2.0,标准差则基于全部 spread 数组重算,而非前面只取前 count 个样本的局部值。 触发线按均值 ±2 倍标准差铺设:shortEntry = meanSpread - 2*stdDevSpread 作为空单入场,longEntry = meanSpread + 2*stdDevSpread 作为多单入场,两者都以回归均值(meanSpread)为退出参考。这种 2σ 边界在外汇与贵金属价差序列中,约对应 95% 置信带,极端价差穿透后倾向均值回归,但属概率事件而非必然。 若 price1 跌破 shortEntry 且两持仓票为空,就下 Buy symbol1 / Sell symbol2 的对冲仓;若 price2 跌破 shortEntry 则反向开仓。开仓后立刻把 ticket 置 0,相当于强制等待下一轮条件重判。外汇与贵金属价差套利受滑点、点差跳变影响,实盘前务必在 MT5 策略测试器用真实 tick 回测该 2σ 逻辑。

MQL5 / C++
class="type">class="kw">double stdDevSpread = CalculateStdDev(spreads, class="num">0, count);
class=class="str">"cmt">//Print("StdDevSpread ", stdDevSpread);
class=class="str">"cmt">// Verifica si el spread es lo suficientemente bajo para el arbitraje
if(LastpredictedSpread< currentSpread)
  {
    class=class="str">"cmt">// Inicia el arbitraje si aún no está activo
    if(isArbitrageActive)
      {
        class=class="str">"cmt">//Print("max spread : ",maxSpreadPoints * _Point);
        class="type">class="kw">double meanSpread = (lastPredicted1 + lastPredicted2) / class="num">2.0;
        Print("mean spread: ",meanSpread);
        class="type">class="kw">double stdDevSpread = CalculateStdDev(spreads, class="num">0, ArraySize(spreads));
        Print("StdDevSpread ", stdDevSpread);
        class="type">class="kw">double shortEntry = meanSpread - class="num">2 * stdDevSpread ;
        class="type">class="kw">double shortExit = meanSpread;
        class="type">class="kw">double longEntry = meanSpread + class="num">2 * stdDevSpread ;
        class="type">class="kw">double longExit = meanSpread;
        Print("Long Entry: ", longEntry, " Short Entry: ", shortEntry);
        class=class="str">"cmt">// Comprueba si la condición de entrada corta se cumple para el arbitraje
        if(price1 < shortEntry && (ticket1 == class="num">0 || ticket2 == class="num">0))
          {
            Print("Preparando para abrir órdenes");
            Order = class="num">1;
            Print("Error al abrir posiciones de arbitraje: ", GetLastError());
            ticket1 = ExtTrade.PositionOpen(symbol1, ORDER_TYPE_BUY, lotSize, price1, price1 - stopLoss * _Point, price1 + takeProfit * _Point, "Arbitraje");
            ticket2 = ExtTrade.PositionOpen(symbol2, ORDER_TYPE_SELL, lotSize, price2, price2 + stopLoss * _Point, price2 - takeProfit * _Point, "Arbitraje");
            ticket1=class="num">0;
            ticket2=class="num">0;
          }
        else
          if(price2 < shortEntry && (ticket1 == class="num">0 || ticket2 == class="num">0))
            {
              Print("Preparando para abrir órdenes");
              Order = class="num">2;
              Print("Error al abrir posiciones de arbitraje: ", GetLastError());
              ticket1 = ExtTrade.PositionOpen(symbol1, ORDER_TYPE_SELL, lotSize, price1, price1 + stopLoss * _Point, price1 - takeProfit * _Point, "Arbitraje");
              ticket2 = ExtTrade.PositionOpen(symbol2, ORDER_TYPE_BUY, lotSize, price2, price2 - stopLoss * _Point, price2 + takeProfit * _Point, "Arbitraje");
              ticket1=class="num">0;
              ticket2=class="num">0;
            }

价差突破后的双向开仓分支

这段逻辑处理的是统计套利里「价差偏离均值超过阈值」之后的下单动作。当 price1 大于 longEntry 且两个订单号至少有一个为 0 时,程序先把 Order 标记为 3,随后以 symbol1 卖、symbol2 买的方式建立对冲仓,止损止盈都按 stopLoss、takeProfit 乘 _Point 换算到点值。 另一侧若 price2 大于 longEntry 且ticket未占满,则走 Order=4 的镜像分支:symbol1 买、symbol2 卖,方向完全反过来。两个分支在开仓后都把 ticket1、ticket2 重置为 0,意味着下一根 K 线若条件再触发会重新尝试建仓,而不是补单。 真正决定「偏哪边开」的是后面这三行:meanSpread 取两个预测值均值,stdDevSpread2 用全样本标准差,shortEntry 直接钉在 meanSpread 减 2 倍标准差。也就是说,只有价差掉到均值下方两个标准差才考虑空价差出场,实战里这属于典型的 95% 置信带外触发,外汇与贵金属价差受隔夜利息干扰大,此类阈值在高波动时段可能频繁假突破,属高风险操作。

MQL5 / C++
      }
      else
         if(price1 > longEntry && (ticket1 == class="num">0 || ticket2 == class="num">0))
          {
            Print("Preparando para abrir órdenes");
            Order = class="num">3;
            Print("Error al abrir posiciones de arbitraje: ", GetLastError());
            ticket1 = ExtTrade.PositionOpen(symbol1, ORDER_TYPE_SELL, lotSize, price1, price1 + stopLoss * _Point, price1 - takeProfit * _Point, "Arbitraje");
            ticket2 = ExtTrade.PositionOpen(symbol2, ORDER_TYPE_BUY, lotSize, price2, price2 - stopLoss * _Point, price2 + takeProfit * _Point, "Arbitraje");
            ticket1=class="num">0;
            ticket2=class="num">0;
          }
         else
            if(price2 > longEntry && (ticket1 == class="num">0 || ticket2 == class="num">0))
             {
               Print("Preparando para abrir órdenes");
               Order = class="num">4;
               Print("Error al abrir posiciones de arbitraje: ", GetLastError());
               ticket1 = ExtTrade.PositionOpen(symbol1, ORDER_TYPE_BUY, lotSize, price1, price1 - stopLoss * _Point, price1 + takeProfit * _Point, "Arbitraje");
               ticket2 = ExtTrade.PositionOpen(symbol2, ORDER_TYPE_SELL, lotSize, price2, price2 + stopLoss * _Point, price2 - takeProfit * _Point, "Arbitraje");
               ticket1=class="num">0;
               ticket2=class="num">0;
             }
      }
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
   class="type">class="kw">double meanSpread = (lastPredicted1 + lastPredicted2) / class="num">2.0;
class=class="str">"cmt">//Print("mean spread: ",meanSpread);
   class="type">class="kw">double stdDevSpread2 = CalculateStdDev(spreads, class="num">0, ArraySize(spreads));
class=class="str">"cmt">//Print("StdDevSpread ", stdDevSpread);
   class="type">class="kw">double shortEntry = meanSpread - class="num">2 * stdDevSpread2 ;
   class="type">class="kw">double shortExit = meanSpread;

◍ 价差回归与标准差计算的底层的平仓逻辑

配对套利里,价差偏离均值 2 个标准差常被当作触发线。下面这行把多头入场阈值钉在 meanSpread + 2 * stdDevSpread2,回中到 meanSpread 就视为离场信号: double longEntry = meanSpread + 2 * stdDevSpread2 ; double longExit = meanSpread; 平仓判断把四种持仓组合都收口到同一次清仓:symbol2 价格跌破 longExit 且持 ticket2(Order==4)、symbol1 升破 shortExit 且持 ticket1(Order==1/3)、symbol2 升破 shortExit 且持 ticket1(Order==2)。任一满足就双向 PositionClose 并把 ticket 归零,日志打“Arbitraje detenido”。 标准差用样本方差开方,CalculateStdDev 对 data[start..start+count] 累加与平方和,variance = sumSq/count - mean^2,返回 MathSqrt(variance)。实盘里若 count 偏小(如 < 30),stdDev 估计会抖,触发线可能虚高。 ArePositionsOpen 只认 symbol1/symbol2 上 volume>0 的持仓,用来挡掉重复开仓;PredictPrice 则先做归一化保护(ExtMin>=ExtMax 直接 return),再用 CopyRates 取日线收盘价填 x_norm。外汇与贵金属价差策略滑点大,回测达标不等于实盘能成交,属高风险。

MQL5 / C++
class="type">class="kw">double longEntry = meanSpread + class="num">2 * stdDevSpread2 ;
class="type">class="kw">double longExit = meanSpread;
if((price2 < longExit && ticket2 != class="num">0 && Order==class="num">4) || (price1 > shortExit && ticket1 != class="num">0 && Order==class="num">1) || (price2 > shortExit && ticket1 != class="num">0 && Order==class="num">2) || (price1 < longExit && ticket2 != class="num">0 && Order==class="num">3))
  {
    ExtTrade.PositionClose(ticket1);
    ExtTrade.PositionClose(ticket2);
    ticket1 = class="num">0;
    ticket2 = class="num">0;
    Print("Arbitraje detenido - Cerrando órdenes");
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CalculateStdDev(class="type">class="kw">double &data[], class="type">int start, class="type">int count)
  {
  class="type">class="kw">double sum = class="num">0;
  class="type">class="kw">double sumSq = class="num">0;
  for(class="type">int i = start; i < start + count; i++)
    {
      sum += data[i];
      sumSq += data[i] * data[i];
    }
  class="type">class="kw">double mean = sum / count;
  class="type">class="kw">double variance = (sumSq / count) - (mean * mean);
  class="kw">return MathSqrt(variance);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool ArePositionsOpen()
  {
class=class="str">"cmt">// Check for positions on symbol1
  if(PositionSelect(symbol1) && PositionGetDouble(POSITION_VOLUME) > class="num">0)
      class="kw">return true;
class=class="str">"cmt">// Check for positions on symbol2
  if(PositionSelect(symbol2) && PositionGetDouble(POSITION_VOLUME) > class="num">0)
      class="kw">return true;
  class="kw">return class="kw">false;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void PredictPrice(class="type">void)
  {
  class="kw">static vectorf output_data(class="num">1);            class=class="str">"cmt">// vector to get result
  class="kw">static vectorf x_norm(SAMPLE_SIZE);       class=class="str">"cmt">// vector for prices normalize
class=class="str">"cmt">//--- check for normalization possibility
  if(ExtMin>=ExtMax)
    {
      Print("ExtMin>=ExtMax");
      class=class="str">"cmt">//ExtPredictedClass=-class="num">1;
      class="kw">return;
    }
class=class="str">"cmt">//--- request last bars
  if(!x_norm.CopyRates(_Symbol,PERIOD_D1,COPY_RATES_CLOSE,class="num">1,SAMPLE_SIZE))
    {
      Print("CopyRates ",x_norm.Size());

「双模型并行推理的价格归一化实现」

这段逻辑里出现了两个对称的预测函数 PredictPrice 与 PredictPrice2,分别针对主品种和第二个品种(symbol2)做 ONNX 推理。两者都依赖 SAMPLE_SIZE 个日线收盘价,代码里 GetMinMax 与 GetMinMax2 用 CopyRates 拉取 PERIOD_D1 的最近 120 根 K 线(SAMPLE_SIZE=120),取收盘价的极小值和极大值做归一化边界。 归一化步骤很直接:x_norm 减去 ExtMin 再除以 (ExtMax-ExtMin),把价格压到 0~1 区间喂给 OnnxRun。推理失败后 Print 报错并直接 return,不会向下写预测值;成功则从 output_data[0] 反归一化:predicted = output_data[0]*(ExtMax-ExtMin)+ExtMin,还原成真实价格量纲。 第二个品种完全复刻该流程,但独立维护 ExtMin2/ExtMax2 与 ExtHandle2。若 ExtMin2>=ExtMax2 会先拦截并打印,避免除零。外汇与贵金属日线波动大,这种 120 日极值归一化在趋势突变周可能压缩近期信号,调参时建议先打印 last_close 与边界值核对。 开 MT5 把这两段挂到 EA 里,分别指定 symbol2 为 XAUUSD 与 EURUSD,跑一周观察 predicted 与盘面收盘的偏离,能快速验证模型输出是否脱钩。

MQL5 / C++
  class=class="str">"cmt">//ExtPredictedClass=-class="num">1;
  class="kw">return;
   }
  class="type">class="kw">float last_close=x_norm[SAMPLE_SIZE-class="num">1];
class=class="str">"cmt">//--- normalize prices
  x_norm-=ExtMin;
  x_norm/=(ExtMax-ExtMin);
class=class="str">"cmt">//--- run the inference
  if(!OnnxRun(ExtHandle,ONNX_NO_CONVERSION,x_norm,output_data))
   {
     Print("OnnxRun");
     class=class="str">"cmt">//ExtPredictedClass=-class="num">1;
     class="kw">return;
   }
class=class="str">"cmt">//--- denormalize the price from the output value
  predicted=output_data[class="num">0]*(ExtMax-ExtMin)+ExtMin;
class=class="str">"cmt">//class="kw">return predicted;
 }
class="type">void PredictPrice2(class="type">void)
  {
  class="kw">static vectorf output_data2(class="num">1);      class=class="str">"cmt">// vector to get result
  class="kw">static vectorf x_norm2(SAMPLE_SIZE);     class=class="str">"cmt">// vector for prices normalize
class=class="str">"cmt">//--- check for normalization possibility
  if(ExtMin2>=ExtMax2)
   {
     Print("ExtMin2>=ExtMax2");
     class=class="str">"cmt">//ExtPredictedClass=-class="num">1;
     class="kw">return;
   }
class=class="str">"cmt">//--- request last bars
  if(!x_norm2.CopyRates(symbol2,PERIOD_D1,COPY_RATES_CLOSE,class="num">1,SAMPLE_SIZE))
   {
     Print("CopyRates ",x_norm2.Size());
     class=class="str">"cmt">//ExtPredictedClass=-class="num">1;
     class="kw">return;
   }
  class="type">class="kw">float last_close2=x_norm2[SAMPLE_SIZE-class="num">1];
class=class="str">"cmt">//--- normalize prices
  x_norm2-=ExtMin2;
  x_norm2/=(ExtMax2-ExtMin2);
class=class="str">"cmt">//--- run the inference
  if(!OnnxRun(ExtHandle2,ONNX_NO_CONVERSION,x_norm2,output_data2))
   {
     Print("OnnxRun");
     class=class="str">"cmt">//ExtPredictedClass=-class="num">1;
     class="kw">return;
   }
class=class="str">"cmt">//--- denormalize the price from the output value
  predicted2=output_data2[class="num">0]*(ExtMax2-ExtMin2)+ExtMin2;
class=class="str">"cmt">//--- classify predicted price movement
class=class="str">"cmt">//class="kw">return predicted2;
 }
class="type">void GetMinMax(class="type">void)
  {
  vectorf close;
  close.CopyRates(_Symbol,PERIOD_D1,COPY_RATES_CLOSE,class="num">0,SAMPLE_SIZE);
  ExtMin=close.Min();
  ExtMax=close.Max();
 }
class="type">void GetMinMax2(class="type">void)
  {
  vectorf close2;

用 D1 收盘价给双品种定极值

在跨品种相关性扫描里,第二只标的的日线收盘序列要先抓满 SAMPLE_SIZE 根,再用 CopyRates 拉 PERIOD_D1 的 COPY_RATES_CLOSE 字段,避免用盘中价污染极值。 ExtMin2 与 ExtMax2 直接取该序列的 Min() 和 Max(),这两个值后续用来做归一化或阈值判定。若 SAMPLE_SIZE 设得太小,极值会被近期噪声主导,跨品种比对会失真。 下面这段 Python 侧先把符号池收窄到 EUR/USD 相关组:凡是以 EUR 开头、USD 开头,或以 USD 结尾的品种才保留,降低无关交叉盘的干扰。外汇与贵金属杠杆高,这类筛选只解决样本相关性,不预示任何方向。

MQL5 / C++
  close2.CopyRates(symbol2,PERIOD_D1,COPY_RATES_CLOSE,class="num">0,SAMPLE_SIZE);
  ExtMin2=close2.Min();
  ExtMax2=close2.Max();
  }
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;)]

◍ 拿纳斯达克配对跑一遍协整筛选

股票领域的统计套利思路搬到外汇/贵金属之外也成立,我用纳斯达克对冲组合做了第二个例子。原脚本里只圈定 EUR/USD 系品种,我改成了从 MT5 全部品种里捞出路径含 NASDAQ 且带 # 号、不含小数点的 CFD 符号,再把协整 p 值小于 0.05 的配对写进 CSV。 脚本跑完打印出『强相关且协整的配对数』,实际筛完我选了 Amazon(#AMZN) 与 Netflix(#NFLX) 这一对,二者相关系数 0.9666、协整 p 值 0.0217,样本内显著。 预测区间取 2023-01-01 到 2024-01-01,为提升拟合我把样本量扩到 120*25*3 = 9000 根 bar,覆盖一年以上 D1 数据。初版止损 400、止盈 800,后经快速优化微调参数,回测指标里出现 6.86、47.01、0.9395、25.98、674.74、0.9736 等数值,可作为你重跑时的对照基准。 外汇/贵金属及股指 CFD 均属高风险品种,这套流程只是研究示例而非即插即用 EA。你要复现就得自己改 .py 里的日期重新训 ONNX,并在策略测试器同步换日期,我建议每周或每月重训一次模型而非长期不换。

MQL5 / C++
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;)]
# Crea un DataFrame con la información completa de los símbolos
symbols_df = pd.DataFrame([{&class="macro">#x27;Symbol&class="macro">#x27;: symbol.name, &class="macro">#x27;Path&class="macro">#x27;: symbol.path} for symbol in all_symbols])
# Filtra adicionalmente para obtener solo los CFDs de NASDAQ
# Asumiendo que los CFDs tienen un identificador único en el &class="macro">#x27;Path&class="macro">#x27;
nasdaq_group4_df = symbols_df[symbols_df[&class="macro">#x27;Path&class="macro">#x27;].str.contains(&class="macro">#x27;NASDAQ&class="macro">#x27;)]
# Filtra aún más para obtener solo los símbolos que NO contienen &class="macro">#x27;.&class="macro">#x27;
nasdaq_group4_df3 = nasdaq_group4_df[nasdaq_group4_df[&class="macro">#x27;Symbol&class="macro">#x27;].str.contains(&class="macro">#x27;#&class="macro">#x27;)]
nasdaq_group4_df2 = nasdaq_group4_df3[~nasdaq_group4_df3[&class="macro">#x27;Symbol&class="macro">#x27;].str.contains(&class="macro">#x27;\.&class="macro">#x27;)]
# Ahora, obtenemos la lista de símbolos filtrados
filtered_symbols = nasdaq_group4_df2[&class="macro">#x27;Symbol&class="macro">#x27;].tolist()
# Descargar datos históricos y almacenar en un diccionario
symbols = filtered_symbols
# Filtrar y guardar solo los pares cointegrados con p-valor menor de class="num">0.05 en un archivo CSV
result_df = pd.DataFrame(cointegrated_pairs, columns=[&class="macro">#x27;Symbol1&class="macro">#x27;, &class="macro">#x27;Symbol2&class="macro">#x27;, &class="macro">#x27;Correlation&class="macro">#x27;, &class="macro">#x27;Cointegration P-value&class="macro">#x27;])
result_df.to_csv(&class="macro">#x27;cointegrated_pairs.csv&class="macro">#x27;, index=False)
# Imprimir el total de pares cointegrados
print(f&class="macro">#x27;Total de pares con fuerte correlación y cointegrados: {len(cointegrated_pairs)}&class="macro">#x27;)
class="macro">#AMZN   class="macro">#NFLX   class="num">0.966605859     class="num">0.021683012
sample_size = class="num">120*class="num">25*class="num">3
class="num">6.856399020501732
class="num">47.010207528337105
class="num">0.9395402850007741 
class="num">25.975755379462548
class="num">674.7398675336775
class="num">0.9735838717570285

「收束」

把皮尔逊系数指标和 ONNX 预测型套利 EA 跑通后,真正拉开差距的往往不是模型本身,而是配对筛选。用文末 .py 筛选器吐出的 cointegrated_pairs.csv(约 220 KB,含多组协整对)去喂 EA,比盲目上全部品种更稳。 实盘前建议在 MT5 里手动拧一遍 SL/TP:原文提到微调止损止盈能改善结果,但外汇与贵金属杠杆高、滑点跳空频繁,任何回测优值都只是概率倾向,不等于实盘必然。 文件落位别搞错——ONNX 模型进 MQL5/Files,mq5 指标进 Indicators,EA 进 Experts,否则 EA 加载会直接报空引用。写完这套流程,剩下就是你自己开 MT5 验证哪组配对在你经纪商点差下还能活。

把重复劳动交给小布
成对资产的皮尔逊系数与协整状态监控,小布盯盘的 AIGC 已内置,打开对应品种页即可看到实时偏离带,你只需判断触发条件是噪声还是结构断裂。

常见问题

相关系数只说明线性同向或反向强度,协整才约束长期均衡。实战中先筛协整对,再用相关系数确认短期触发窗口,避免被伪相关误导。
点差与成交延迟会吃掉均值回归利润,建议在测试器里手动注入滑点,并分段测试不同波动率 regime,否则样本内漂亮样本外崩。
协整基于平稳均衡假设,宏观冲击会改变资产底层驱动,绳子断裂后两游泳者各游各的,此时模型给出的偏离信号只是陷阱。
小布在品种页叠加偏离带与半衰期估计,把重复劳动交给小布,你专注决策;当偏离带宽度突破历史分位时提示结构风险而非普通噪声。
半衰期长的对子容忍度更高,止损可放宽到 2–3 倍常规 ATR;半衰期短则必须紧止损,因为回归快但反转也猛,外汇贵金属高风险需严控仓位。