MQL4 vs MQL5: the 7 differences that break your ported EA
Porting an MQL4 expert advisor to MQL5 is rarely a find-and-replace job. The two languages look similar enough to lull you into a false sense of security, then the compiler throws forty errors and, once it finally builds, the EA trades nothing or closes positions it never opened. Here are the seven differences that do the damage.
The short version
| MQL4 | MQL5 |
|---|---|
| OrdersTotal / OrderSelect | PositionsTotal / PositionGetTicket, plus orders for pending |
| OrderSend(...) | OrderSend(request, result) with MqlTradeRequest |
| OrderClose / OrderModify | TRADE_ACTION_DEAL / TRADE_ACTION_SLTP |
| iMA(...) returns a value | iMA(...) returns a handle, then CopyBuffer |
| Point, Digits, GetLastError | _Point, _Digits, _LastError |
| MarketInfo(Symbol(), MODE_X) | SymbolInfoDouble(_Symbol, SYMBOL_X) |
| start() | OnTick() |
1. Orders, positions and deals are three things now
In MQL4 an order is an order, whether it is pending or filled. In MQL5 the lifecycle is split: a *pending order* becomes a *deal* when executed, and the deal opens or closes a *position*. OrdersTotal() in MQL5 returns pending orders only. If your ported EA counts positions with it, it will always see zero.
The loop you actually want looks like this:
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
if(PositionGetInteger(POSITION_MAGIC) != InpMagic) continue;
// this position is yours
}
Note that PositionGetTicket also selects the position, so the subsequent getters refer to it. And on a netting account, multiple buys on the same symbol merge into one position, which breaks any MQL4 logic built on "one ticket per trade".
2. OrderSend is a different function
MQL4:
int ticket = OrderSend(_Symbol, OP_BUY, 0.10, Ask, 3, 0, 0, "first", 12345, 0, clrBlue);
if(ticket < 0) Print("failed: ", GetLastError());
MQL5:
#include <Trade\Trade.mqh>
CTrade trade;
trade.SetExpertMagicNumber(12345);
if(!trade.Buy(0.10, _Symbol, 0.0, 0.0, "first"))
Print("failed: ", trade.ResultRetcode(), " ", trade.ResultRetcodeDescription());
You can build MqlTradeRequest by hand if you prefer, but CTrade exists precisely so you do not have to. The important change is that failure is reported through retcode, not just the last error, and a true return still does not guarantee the deal was filled at your price.
3. Indicators hand you a handle, not a value
This one catches everybody. iMA in MQL5 does not return a moving average. It returns a handle you must request data from, and the handle should be created once in OnInit.
int maHandle = INVALID_HANDLE;
int OnInit()
{
maHandle = iMA(_Symbol, PERIOD_H1, 50, 0, MODE_EMA, PRICE_CLOSE);
return(INIT_SUCCEEDED);
}
double Ma(const int shift)
{
double buf[];
if(CopyBuffer(maHandle, 0, shift, 1, buf) != 1)
return(0.0);
return(buf[0]);
}
Calling iMA on every tick, as MQL4 code often does, leaks handles until the terminal refuses to create more. Move them all into OnInit and add one CopyBuffer per value you need.
4. There are no predefined price arrays
Close[], Open[], High[] do not exist in MQL5 as series arrays. The iClose, iHigh and friends are still available, but they are slower than copying data into your own buffer. For anything that reads several bars per tick, copy once:
double closes[];
if(CopyClose(_Symbol, PERIOD_H1, 0, 100, closes) != 100)
return;
// closes[0] is the oldest of the 100, closes[99] the current bar
Watch the ordering. CopyClose fills the array oldest-first by default, which is the reverse of the way most ported MQL4 loops are written.
5. Closing and modifying go through the trade request
There is no OrderClose or OrderModify. Both become OrderSend calls with a different action. Modifying stops:
MqlTradeRequest req;
MqlTradeResult res;
ZeroMemory(req);
req.action = TRADE_ACTION_SLTP;
req.position = PositionGetInteger(POSITION_TICKET);
req.sl = sl;
req.tp = tp;
if(!OrderSend(req, res))
Print("modify failed: ", res.retcode);
Closing is TRADE_ACTION_DEAL with the opposite ORDER_TYPE and DEAL_ENTRY_OUT. If you are using CTrade, PositionClose and PositionModify cover both.
6. Closed trades live in history, and you must select it
MQL4 let you select a closed order from the terminal's order pool. MQL5 keeps deals in a separate history that is not loaded until you ask. Summing today's profit:
if(!HistorySelect(0, TimeCurrent()))
return;
double profit = 0.0;
int total = HistoryDealsTotal();
for(int i = 0; i < total; i++)
{
ulong ticket = HistoryDealGetTicket(i);
if(HistoryDealGetInteger(ticket, DEAL_MAGIC) != InpMagic) continue;
profit += HistoryDealGetDouble(ticket, DEAL_PROFIT)
+ HistoryDealGetDouble(ticket, DEAL_SWAP)
+ HistoryDealGetDouble(ticket, DEAL_COMMISSION);
}
Remember that commission and swap are separate fields here and that an open-and-close pair means two deals, not one.
7. The small things that fail silently
MarketInfo(Symbol(), MODE_TICKVALUE) becomes SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE). The trap is that an invalid property in MQL5 returns 0.0 with no error set, so a lot-size calculation quietly divides by zero. Point becomes _Point, Digits becomes _Digits, GetLastError() becomes _LastError, and #property strict should simply be deleted. None of these produce a useful compiler message if you get them subtly wrong.
Test again after you port
Even after a clean port, do not trust the old report. MQL5's tester models ticks, spread variation and execution differently, so an identical strategy can produce a different equity curve. Re-run it and read the new output carefully — [see a real backtest](https://mql.ranartech.com/r/YNQBPo_4yZ5S) — EURUSD H1 EA (+$1,880, 68% win, 110 trades) — as an example of the level of detail worth checking: trade count, win rate and net profit together, not just the headline number. A port that compiles is not the same as a port that behaves like the original.
Build & backtest your own EA free →