Part 1 covered the three MT4-to-MT5 failures that show up the instant your EA places an order. This article is about the four that wait — the ones that compile clean, pass the Strategy Tester, and survive your first week on demo before they show themselves. Each one lives in a different part of the architecture: how MT5 iterates your trades, how it indexes price arrays, how it computes lot sizes, and how it rebuilds state after a restart — and each produces a silent live-trading failure no backtest will catch.
Where Part 1 Stopped
Part 1 dealt with failures that fire at the moment of execution: the netting-versus-hedging account model collapsing concurrent positions, the CTrade execution contract that returns `true` before an order is actually filled, and synthetic ticks fooling logic that counts price events. Those break loudly enough that you usually catch them in the first few trades.
The four failures in this article are different. They are not at order time — they live in the parts of the EA that run continuously or on restart: the management loop that touches your open trades, the indicator reads that generate your signals, the lot-size math, and the state the EA rebuilds when it comes back up. They get past the backtest for the same underlying reason: none of them throws an error, and a backtest you validate by the equity curve alone will not surface any of them. The logic runs, the curve looks plausible, and the specific misbehavior only appears against a live broker, a real restart, or a chart a human is actually watching. Here are the four, in the order they tend to bite.
Failure #1: The Management Loop That Quietly Stops Looping
In MT5, a trade-management loop copied straight from MT4 can run on every tick and never touch a single open position. The function executes. It logs nothing. Your trailing stops and break-even logic simply never fire.
The cause is that MT4 kept everything in one pool. `OrdersTotal()` returned the count of open market positions *and* pending orders together, and you walked them with `OrderSelect(i, SELECT_BY_POS, MODE_TRADES)`. MT5 split that single pool into separate collections:
| What you want to iterate | MT4 | MT5 |
|---|---|---|
| Open market positions | `OrdersTotal()` + `OrderSelect()` | `PositionsTotal()` + `PositionGetTicket(i)` |
| Pending orders | `OrdersTotal()` (same pool) | `OrdersTotal()` + `OrderGetTicket(i)` — pending only |
| Closed/filled history | `OrdersHistoryTotal()` | `HistoryDealsTotal()` after `HistorySelect()` |

The trap is that `OrdersTotal()` still exists in MT5 — it just means something different. It now counts pending orders only. An EA that loops over `OrdersTotal()` to trail its stops will, on an account with no pending orders, get a count of zero and skip the loop body entirely.
I migrated a break-even EA last year that did exactly this. It compiled, it ran clean in the Strategy Tester, and the equity curve looked plausible — so nobody opened the logs to confirm the break-even moves were actually firing. Nothing errored, so there was no reason to look. On the live account the same loop went quiet: three open positions, none of them ever moved to break-even, because the loop iterating `OrdersTotal()` found a count of zero and did nothing. The trader’s risk sat fully exposed on every trade while the EA reported normal operation. The backtest never flagged it because a missing break-even move is not an error — it is an absence, and you only catch an absence if you go looking for it.
The fix is to route open-trade management through `PositionsTotal()` and `PositionGetTicket()`, and reserve `OrdersTotal()` strictly for pending-order logic. This is a different problem from Part 1’s netting model — that one was about positions merging; this one is about the EA never seeing them.
Failure #2: Your Signals Are Reading the Wrong End of the Array
A migrated EA can compute every entry and exit on the wrong bar — and still produce a believable backtest. The cause is array direction, and it is a common reason a recompiled EA’s trades look scattered and senseless against the chart.
In MT4, price arrays like `Close[]` and `High[]` were series-indexed by default: index 0 was the current bar, index 1 the previous bar, and so on backward in time. Most EA logic is built on that assumption — `Close[1]` means “last completed bar.” In MT5 you no longer read those predefined arrays. You copy data into your own array with `CopyRates()`, `CopyClose()`, or, for an indicator, you create a handle in `OnInit()` and pull values with `CopyBuffer()`. Those arrays are not series-indexed by default. Index 0 is the oldest bar in the copied range unless you explicitly call `ArraySetAsSeries(buffer, true)`.
Miss that one line, and the index your logic trusts points the wrong way:
| Index | MT4 array (series by default) | MT5 copied array (no `ArraySetAsSeries`) |
|---|---|---|
| `[0]` | current bar | oldest bar in the copied range |
| `[1]` | previous bar | second-oldest bar in the copied range |
The EA I trace this to most often was a moving-average crossover system. After migration it created the MA handle correctly and called `CopyBuffer()` correctly, but never set the result array as series. So “fast MA on the previous bar versus slow MA on the previous bar” was actually comparing two values from the far historical end of the buffer — crossovers that had happened days earlier. On the chart, the arrows looked random. In the backtest, the same wrong-end read was applied to every bar uniformly, so the equity curve was merely mediocre, not obviously broken.
The fix is one line per copied array: `ArraySetAsSeries(buffer, true)` immediately after the copy, restoring the MT4 convention the rest of your logic assumes. The consequence of skipping it is not a crash — it is an EA trading on stale signals while looking, by its own logs, completely healthy.
Failure #3: Lot Sizes Drift After Migration on Small Accounts
Money-based position sizing that produced clean lots in MT4 can return subtly wrong lots in MT5, and on a small account that quietly changes the risk you actually carry per trade. The inputs to the calculation are read through different APIs after migration, and the differences land in the rounding.
MT4 sizing typically reads `AccountBalance()` and the broker’s volume constraints through `MarketInfo(Symbol(), MODE_LOTSTEP)`, `MODE_MINLOT`, and `MODE_MAXLOT`. MT5 deprecates those in favor of `AccountInfoDouble(ACCOUNT_BALANCE)` and `SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_STEP / SYMBOL_VOLUME_MIN / SYMBOL_VOLUME_MAX)`. MT5 keeps the old function names alive as compatibility wrappers, which is exactly why this slips through — the code still compiles and still runs.
In one migration I traced a sizer that turned a risk percentage into lots and then normalized the result. In MT4 it produced clean 0.10–0.30 lots. Ported to MT5, the same arithmetic — reading the volume step through the legacy wrapper and rounding with the old assumptions — normalized an intended 0.13 lots to a different step value. On a $1,000 account, where one volume step is a meaningful slice of the position, that is the difference between the 1% risk the trader configured and something they never agreed to. The tester showed no error, because there is no error — the EA simply sized against the wrong precision, and the curve reflected whatever lots it actually opened.
The reliable pattern in MT5 is to read the step natively and normalize to it explicitly:
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));Lot-size errors are their own category of bug — I have written separately about why an EA calculates lot size wrong and sizing across multiple open positions. What makes this one a migration failure specifically is the cause: the calculation did not change, the platform underneath it did.
Failure #4: A Restart Leaves the EA Blind to Its Own Positions
A migrated EA can come back from a restart convinced it holds no positions — and open a fresh set on top of the ones it already has. This is the most expensive failure on the list, because the result is not unmanaged risk. It is doubled risk.
Start with what every EA loses on a restart, on either platform. When the terminal restarts — a VPS reboot, a terminal update, a manual relaunch — it runs `OnDeinit()` and then `OnInit()`, and every `static` or global variable holding state in memory resets. An EA that tracks its grid level, daily trade count, or open-position list only in memory comes back blank. That much is true in MT4 and MT5 alike, and it is why robust EAs rebuild their state in `OnInit()` instead of assuming a clean start.

