MQL5 Expert Advisor Architecture: How to Build Production-Grade Trading Robots
Most EAs break after 3 months. Learn the modular architecture — signal, risk, execution, reporting modules — that makes EAs maintainable, testable and prop firm ready.
Why Most EAs Fail After a Few Months
I've seen hundreds of EAs. 90% look like this:
****mql5 void OnTick() { if(iMA(NULL,0,20,0,MODE_EMA,PRICE_CLOSE,0) > iMA(NULL,0,50,0,MODE_EMA,PRICE_CLOSE,0)) { if(OrdersTotal() == 0) { OrderSend(Symbol(), OP_BUY, 0.1, Ask, 3, Ask-500*Point, Ask+1000*Point); } } } ****
This works. Until it doesn't. Then the trader can't figure out why, can't fix it, can't optimize it.
The problem: no separation of concerns. Signal logic, risk logic, execution logic all tangled together.
The Modular Architecture
After 5+ years and 120+ EAs, I now use this structure for every project:
**** EA Root ├── Signal Module (entry/exit conditions) ├── Risk Module (position sizing, max drawdown) ├── Execution Module (order placement, management) ├── Journal Module (logging, reporting) └── Utils (helpers, constants) ****
Each module is a separate .mqh include file. The main .mq5 orchestrates them.
Module 1: Signal Module
****`mql5
// signals.mqh
#pragma once
enum SIGNAL_TYPE { SIGNAL_NONE, SIGNAL_BUY, SIGNAL_SELL };
class CSignalModule {
private:
int m_fastMA_handle;
int m_slowMA_handle;
int m_rsi_handle;
double m_rsi_level_ob; // Overbought
double m_rsi_level_os; // Oversold
public:
CSignalModule(int fastPeriod=20, int slowPeriod=50,
int rsiPeriod=14, double ob=70, double os=30) {
m_fastMA_handle = iMA(NULL, 0, fastPeriod, 0, MODE_EMA, PRICE_CLOSE);
m_slowMA_handle = iMA(NULL, 0, slowPeriod, 0, MODE_EMA, PRICE_CLOSE);
m_rsi_handle = iRSI(NULL, 0, rsiPeriod, PRICE_CLOSE);
m_rsi_level_ob = ob;
m_rsi_level_os = os;
}
~CSignalModule() {
IndicatorRelease(m_fastMA_handle);
IndicatorRelease(m_slowMA_handle);
IndicatorRelease(m_rsi_handle);
}
SIGNAL_TYPE GetSignal() {
double fastMA[2], slowMA[2], rsi[2];
if(CopyBuffer(m_fastMA_handle, 0, 0, 2, fastMA) < 2) return SIGNAL_NONE;
if(CopyBuffer(m_slowMA_handle, 0, 0, 2, slowMA) < 2) return SIGNAL_NONE;
if(CopyBuffer(m_rsi_handle, 0, 0, 2, rsi) < 2) return SIGNAL_NONE;
// EMA crossover + RSI confirmation
bool bullCross = fastMA[1] < slowMA[1] && fastMA[0] > slowMA[0];
bool bearCross = fastMA[1] > slowMA[1] && fastMA[0] < slowMA[0];
if(bullCross && rsi[0] < m_rsi_level_ob) return SIGNAL_BUY;
if(bearCross && rsi[0] > m_rsi_level_os) return SIGNAL_SELL;
return SIGNAL_NONE;
}
};
****`
Why this matters: now you can swap the signal logic without touching risk or execution. Want to test SMC entries instead of EMA? Replace the signal module. Everything else stays.
Module 2: Risk Module
This is where most EAs fail. Fixed lot sizes are amateur. Here's professional risk management:
****`mql5
// risk.mqh
#pragma once
class CRiskModule {
private:
double m_riskPercent; // % of balance per trade
double m_maxDailyLoss; // % of balance max daily loss
double m_maxDrawdown; // % max total drawdown
double m_startBalance;
double m_dailyStartBalance;
datetime m_lastDay;
public:
CRiskModule(double riskPct=0.5, double maxDailyLoss=1.5, double maxDD=8.0) {
m_riskPercent = riskPct;
m_maxDailyLoss = maxDailyLoss;
m_maxDrawdown = maxDD;
m_startBalance = AccountInfoDouble(ACCOUNT_BALANCE);
m_dailyStartBalance = m_startBalance;
m_lastDay = 0;
}
// Check if we can open a new trade
bool CanTrade() {
// Reset daily counter
if(TimeDay(TimeCurrent()) != TimeDay(m_lastDay)) {
m_dailyStartBalance = AccountInfoDouble(ACCOUNT_BALANCE);
m_lastDay = TimeCurrent();
}
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
double dailyLoss = (m_dailyStartBalance - balance) / m_dailyStartBalance * 100;
double totalDD = (m_startBalance - balance) / m_startBalance * 100;
if(dailyLoss >= m_maxDailyLoss) {
Print("Daily loss limit reached: ", DoubleToString(dailyLoss, 2), "%");
return false;
}
if(totalDD >= m_maxDrawdown) {
Print("Max drawdown reached: ", DoubleToString(totalDD, 2), "%");
return false;
}
return true;
}
// Calculate lot size based on SL distance
double CalculateLots(double slPoints) {
if(slPoints <= 0) return 0.01;
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
double riskAmount = balance * m_riskPercent / 100.0;
double tickValue = SymbolInfoDouble(Symbol(), SYMBOL_TRADE_TICK_VALUE);
double tickSize = SymbolInfoDouble(Symbol(), SYMBOL_TRADE_TICK_SIZE);
double lotsRaw = riskAmount / (slPoints * tickValue / tickSize);
double lotsMin = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN);
double lotsMax = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MAX);
double lotsStep = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_STEP);
double lots = MathRound(lotsRaw / lotsStep) * lotsStep;
return MathMin(MathMax(lots, lotsMin), lotsMax);
}
};
****`
The key insight: CalculateLots() takes the SL distance in points and returns the exact lot size that risks exactly m_riskPercent of the balance. No guessing. No fixed lots.
Module 3: Execution Module
****`mql5
// execution.mqh
#pragma once
#include <Trade/Trade.mqh>
class CExecutionModule {
private:
CTrade m_trade;
int m_slippage;
int m_magicNumber;
public:
CExecutionModule(int magic=123456, int slippage=10) {
m_magicNumber = magic;
m_slippage = slippage;
m_trade.SetExpertMagicNumber(magic);
m_trade.SetDeviationInPoints(slippage);
m_trade.SetTypeFilling(ORDER_FILLING_IOC);
}
bool OpenBuy(double lots, double sl, double tp, string comment="") {
double ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK);
return m_trade.Buy(lots, Symbol(), ask, sl, tp, comment);
}
bool OpenSell(double lots, double sl, double tp, string comment="") {
double bid = SymbolInfoDouble(Symbol(), SYMBOL_BID);
return m_trade.Sell(lots, Symbol(), bid, sl, tp, comment);
}
bool CloseAll() {
for(int i = PositionsTotal()-1; i >= 0; i--) {
ulong ticket = PositionGetTicket(i);
if(PositionGetInteger(POSITION_MAGIC) == m_magicNumber)
m_trade.PositionClose(ticket);
}
return true;
}
int CountOpenPositions() {
int count = 0;
for(int i = 0; i < PositionsTotal(); i++) {
ulong ticket = PositionGetTicket(i);
if(PositionGetInteger(POSITION_MAGIC) == m_magicNumber) count++;
}
return count;
}
};
****`
Module 4: Journal Module
Automated logging that generates a report you can share with clients:
****`mql5
// journal.mqh
#pragma once
class CJournalModule {
private:
string m_logFile;
int m_fileHandle;
public:
CJournalModule() {
string dt = TimeToString(TimeCurrent(), TIME_DATE);
StringReplace(dt, ".", "-");
m_logFile = "EA_Journal_" + dt + ".csv";
m_fileHandle = FileOpen(m_logFile, FILE_WRITE|FILE_CSV|FILE_ANSI);
if(m_fileHandle != INVALID_HANDLE)
FileWrite(m_fileHandle, "Time,Symbol,Type,Lots,Entry,SL,TP,Profit,Comment");
}
~CJournalModule() {
if(m_fileHandle != INVALID_HANDLE) FileClose(m_fileHandle);
}
void LogTrade(string type, double lots, double entry,
double sl, double tp, double profit, string comment) {
if(m_fileHandle == INVALID_HANDLE) return;
FileWrite(m_fileHandle,
TimeToString(TimeCurrent()),
Symbol(), type,
DoubleToString(lots, 2),
DoubleToString(entry, _Digits),
DoubleToString(sl, _Digits),
DoubleToString(tp, _Digits),
DoubleToString(profit, 2),
comment
);
}
};
****`
The Main EA — Putting It All Together
****`mql5
// MyEA.mq5
#property strict
#include "signals.mqh"
#include "risk.mqh"
#include "execution.mqh"
#include "journal.mqh"
// ─── Inputs ──────────────────────────────────────────────────────
input double RiskPercent = 0.5;
input double MaxDailyLoss = 1.5;
input double MaxDrawdown = 8.0;
input int FastMAPeriod = 20;
input int SlowMAPeriod = 50;
input int SL_Points = 350; // Stop loss in points
input int TP_Points = 700; // Take profit (1:2 RR)
input bool NewsFilter = true;
input int MagicNumber = 123456;
// ─── Module instances ────────────────────────────────────────────
CSignalModule* g_signal;
CRiskModule* g_risk;
CExecutionModule* g_exec;
CJournalModule* g_journal;
int OnInit() {
g_signal = new CSignalModule(FastMAPeriod, SlowMAPeriod);
g_risk = new CRiskModule(RiskPercent, MaxDailyLoss, MaxDrawdown);
g_exec = new CExecutionModule(MagicNumber);
g_journal = new CJournalModule();
return INIT_SUCCEEDED;
}
void OnDeinit(const int reason) {
delete g_signal;
delete g_risk;
delete g_exec;
delete g_journal;
}
void OnTick() {
// Only on new bar
static datetime lastBar = 0;
if(iTime(NULL, 0, 0) == lastBar) return;
lastBar = iTime(NULL, 0, 0);
// Risk check first
if(!g_risk.CanTrade()) return;
// No stacking
if(g_exec.CountOpenPositions() > 0) return;
// Get signal
SIGNAL_TYPE signal = g_signal.GetSignal();
if(signal == SIGNAL_NONE) return;
// Calculate position size
double lots = g_risk.CalculateLots(SL_Points);
double sl, tp;
if(signal == SIGNAL_BUY) {
double ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK);
sl = ask - SL_Points * _Point;
tp = ask + TP_Points * _Point;
if(g_exec.OpenBuy(lots, sl, tp, "EMA Cross BUY"))
g_journal.LogTrade("BUY", lots, ask, sl, tp, 0, "EMA Cross");
}
else if(signal == SIGNAL_SELL) {
double bid = SymbolInfoDouble(Symbol(), SYMBOL_BID);
sl = bid + SL_Points * _Point;
tp = bid - TP_Points * _Point;
if(g_exec.OpenSell(lots, sl, tp, "EMA Cross SELL"))
g_journal.LogTrade("SELL", lots, bid, sl, tp, 0, "EMA Cross");
}
}
****`
Prop Firm Compliance Checklist
Before submitting any EA to a prop firm challenge, verify:
| Check | Detail | Status |
|---|---|---|
| Max daily loss | Hard stop at 4.5% (buffer before 5% limit) | ✅ |
| Max total drawdown | Hard stop at 8% (buffer before 10%) | ✅ |
| No news trading | Block 30 min before/after high-impact news | ✅ |
| No weekend holding | Close all trades Friday 20:00 | Add if needed |
| Magic number | Unique per EA to avoid conflicts | ✅ |
| Lot size calculation | Dynamic based on balance | ✅ |
Backtesting This Architecture
The modular design makes optimization trivial. Use the built-in MT5 Strategy Tester with genetic optimization on:
- FastMAPeriod: 10-30
- SlowMAPeriod: 40-100
- SL_Points: 200-600
- TP_Points: 400-1200
Run Walk-Forward analysis to avoid curve-fitting. Your out-of-sample results should be within 30% of in-sample.
Want a Custom EA Built?
I build EAs using this exact architecture. Every client receives:
- Full source code with comments
- Backtest report (5+ years, multiple pairs)
- Walk-Forward analysis
- Optimization guide
- Lifetime support for bugs