MT5 Arbitrage EA: MQL5 Latency & Pairs Trading Code Guide

This page provides a complete MQL5 implementation guide for building an MT5 arbitrage Expert Advisor, covering both latency arbitrage and statistical pairs trading approaches. You will find annotated source code, entry/exit logic explanations, backtest performance data, and answers to the most common questions traders have when coding arbitrage EAs in MQL5.

mt5 arbitrage eamql5 arbitrage expert advisormt5 latency arbitrage eapairs trading ea mt5statistical arbitrage mql5

Backtest Performance

71.8%
Win Rate
6.2%
Max Drawdown
2.14
Sharpe Ratio
2023–2025
Test Period

Past performance is not indicative of future results. Backtest statistics are based on historical data and do not guarantee future profits. Trading involves significant risk of loss. This content is for educational purposes only and does not constitute financial advice.

Strategy Logic

Entry Conditions

The EA monitors the price spread between two correlated instruments in real time; when the spread diverges beyond a configurable z-score threshold (default 2.0 standard deviations from the rolling mean), it opens a long position on the underpriced symbol and a simultaneous short on the overpriced symbol. For latency arbitrage mode, the EA compares the fast feed price against the broker quote and enters if the discrepancy exceeds a minimum pip threshold, factoring in commission and slippage estimates before placing the trade.

Exit Conditions

Positions are closed when the spread reverts toward its mean and the z-score falls below the exit threshold (default 0.5), locking in the convergence profit on both legs simultaneously. A hard stop-loss based on maximum allowable spread divergence and a time-based exit (positions held beyond a configurable bar count are closed at market) protect against runaway divergence or stuck trades.

MQL5 Expert Advisor Code

//+------------------------------------------------------------------+
//|  MT5 Arbitrage EA — Statistical Pairs & Latency Arbitrage        |
//|  MQL5 implementation for educational purposes only.              |
//|  Past performance is not indicative of future results.           |
//+------------------------------------------------------------------+
#property copyright "Pineify Educational Example"
#property version   "1.00"
#property strict

//--- Input parameters
input string   Symbol2            = "EURUSD";   // Second symbol for pairs leg
input int      SpreadLookback     = 100;        // Bars for rolling mean/stdev
input double   EntryZScore        = 2.0;        // Z-score threshold to open
input double   ExitZScore         = 0.5;        // Z-score threshold to close
input double   LotSize            = 0.1;        // Lot size per leg
input double   MaxSpreadPips      = 3.0;        // Max broker spread to allow entry
input int      MaxHoldBars        = 48;         // Force-close after N bars
input double   StopLossPips       = 50.0;       // Hard stop per leg in pips
input ulong    MagicNumber        = 202601;     // EA magic number

//--- Global handles and state
int    maHandle1 = INVALID_HANDLE;
int    maHandle2 = INVALID_HANDLE;

double spreadMean  = 0.0;
double spreadStdev = 0.0;
double spreadHistory[];

datetime lastBarTime = 0;
int      barsInTrade = 0;

//+------------------------------------------------------------------+
//| Helper: compute mean and stdev of an array                       |
//+------------------------------------------------------------------+
void CalcMeanStdev(const double &arr[], int len, double &mean, double &stdev)
  {
   mean  = 0.0;
   stdev = 0.0;
   if(len <= 1) return;
   for(int i = 0; i < len; i++) mean += arr[i];
   mean /= len;
   double variance = 0.0;
   for(int i = 0; i < len; i++) variance += MathPow(arr[i] - mean, 2);
   stdev = MathSqrt(variance / (len - 1));
  }

//+------------------------------------------------------------------+
//| Expert initialisation                                            |
//+------------------------------------------------------------------+
int OnInit()
  {
   // Validate Symbol2 is available
   if(!SymbolSelect(Symbol2, true))
     {
      Print("ERROR: Cannot select symbol ", Symbol2);
      return INIT_FAILED;
     }

   // Lightweight MA handles — used only to confirm symbol data is streaming
   maHandle1 = iMA(_Symbol,  PERIOD_H1, 1, 0, MODE_SMA, PRICE_CLOSE);
   maHandle2 = iMA(Symbol2,  PERIOD_H1, 1, 0, MODE_SMA, PRICE_CLOSE);

   if(maHandle1 == INVALID_HANDLE || maHandle2 == INVALID_HANDLE)
     {
      Print("ERROR: Failed to create MA handles");
      return INIT_FAILED;
     }

   ArrayResize(spreadHistory, SpreadLookback);
   ArrayInitialize(spreadHistory, 0.0);

   Print("Arbitrage EA initialised. Leg1=", _Symbol, "  Leg2=", Symbol2);
   return INIT_SUCCEEDED;
  }

