MT4 to MT5: four migration bugs that survive a clean recompile

MT4 to MT5: four migration bugs that survive a clean recompile

11 June 2026, 22:14
Boris Armenteros
0
27
A clean recompile is where an MT4-to-MT5 migration starts, not where it ends. The functions rename, the errors clear, the Strategy Tester runs — and four architecture differences sit underneath, none of which throws an error. These are the four I keep finding in migrated EAs, with the patterns that cause them. They share one property: the tester won't show them, because the failure is an absence or a silent wrong value, not an error.

The order pool split

In MT4, `OrdersTotal()` counted open positions and pending orders in one pool, and you walked it with `OrderSelect()`. In MT5 that pool is split: positions live in `PositionsTotal()`, pending orders in `OrdersTotal()` (now pending-only), and closed deals in history. The function name survives the recompile, so a management loop ported verbatim looks correct and quietly manages nothing.

// MT4 habit — in MT5 this iterates pending orders only:
for(int i = OrdersTotal() - 1; i >= 0; i--) { /* trail stops... */ }

// MT5 — open trades live here:
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
   ulong ticket = PositionGetTicket(i);
   // trail / break-even on the actual position
}

On an account with no pending orders, the first loop runs zero iterations. Trailing stops and break-even never fire, and the position sits fully exposed.

Array series direction

MT4 price arrays were series-indexed by default: index 0 was the current bar. In MT5 you copy into your own array with `CopyRates()`, `CopyClose()`, or `CopyBuffer()`, and those arrays are **not** series by default — index 0 is the oldest copied bar unless you say otherwise.

double buf[];
CopyBuffer(maHandle, 0, 0, 3, buf);
ArraySetAsSeries(buf, true);   // without this, buf[1] is NOT the previous bar
Skip `ArraySetAsSeries()` and `buf[1]` points at the far historical end of the copied range. The EA computes crossovers on bars from days ago; on the chart the entries look random, but the backtest applies the same wrong-end read uniformly, so the curve only looks mediocre.

Lot-size precision

Money-based sizing reads the balance and the broker's volume constraints. MT4 used `AccountBalance()` and `MarketInfo(Symbol(), MODE_LOTSTEP)`; MT5 deprecates both in favor of `AccountInfoDouble()` and `SymbolInfoDouble()`. The compatibility wrappers still compile, which is exactly why the drift slips through.

double rawLots = 0.13;   // risk-based size from your sizing logic, before broker rounding
double step    = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
double lots    = MathFloor(rawLots / step) * step;
lots = MathMax(lots, SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN));
lots = MathMin(lots, SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX));
Read the step natively and normalize to it. On a small account, where one volume step is a meaningful slice of the position, the wrong normalization turns a configured 1% risk into something else — with no error to flag it.

State rebuild after a restart

A restart — a VPS reboot, a terminal update, a relaunch — runs `OnDeinit()` then `OnInit()` and wipes `static` and global in-memory state. That is true in both platforms, so robust EAs rebuild state in `OnInit()`. The migration-specific trap is *how* the rebuild reads the account: an MT4 reconstruction that scans `OrdersTotal()` finds the open positions; ported to MT5, it scans the pending-only pool, sees nothing, and concludes the account is flat.

int OnInit()
{
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      // re-attach magic number, volume, grid level...
   }
   return INIT_SUCCEEDED;
}
Rebuild from `PositionsTotal()`, not `OrdersTotal()`. Otherwise the EA opens a fresh grid on top of positions it already holds — doubled exposure from a routine reboot.

A second-pass audit

None of these four fires at order time, so the usual order-placement checks miss them. The pass that catches them runs after the EA compiles and before it touches a funded account: confirm management loops iterate `PositionsTotal()`; confirm every copied array sets its direction; re-derive one lot size by hand against `SYMBOL_VOLUME_STEP`; and restart the terminal with positions open to confirm the EA comes back aware of them. The Strategy Tester validates your logic against itself — it can't validate it against a live MT5 terminal that iterates trades, indexes arrays, and rebuilds state differently.