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) ; MT5 deprecates both in favor of AccountInfoDouble() and SymbolInfoDouble() . The compatibility wrappers still compile, 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.