The migration-specific failure is in *how* that rebuild reads the account. In MT4, an EA reconstructs “do I already hold positions?” by scanning the single `OrdersTotal()` pool — which, as in Failure #1, included open positions. Port that reconstruction verbatim to MT5 and it scans `OrdersTotal()`, now pending-only, finds nothing, and concludes the account is flat. The same pool split that hid positions from the management loop also hides them from the restart rebuild — except here the EA does not just leave them unmanaged. It acts on the false belief that it is starting fresh.
I saw this on a grid EA running on a VPS. An overnight VPS reboot triggered the `OnInit()` cycle. The EA’s reconstruction scanned `OrdersTotal()`, saw zero, reset its grid counter, and opened a fresh first grid level on top of the live positions it had just failed to detect. By morning the account carried roughly double the intended exposure, built on a grid the EA no longer knew existed.
The rebuild has to read the MT5 positions pool directly:
// On restart, rebuild "do I already hold positions?" from the
// MT5 positions pool — NOT OrdersTotal(), which is pending-only.
int held = PositionsTotal();
for(int i = held - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
// re-attach state: magic number, volume, grid level...
}Reconstructing from live positions is one half of the fix; persisting the EA’s own state across the restart is the other. I have covered that persistence pattern separately in why your EA loses its state on terminal restart. The point specific to migration is narrower: the restart-rebuild logic that was correct in MT4 reads the wrong collection in MT5, and the failure mode it produces is the costly one.
The Second-Pass Audit
These four do not show up at order time, so the order-time checks from Part 1 will not catch them. They need a second pass — a short behavioral audit you run *after* the EA compiles and *before* it touches a funded account. Four checks, one per failure:
- List every management loop and confirm what it iterates. Open-trade logic must run on `PositionsTotal()` / `PositionGetTicket()`, not `OrdersTotal()`. If a trailing or break-even loop reads `OrdersTotal()`, it is managing pending orders only.
- Confirm every copied array sets its direction. Every `CopyRates()`, `CopyClose()`, or `CopyBuffer()` result your logic indexes as “recent versus previous” needs `ArraySetAsSeries(arr, true)` right after the copy.
- Re-derive one lot size by hand. Take a real balance and stop distance, compute the lot the EA should produce against the broker’s MT5 `SYMBOL_VOLUME_STEP`, and compare it to what the EA actually opens. They should match to the step.
- Restart the EA on demo and check what it rebuilds. With positions open, restart the terminal and confirm the EA comes back aware of them — that its trade count, grid level, and position list reflect reality, not a clean slate.
The principle is the same one Part 1 closed on: failures that look like broker problems in live trading are almost always development problems an audit would have caught. The Strategy Tester validates your logic against itself. It cannot validate your logic against a live MT5 terminal that iterates trades differently, indexes arrays differently, and hands your EA a different account model to rebuild from after a restart.
At barmenteros FX, this second-pass audit is the first thing we run on an MT4-to-MT5 migration — it is the work that separates “it recompiled” from “it behaves the way it did before.” When a migrated EA is already live and doing things the chart cannot explain, the same audit is where a migration assessment begins.


Leave a Reply