MQL4 Expert Advisor Architecture: How Professionals Structure Your EA
MQL4 looks simple — init, start, deinit — and that simplicity is a trap. Every professional MQL4 EA carries the same invisible machinery underneath: tick filtering, order loops, pip normalization, requote retries. Here is that machinery, layer by layer, at the standard we hold every MQL4/MQL5 developer we hire to.
Table of Contents
The MQL4 Skeleton
MQL4 gives an EA exactly three entry points, and every line you write lives inside one of them: init() runs once when the EA is attached or the chart timeframe changes, start() runs on every tick, and deinit() runs when the EA is removed.
What belongs where is the first professional habit. init(): parameter validation, computed constants, indicator handle loading, and one-time sanity checks. deinit(): object removal, file and global-variable cleanup. start(): everything else — but only after the filters below decide work is actually needed.
The classic shape: int init() { ... return(INIT_SUCCEEDED); } — init() return codes tell the terminal whether the EA is safe to load. A failure code on a bad parameter is the difference between a clean journal error and a broken EA trading silently with defaults.
The Tick-Loop Problem
start() fires thousands of times per hour — on a normal EURUSD day, that is 50,000 to 150,000 calls. Whatever sits in it runs that often, so the cheapest professional habit is also the most valuable: do nothing unless something actually changed. Three guards, applied in order:
Spread guard. if (MarketInfo(Symbol(), MODE_SPREAD) > MaxSpread) return(0); — never enter when the spread is wide enough to make the trade mathematically bad.
New-bar detection. static datetime lastBar = 0; if (Time[0] == lastBar) return(0); lastBar = Time[0]; — signal logic runs once per bar, matching the strategy you actually designed instead of a tick-twitching approximation of it.
Context guard. if (IsTradeContextBusy()) return(0); — the terminal serializes trading operations; checking this avoids fighting your own pending request.
New-bar detection changes behavior more than people expect: a martingale entry evaluated on every tick can fire five times in one second of volatility; the same logic on a new bar fires once.
Order Management in MQL4
MQL4 exposes open orders through a positional loop, not an object model: OrdersTotal() counts them, OrderSelect() points at one, and the Order*() family reads its fields. Every order-management block is a variation of:
for (int i = OrdersTotal() - 1; i >= 0; i--) { if (!OrderSelect(i, SELECT_BY_POS)) continue; if (OrderMagicNumber() != Magic) continue; ... }
Two details carry the whole discipline. Count down, never up — closing order i shifts the positions behind it. And the magic number is your EA's fingerprint: every OrderSend() stamps it, every loop filters by it, so two EAs on one account never touch each other's trades and the EA never manages a manual trade it does not own.
OrderSend() itself takes the symbol, action, volume, price, slippage, stop loss, take profit, a comment, the magic number, and an expiration. Professionals use the comment field as a readable trade ID — strategy tag, entry reason, generation number — because it survives in closed history, and history is how you audit an EA after a bad week.
Pip Math on 4-Digit and 5-Digit Brokers
Point is the smallest price increment — 0.00001 on a 5-digit EURUSD broker, 0.0001 on a 4-digit one. A pip is the unit your strategy thinks in: 0.0001 for EURUSD, 0.01 for USDJPY. Confusing the two is the most common EA defect in MQL4, and it fails silently.
The normalization lives in one function: double Pip() { return (Digits % 2 == 1) ? Point * 10.0 : Point; } — three-digit JPY pairs and five-digit majors both divide into pips correctly. Call it once in init() and store the result.
A stop hardcoded at 30 points is 30 pips on a 4-digit broker and 3 pips on a 5-digit broker — the EA silently changes its risk profile depending on where it runs, and a rollover spread wider than the stop kills the position at entry. That is the failure in the case study below.
Error Handling: Requotes and Busy Loops
The MQL4 order pipeline rejects requests constantly; three errors dominate live EAs: 138 — requote, the price moved, resend with the fresh quote; 146 — trade context busy, another operation is in flight; 133 — trading disabled, stop trying entirely.
The professional pattern is a bounded retry loop:
for (int tries = 0; tries < 5; tries++) { ticket = OrderSend(...); if (ticket > 0) break; if (GetLastError() != 138 && GetLastError() != 146) break; RefreshRates(); Sleep(300); }
Unbounded retries are almost as bad as none: a loop that never gives up freezes the EA in a live market. A fixed attempt count, a short sleep, and a decision when retries run out — log it, skip the signal, or flag the account — is the whole discipline.
Trade Manager Patterns
Everything that manages open trades is built from the same loop: select each order by magic number, read its fields, and modify or close when a condition is met. The four patterns that cover most real EAs:
- Trailing stop. One
OrderModify()when price has moved the trail step since the last modification — check the current stop first so you never move a stop backward. - Break-even. A single OrderModify that moves the stop to entry plus offset the first time profit crosses the threshold, and never again.
- Basket close. Iterate all orders matching the magic number and close them in one pass, respecting the count-down rule.
- Partial close.
OrderClose(OrderTicket(), lotsToClose, OrderClosePrice(), 3)— close part of the volume, leaving the remainder under the same magic number and ticket.
Every one of these must survive the same reality: stops are modified one order at a time, on ticks, against a moving price — and a trailing stop that silently fails to modify is an EA trading with no stop at all.
A Real Failure Case
In 2016, a client came to us with an EA that had traded profitably on a 4-digit broker for two years. They had just moved the account to a new 5-digit broker — same EA, same parameters — and it was losing on nearly every trade. The EA had been built with stops hardcoded at 30 points.
On the 4-digit broker, 30 points was 30 pips. On the 5-digit broker, the same 30 points became 3 pips. The stop now sat inside the spread at rollover and during news, so positions died at entry, one spread at a time. The EA was not broken — its assumptions were. The fix took an afternoon: a normalization layer that computes every pip value from Digits once in init(), logs the computed values to the journal, and asserts the stop distance is wider than the current spread before any entry.
Broker digits, spread, stop levels, and lot step are inputs to the architecture, not environment details. An EA that treats them as fixed constants has an expiration date.
Limits to Respect
Professional architecture works inside the platform's shape instead of fighting it. MQL4's shape: a single-thread strategy tester — one symbol per pass, so portfolio validation is manual labor; FIFO rules on US-regulated brokers; netting-only accounting, no separate buy and sell positions on one symbol; 8 indicator buffers per custom indicator; and no trade-transaction events, which forces every EA to poll positions on ticks.
None of these are bugs. They are constraints, and the difference between a junior and a senior build is whether the constraints were designed in or discovered in production.
When to Move to MQL5
When a system starts fighting these limits — a second symbol, a copier, hedging requirements — that is the signal to move to MQL5, not to add more scaffolding on MQL4. The port is a structured job: the MQL4 to MQL5 migration case study shows a grid EA making the trip with parity backtests, and the MQL4 vs MQL5 comparison maps every difference the port will touch. The architecture above also survives the move: tick filters become OnTick guards, order loops become PositionSelect, pip normalization carries over unchanged — and the same applies in reverse when a project ships on MT4 first, see MT4 development.
Frequently Asked Questions
What is the entry point of an MQL4 EA?
MQL4 has three entry points: init() runs once when the EA loads or the chart timeframe changes, start() runs on every tick and is the main execution loop, and deinit() runs when the EA is removed. All trading logic belongs in start(), guarded by tick and new-bar filters.
How do you detect a new bar in MQL4?
Compare Time[0] with a stored datetime variable. When they differ, a new bar has opened. Keep the variable static or global so it survives between ticks, and reset it whenever the chart timeframe changes so stale values never block signal evaluation.
How do I make my MQL4 EA work on a 5-digit broker?
Never hardcode pip distances. Compute the pip size from Digits: if Digits is 3 or 5, one pip is 10 points; otherwise one pip is 1 point. Convert every user-facing pip input through this normalization once in init() and store the result for all stop and take-profit calculations.
Why does my MT4 EA get requotes?
Error 138 means the broker rejected your order because the price moved between the quote your EA saw and the moment the request arrived. Fix it with a bounded retry loop: refresh rates, sleep briefly, resend, and stop after a fixed number of attempts so the EA never freezes in a fast market.
How many indicators can an MQL4 EA use?
Each custom indicator is limited to 8 buffers, and the main chart window can display 8 indicators at once. An EA can call many indicators through iCustom, but every custom indicator it relies on must work within the 8-buffer ceiling.
Architecture Is What Survives Contact With a Live Market
The layers above are the difference between an EA that backtests beautifully and one that trades next year. Send us an existing MQL4 EA and we will audit it against exactly this structure; a new build starts from the same standard.