MT4 to MT5: four migration bugs that survive a clean recompile
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.
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.
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) ; MQL5 has neither, and reads the same values through AccountInfoDouble() and SymbolInfoDouble() . A straight recompile stops on the old names as undeclared identifiers; once they are replaced and the code compiles again, nothing checks that the sizing still rounds to the broker's step, which is exactly why the drift slips through.
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.
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.
Correction, 12 October 2026: an earlier version said MT5 keeps the MQL4 function names alive as compatibility wrappers that still compile. MQL5 has no MarketInfo(), MODE_ constants or AccountBalance(), and a straight recompile reports them as undeclared identifiers. The lot-size paragraph now says so.


