An MT4 EA that takes two hours to recompile into MT5 syntax can take three weeks to start failing in ways you cannot explain. That gap — between “it compiles” and “it works the same way it did in MT4” — is the migration problem nobody talks about. The three things that actually break are not syntax. They are architecture.
The Recompile Assumption
The standard advice circulating on forums goes like this: rename the file, resolve the compile errors — most of which are renamed functions — and run the backtester. If the equity curve looks the same, you are done.
This process works for simple EAs with no concurrent position logic, no execution verification, and no tick-based timing. For anything else, it produces an EA that appears to migrate successfully because it compiles, opens trades, and logs nothing unusual — while quietly failing at the architectural assumptions MT5 does not share with MT4.
A client brought me a hedging EA three weeks into a migration. The developer had recompiled it in an afternoon, tested it on demo, and considered it production-ready. The EA’s internal position counter showed 2 open positions. The account held 0. Every hedge order had been silently converted to a position close by the MT5 execution engine. The EA had been managing a ghost inventory for three weeks. The code never threw an error. It was operating on a fundamental assumption about how MT5 accounts work — an assumption MT4 never needed to make.
That is the recompile problem. The failure is silent because the EA does not crash. It continues doing work. Just the wrong work.
Architecture Failure #1: Netting vs. Hedging Accounts
MT4 did not have account types in the sense MT5 does. Every MT4 account let you hold multiple positions on the same symbol simultaneously — a buy and a sell, two buys at different levels, a grid of seven concurrent positions. This was the only mode that existed.
MT5 introduced a netting account model where the broker aggregates all positions on a single symbol into one net position. Open Buy 0.10, then open another Buy 0.10 on the same symbol: you get one position at 0.20, not two positions at 0.10 each. Open Buy 0.10, then Sell 0.10: both cancel out. You hold 0.
Any EA that relies on concurrent positions — grids, hedges, partial closes via a reverse order, martingale systems — will behave differently on a netting account. The EA receives no error when this happens.
The specific failure mode in a grid EA: price drops, the EA places grid level 2 (Buy 0.10 at 1.0950). On a MT5 hedging account, the account now holds two positions — level 1 at 1.1000, level 2 at 1.0950. The EA tracks both, calculates take-profit and trailing independently for each. On a MT5 netting account, the account holds one position at 0.20 lots at an averaged entry of approximately 1.0975. The EA’s internal grid tracker still shows two levels. It tries to close “level 1” independently — which triggers a partial close that leaves residual exposure the EA no longer tracks. The position management logic operates on a position inventory that does not match what the account holds.
Account type behavior — what MT4 assumed vs. what MT5 enforces:
| Scenario | MT5 Netting Account | MT5 Hedging Account |
|---|---|---|
| Buy 0.10, then Buy 0.10 (same symbol) | Single position at 0.20 lots, averaged entry | Two independent positions at 0.10 each |
| Buy 0.10, then Sell 0.10 (same symbol) | Positions net to zero — both close | Two opposing positions held simultaneously |
| Close “level 1” independently in a grid | Partial close: reduces net position, orphaned remainder | Closes the specific position by ticket |
| EA’s internal tracker vs. account state | Mismatch — inventory diverges silently | Matches — each ticket corresponds to one position |

The fix is not code. It is deciding which account type the EA requires, then enforcing that as a hard precondition before migration begins. An `OnInit()` check against `AccountInfoInteger(ACCOUNT_MARGIN_MODE)`takes three lines and stops the EA from running on the wrong account type entirely.
Architecture Failure #2: OrderSend Is Not a CTrade Wrapper
The first thing most migration guides do is show you how to replace `OrderSend()` with `CTrade::Buy()` and `CTrade::Sell()`. The syntax is cleaner. The function signatures are more descriptive. It looks like a straightforward swap.
The problem is not the syntax. It is the execution contract.
MT4’s `OrderSend()` returns an integer: the ticket number if the order was accepted and filled, `-1` if it failed. One return value carries both pieces of information — was it accepted, and did it execute. Most MT4 EAs check `if(ticket < 0)` to detect failure. If the ticket is positive, the EA assumes the position now exists at the intended size.
MT5’s CTrade methods return `bool`. `true` when the server accepted the request. `false` only when local validation fails — invalid lot size, insufficient margin the terminal can detect before sending. A `true` return means the request left your machine. It does not mean the order was filled. It does not mean the position was opened at the size you requested.
On ECN and STP accounts in MT5, partial fills are routine. The EA calls `trade.Buy(0.10, …)` and receives `true`. The server fills 0.07 lots. The remaining 0.03 is rejected or queued depending on the broker’s fill policy. The EA’s internal position tracker records 0.10. The account holds 0.07. Every subsequent lot size calculation for grid spacing, take-profit, and position management carries that error forward.