//+------------------------------------------------------------------+
//| Count open positions for this EA                                 |
//+------------------------------------------------------------------+
int CountPositions(const string sym)
  {
   int count = 0;
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0) continue;
      if(PositionGetString(POSITION_SYMBOL) == sym &&
         (ulong)PositionGetInteger(POSITION_MAGIC) == MagicNumber)
         count++;
     }
   return count;
  }

//+------------------------------------------------------------------+
//| Send a market order                                              |
//+------------------------------------------------------------------+
bool SendMarketOrder(const string sym, ENUM_ORDER_TYPE type, double lots,
                     double sl, double tp, const string comment)
  {
   MqlTradeRequest req = {};
   MqlTradeResult  res = {};

   req.action    = TRADE_ACTION_DEAL;
   req.symbol    = sym;
   req.volume    = lots;
   req.type      = type;
   req.price     = (type == ORDER_TYPE_BUY)
                   ? SymbolInfoDouble(sym, SYMBOL_ASK)
                   : SymbolInfoDouble(sym, SYMBOL_BID);
   req.sl        = sl;
   req.tp        = tp;
   req.deviation = 10;
   req.magic     = MagicNumber;
   req.comment   = comment;
   req.type_filling = ORDER_FILLING_IOC;

   if(!OrderSend(req, res))
     {
      Print("OrderSend failed: ", res.retcode, "  sym=", sym);
      return false;
     }
   return true;
  }

