BTC·
ETH·
SOL·
BNB·
XRP·
ADA·
DOGE·
AVAX·
LINK·
MATIC·
DOT·
ATOM·
BTC·
ETH·
SOL·
BNB·
XRP·
ADA·
DOGE·
AVAX·
LINK·
MATIC·
DOT·
ATOM·
Back to blog
MQL5Expert AdvisorArchitectureTrading

MQL5 Expert Advisor Architecture: How to Build Production-Grade Trading Robots

iL
Ismaël Ladjohounlou
September 10, 202518 min read

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

Contact me on Telegram →