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.
Backtest Performance
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
| Aspect | Pine Script (TradingView) | MQL5 (MetaTrader 5) |
|---|---|---|
| Execution | Bar-based, backtesting only | Tick-based, live trading |
| Deployment | TradingView alerts | Runs 24/5 on VPS/MT5 |
| Broker access | Via TradingView broker integration | Direct broker connectivity |
| Backtesting | Built-in, no data download needed | Strategy Tester, tick data required |
| Code complexity | Simpler, functional syntax | C++-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.