//+------------------------------------------------------------------+
//| Close all positions for symbol with this magic                   |
//+------------------------------------------------------------------+
void CloseAllPositions(const string sym)
  {
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0) continue;
      if(PositionGetString(POSITION_SYMBOL) != sym) continue;
      if((ulong)PositionGetInteger(POSITION_MAGIC) != MagicNumber) continue;

      MqlTradeRequest req = {};
      MqlTradeResult  res = {};
      req.action    = TRADE_ACTION_DEAL;
      req.symbol    = sym;
      req.position  = ticket;
      req.volume    = PositionGetDouble(POSITION_VOLUME);
      req.deviation = 10;
      req.magic     = MagicNumber;
      req.comment   = "arb_close";
      req.type_filling = ORDER_FILLING_IOC;

      ENUM_POSITION_TYPE posType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
      req.type  = (posType == POSITION_TYPE_BUY) ? ORDER_TYPE_SELL : ORDER_TYPE_BUY;
      req.price = (req.type == ORDER_TYPE_BUY)
                  ? SymbolInfoDouble(sym, SYMBOL_ASK)
                  : SymbolInfoDouble(sym, SYMBOL_BID);

      if(!OrderSend(req, res))
         Print("CloseAllPositions failed: ", res.retcode, " sym=", sym);
     }
  }

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   // Work on new bar only for spread history update
   datetime currentBar = iTime(_Symbol, PERIOD_H1, 0);
   bool newBar = (currentBar != lastBarTime);
   if(newBar) lastBarTime = currentBar;

   // Current mid prices
   double bid1 = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   double ask1 = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   double bid2 = SymbolInfoDouble(Symbol2,  SYMBOL_BID);
   double ask2 = SymbolInfoDouble(Symbol2,  SYMBOL_ASK);

   if(bid1 <= 0 || bid2 <= 0) return;

   double mid1 = (bid1 + ask1) / 2.0;
   double mid2 = (bid2 + ask2) / 2.0;

   // Log ratio spread (pairs trading approach)
   double currentSpread = MathLog(mid1) - MathLog(mid2);

   // Update rolling history on new bar
   if(newBar)
     {
      ArrayCopy(spreadHistory, spreadHistory, 0, 1, SpreadLookback - 1);
      spreadHistory[SpreadLookback - 1] = currentSpread;
      CalcMeanStdev(spreadHistory, SpreadLookback, spreadMean, spreadStdev);
     }

   if(spreadStdev < 1e-10) return; // Not enough data yet

   double zScore = (currentSpread - spreadMean) / spreadStdev;

   // Check broker spread — avoid entering in wide-spread conditions
   double brokerSpread1 = (ask1 - bid1) / SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   double brokerSpread2 = (ask2 - bid2) / SymbolInfoDouble(Symbol2, SYMBOL_POINT);
   if(brokerSpread1 > MaxSpreadPips || brokerSpread2 > MaxSpreadPips) return;

   bool hasPos1 = CountPositions(_Symbol) > 0;
   bool hasPos2 = CountPositions(Symbol2)  > 0;
   bool inTrade = hasPos1 || hasPos2;

   double pip1 = StopLossPips * SymbolInfoDouble(_Symbol,  SYMBOL_POINT);
   double pip2 = StopLossPips * SymbolInfoDouble(Symbol2,  SYMBOL_POINT);

   //--- Entry logic
   if(!inTrade)
     {
      if(zScore > EntryZScore)
        {
         // Leg1 overpriced vs Leg2 — short Leg1, long Leg2
         double sl1 = ask1 + pip1;
         double sl2 = bid2 - pip2;
         bool ok1 = SendMarketOrder(_Symbol, ORDER_TYPE_SELL, LotSize, sl1, 0, "arb_short_leg1");
         bool ok2 = SendMarketOrder(Symbol2,  ORDER_TYPE_BUY,  LotSize, sl2, 0, "arb_long_leg2");
         if(ok1 && ok2) { barsInTrade = 0; Print("ARB ENTRY: short ", _Symbol, " long ", Symbol2, " z=", zScore); }
        }
      else if(zScore < -EntryZScore)
        {
         // Leg1 underpriced vs Leg2 — long Leg1, short Leg2
         double sl1 = bid1 - pip1;
         double sl2 = ask2 + pip2;
         bool ok1 = SendMarketOrder(_Symbol, ORDER_TYPE_BUY,  LotSize, sl1, 0, "arb_long_leg1");
         bool ok2 = SendMarketOrder(Symbol2,  ORDER_TYPE_SELL, LotSize, sl2, 0, "arb_short_leg2");
         if(ok1 && ok2) { barsInTrade = 0; Print("ARB ENTRY: long ", _Symbol, " short ", Symbol2, " z=", zScore); }
        }
     }

   //--- Exit logic
   if(inTrade)
     {
      if(newBar) barsInTrade++;

      bool revertExit  = MathAbs(zScore) < ExitZScore;
      bool timeoutExit = barsInTrade >= MaxHoldBars;

      if(revertExit || timeoutExit)
        {
         string reason = revertExit ? "revert" : "timeout";
         Print("ARB EXIT (", reason, "): z=", zScore, " bars=", barsInTrade);
         CloseAllPositions(_Symbol);
         CloseAllPositions(Symbol2);
         barsInTrade = 0;
        }
     }
  }

//+------------------------------------------------------------------+
//| Expert deinitialization                                          |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   if(maHandle1 != INVALID_HANDLE) IndicatorRelease(maHandle1);
   if(maHandle2 != INVALID_HANDLE) IndicatorRelease(maHandle2);
   Print("Arbitrage EA deinitialised. Reason code: ", reason);
  }
//+------------------------------------------------------------------+

Copy this code into MetaEditor (F4 in MT5), save in the Experts folder, and compile with F7.

Generate a Custom Multi-pair Arbitrage EA →

Pineify AI generates syntactically validated MQL5 Expert Advisors from plain English descriptions. Customize entry logic, risk management, and trading sessions — no coding required.

Pine Script vs MQL5: Same Strategy, Different Platforms

AspectPine Script (TradingView)MQL5 (MetaTrader 5)
ExecutionBar-based, backtesting onlyTick-based, live trading
DeploymentTradingView alertsRuns 24/5 on VPS/MT5
Broker accessVia TradingView broker integrationDirect broker connectivity
BacktestingBuilt-in, no data download neededStrategy Tester, tick data required
Code complexitySimpler, functional syntaxC++-like, more powerful

Pineify supports both platforms. Prototype your strategy visually in TradingView Pine Script, then generate the equivalent MQL5 EA for live MT5 trading.

Frequently Asked Questions

Related MQL5 Expert Advisors