Complete .mq5 source code

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.

Historical test results

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, save it in the MQL5/Experts folder, and compile with F7.

Generate a custom Multi-pair arbitrage EA

Describe the ATR inputs, alert rules, visual style, or confirmation filter you want. Pineify generates editable MQL5 source code that you can inspect and compile in MetaEditor.

Pine Script vs MQL5: Same Strategy, Different Platforms

AspectPine Script (TradingView)MQL5 (MetaTrader 5)
ExecutionSeries evaluated on chart updatesEvent handlers with explicit buffers
DeploymentRuns in TradingView with alertsRuns in the MT5 terminal or on a VPS
Broker accessVia TradingView broker integrationDirect broker connectivity
BacktestingStrategy Tester for strategy scriptsStrategy Tester for Expert Advisors
Code complexitySimpler, functional syntaxC++-like, more powerful

The calculation can be implemented on either platform, but the runtime model and buffer APIs differ. Read the Pine Script Supertrend reference before porting logic between them.

Frequently Asked Questions

Related MQL5 pages

Risk and testing note

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.