MQL5 Expert Advisor Architecture: The 9-Layer Blueprint Professional Developers Use
Most failed MQL5 projects never had a strategy bug — they had no architecture. A typical EA starts as one OnTick function, grows past 3,000 lines, and gets abandoned because nobody can change the risk rules without breaking the entries. This is the 9-layer blueprint behind every professional EA we build — the standard in our guide to hiring MQL4/MQL5 developers.
Table of Contents
The 9 Layers of a Professional EA
The nine layers are not a style preference. Each answers one question — who decides, who filters, who sizes, who executes — and each is testable in isolation. When you hire MQL4/MQL5 developers, make candidates draw this diagram in the interview: it separates architects from template assemblers.
- Inputs. Every tunable lives in the
inputblock, andOnInitvalidates ranges before the first tick. A 500-point stop with a 50-point target should fail loudly at load. - Configuration. Derived values computed once: risk to money, magic numbers, timezone offsets, point normalization for 4/5-digit pairs. Read-only after init.
- Signal engine. Entry conditions as pure functions returning an enum —
SIGNAL_BUY,SIGNAL_SELL,SIGNAL_NONE. No position checks, no risk math, no order calls — a function that needs only rates and buffers is unit-testable. - Filter stack. Independent boolean gates: session, spread, news blackout, day-of-week. A prop-firm news restriction later means adding one gate, not editing entries.
- Risk engine. Lot sizing from percent risk and stop distance, daily loss limits, exposure caps. The only layer allowed to produce a volume number.
- Execution. One
CTradewrapper owns every order request: retries, deviation, filling mode, magic numbers. The rest of the EA never callsOrderSenddirectly. - Trade management. Trailing, break-even, partials, baskets. Driven by
OnTradeTransaction, not by ticks. - State persistence. Everything the EA must remember across restarts — basket levels, daily loss counters — written to globals or files and rebuilt from account history on startup.
- Diagnostics. Leveled logging, push notifications, and guards that stop the EA cleanly when configuration is invalid.
Data flows one way: signal, filter, risk, execution — and every returning fill flows through trade management. Break the order and determinism dies with it.
The MQL5 Event Model
MQL5 is event-driven, and professional architecture is mostly putting each responsibility in the right handler. Get it wrong and a good signal engine will miss fills and behave differently on a VPS than in the tester.
- OnInit. Load and validate inputs, build configuration, restore state. If validation fails: log, notify,
ExpertRemove. - OnTick. Read market data, run the signal engine and filter stack. A surviving signal goes to risk and execution — OnTick is a reader, not a trader.
- OnTradeTransaction. The server's confirmation channel: fills, partial closes and stop triggers arrive as
MqlTradeTransactionevents. The trade management layer lives here — a stop loss moves the moment a position opens; a basket counter starts the moment a leg closes. - OnDeinit. Flush state to storage, release globals, remove chart objects. An EA that shuts down cleanly restarts cleanly.
- OnTester*.
OnTesterInit,OnTesterDeinit,OnTesterPassandOnTesterrun inside the Strategy Tester during optimization and walk-forward runs, so optimization measures more than raw profit — the methodology is in MQL5 optimization mastery.
Why execution must never live directly in OnTick: ticks arrive hundreds of times per second during news, and nesting trade calls in tick conditions produces duplicate entries when a condition stays true, missed fill handling, and EAs that only work on charts actually receiving ticks.
CTrade vs Raw OrderSend
Both APIs place orders; they are not interchangeable — choosing wrong is how beginner EAs grow a different error policy per call site.
| Feature | CTrade | Raw OrderSend |
|---|---|---|
| Retry logic | Centralized — one retry policy in the execution layer, inherited by every call | None built in — each call site checks the retcode; requotes are your problem everywhere |
| Result struct | Stored internally — ResultRetcode(), ResultPrice(), ResultVolume() read the last MqlTradeResult | Returned through a reference you supply — you validate retcode and every field yourself |
| Async support | SetAsyncMode(true) decouples request and response, pairing with OnTradeTransaction | Synchronous by default — async needs your own event plumbing |
| When to use each | Default execution layer — entries, exits, modifications, one consistent policy | Custom wrappers, MQL4 OrderSend ports, hot loops over many symbols |
CTrade is not a beginner shortcut — it is the professional default because it makes execution auditable. Raw OrderSend remains right when the wrapper itself is the deliverable. The same discipline applies in MT4 — covered in MQL4 vs MQL5 development.
Position Accounting: Netting and Hedging
Your EA will meet both account types, and a developer who builds for one has built for half your clients. A netting account holds at most one position per symbol; new orders in the same direction merge at a volume-weighted average price. A hedging account can hold many positions per symbol, each identified by POSITION_IDENTIFIER. MQL5 exposes the mode via AccountInfoInteger(ACCOUNT_MARGIN_MODE) — ACCOUNT_MARGIN_MODE_RETAIL_NETTING or ACCOUNT_MARGIN_MODE_RETAIL_HEDGING.
The naive approach branches everywhere. The professional approach confines the difference to two helpers: GetExposure(symbol) and CloseSymbol(symbol). Netting: GetExposure reads PositionGetDouble(POSITION_VOLUME) — one position, one number. Hedging: iterate positions, sum volumes per direction, matching POSITION_IDENTIFIER and magic. CloseSymbol mirrors it: one close call versus a per-identifier loop. Every other layer stays identical — one codebase, any broker.
One trap: POSITION_IDENTIFIER is only meaningful in hedging mode; code reading it unconditionally breaks on netting accounts. Confine identifier logic to the helpers and the trap disappears — the same discipline we apply on every MT5 EA development engagement.
Why Modular Beats Script-Style
Script-style EAs — one file, one giant OnTick — work until the second revision; then every change risks regressions because nothing separates concerns. Modular EAs split each layer into an .mqh include, and the main .mq5 file becomes a thin orchestrator that includes modules and delegates.
The folder layout we standardize on:
MyEA.mq5— orchestrator: owns the event handlersInclude/MyEA/Signals.mqh— signal engine, pure functionsInclude/MyEA/Filters.mqh— session, spread, news, day-of-week gatesInclude/MyEA/Risk.mqh— lot sizing, daily loss, exposure capsInclude/MyEA/Execution.mqh— CTrade wrapper and retry policyInclude/MyEA/Manager.mqh— trailing, break-even, partials, basketsInclude/MyEA/State.mqh— persistence and restart rebuildInclude/MyEA/Log.mqh— leveled logging, notifications
The payoff is testability. Feed the signal engine synthetic MqlRates bars and assert the expected enum; test lot sizing against a table of account sizes, stops and risk percentages. When a client asks for a news filter or a tighter daily loss cap, we change one module and re-run its checks.
State That Survives Restarts
An EA that resets to zero after a restart is a liability. VPS reboots, terminal updates and parameter edits tear it down and bring it back — everything in member variables is gone. Survivable state lives in globals via GlobalVariableSet, or in files via FileOpen with the common flag. Globals suit small values but are shared across every chart and EA in the terminal, so names must be namespaced with symbol and magic. Files suit structured state: basket legs, in-flight trade IDs.
The deeper rule: never trust memory for anything financial. Daily loss must be computed from the account's own history on startup — sum the day's realized profit for your magic — because the broker's number is the one that matters. Rebuild baskets the same way: enumerate positions by magic, reconstruct legs from POSITION_IDENTIFIER and volume, never from a saved counter that may be stale.
A restart test belongs in every delivery checklist: run the EA with live positions, force a terminal restart, verify identical trading state.
Diagnostics and Logging
A professional EA tells you what it is doing; a broken one leaves you guessing. Diagnostics is a layer: a Log() helper taking a level and a module name. Levels are input-controlled, so a live account never floods the journal while a test account shows everything. SendNotification pushes fills, errors and daily-loss warnings to your phone; SendMail handles the daily digest. Both need terminal-side configuration — verify availability at startup.
Guards are the last piece. If validation fails at OnInit — invalid lot range, missing symbol, unsupported broker mode — the EA logs the reason, notifies, and calls ExpertRemove instead of trading on assumptions.
Frequently Asked Questions
What is the best structure for an MQL5 EA?
The best structure separates concerns into layers: inputs, configuration, signal engine, filter stack, risk engine, execution, trade management, state persistence and diagnostics. The signal engine returns an enum, filters are boolean gates, execution goes through one CTrade wrapper. Modular .mqh files per layer beat a monolith because each layer is testable alone.
What is the difference between OnTick and OnTradeTransaction?
OnTick fires on every price change and should only read market data and request entries. OnTradeTransaction fires when the server processes a trade event — a fill, partial close or stop trigger — and is where trade management lives. Reacting to fills in OnTick means polling and guessing; OnTradeTransaction tells you exactly what happened, once.
Should I use CTrade or OrderSend?
Use CTrade for the standard execution layer: centralized retry policy, internal result struct, async mode, readable code. Use raw OrderSend for full control — custom wrappers, MQL4 OrderSend ports, many-symbol loops. Most professional EAs use CTrade behind a thin execution module.
How do I make an EA work on both netting and hedging accounts?
Abstract position access behind helpers that check AccountInfoInteger(ACCOUNT_MARGIN_MODE). Netting: at most one position per symbol — read PositionGetDouble(POSITION_VOLUME) directly. Hedging: iterate positions and match by POSITION_IDENTIFIER and magic number. Everywhere else the EA calls GetExposure and CloseSymbol helpers; only those functions know the difference.
How do professionals test an EA before delivery?
In phases: per-module unit checks in the Strategy Tester, a multi-symbol backtest across years of tick data, then optimization with walk-forward validation. Finally a forward test on a demo account, comparing the backtest curve against live results.
The Architecture Is the Product
Strategies come and go; structure is what lets you change them. Whether you are hiring or building yourself, the 9 layers above separate an EA you revise for years from one you rewrite from scratch. Tell us what your system should do — we will map it onto this architecture and deliver the source.