In every MT4 EA I have audited for migration, the pattern appears in some form: `if(ticket > 0)` followed immediately by state updates that assume the full intended position is now open. Each one of these becomes a silent mismatch risk in MT5. After every CTrade execution call, the correct checks are `trade.ResultRetcode() == TRADE_RETCODE_DONE` to confirm the order was fully processed, and `trade.ResultVolume()` to verify the actual filled size matches what was requested. State updates happen only after both checks pass.
Architecture Failure #3: OnTick() Does Not Mean New Price
In MT4, `OnTick()` fired when price changed. Every call brought a new Bid and Ask. Developers who used tick count as a proxy for market activity — counting ticks since the session opened, measuring tick frequency as a liquidity filter — were counting real price events.
In MT5, `OnTick()` fires on any market data event, including broker-generated keepalive ticks where Bid and Ask are identical to the previous call. During low-liquidity periods and in the minutes before major sessions open, these synthetic ticks can dominate the call count.
A session-filter EA I migrated for a client used a tick counter for session activation: once the counter reached 20 ticks after the session open time, the EA would begin evaluating entry signals. In MT4, 20 ticks meant 20 price movements — a reasonable proxy for market participation. After migrating to MT5 and running for two weeks on demo, the EA was opening positions in the exact low-liquidity window it was designed to avoid. The MT5 tick stream included keepalive ticks indistinguishable to the counter from real price events. By the time actual price movement began, the threshold had already been cleared on synthetic ticks.
The fix is two lines: before processing any `OnTick()` logic that depends on price change, check `if(Bid == prevBid && Ask == prevAsk) return;`. Any tick-dependent logic in a migrated EA needs this guard — and it takes a targeted search to find all the places where it belongs.
The Migration That Actually Works
Correct migration runs three audits before the first line of MT5 code is written. The audits are inventory checks on assumptions the MT4 EA makes that MT5 may not honor.
In client migration projects, these are the three checks that run before any code is touched:
Order topology audit. List every `OrderSend()` call in the MT4 EA. Classify each one: does it open a standalone position, add a grid level, or create a hedge? Any call that opens a second concurrent position on the same symbol is netting-incompatible. The account type decision — require hedging, restructure logic for netting, or both — is made here, not after the fact.
Execution confirmation audit. Find every location where the EA acts on the assumption that an order was filled. `if(ticket > 0)` and `if(ticket != EMPTY)` are the patterns to search for. Each becomes an explicit result inspection in MT5: `trade.ResultRetcode() == TRADE_RETCODE_DONE` to confirm execution, `trade.ResultVolume()` to confirm filled size. State updates are moved to after both checks pass.
Tick dependency audit. Search for tick counters, tick-based timers, and any `static` variable that increments in `OnTick()` and gates behavior through a conditional. Each one gets a price-change guard before the increment.
These three audits take two to four hours on a typical client EA. They surface every silent-failure risk before any code changes begin. The actual MT5 migration work — rewriting order calls, restructuring CTrade logic, adding guards — is then targeted and verifiable. The failures that look like broker problems during live trading are development problems. Catching them in audit is the difference between a migration that works and three weeks of unexplained account behavior.
At barmenteros FX, these three audits are the first step of every MT4 to MT5 migration project we take on. If your EA has been migrated and is behaving unexpectedly — or you want to know exactly what needs to change before the migration begins — request a free assessment.


Leave a Reply