A C++ compiler and a terminal. No MetaTrader, no broker, no account and no money, and
nothing below can place a trade. Most Linux machines already have g++; on
macOS run xcode-select --install once; on Windows use the Windows Subsystem for Linux
or MSYS2.
If you have done an earlier MQL5 page you already have the two setup files — reuse them and start at step 3.
The trade calls here are simulated. OrderSend needs a running
terminal and a broker connection, so step 5 uses a stand-in that applies the same validation a
server applies — which is the part worth practising. When you move this to MetaEditor, test it
on a DEMO account first, and leave it there long enough to see it behave over several days.
What We Will Build
We will create a complete Expert Advisor that trades a simple moving average crossover strategy: buy when the fast MA crosses above the slow MA, sell when it crosses below. The EA includes stop-loss, take-profit, and only one position at a time.
Complete EA Source Code
//+------------------------------------------------------------------+
//| MACrossoverEA.mq5 |
//| Simple Moving Average Crossover Expert Advisor |
//+------------------------------------------------------------------+
#include <Trade/Trade.mqh> // Include the CTrade class
// Input parameters
input int FastMAPeriod = 10; // Fast MA Period
input int SlowMAPeriod = 50; // Slow MA Period
input double LotSize = 0.1; // Lot Size
input int StopLoss = 100; // Stop Loss (points)
input int TakeProfit = 200; // Take Profit (points)
input int MagicNumber = 12345; // Magic Number (unique EA ID)
// Global variables
int fastMAHandle, slowMAHandle;
datetime lastBarTime;
CTrade trade; // Trade execution object
//+------------------------------------------------------------------+
int OnInit()
{
// Validate inputs
if(FastMAPeriod >= SlowMAPeriod)
{
Print("Error: Fast MA must be smaller than Slow MA");
return INIT_PARAMETERS_INCORRECT;
}
// Create indicator handles
fastMAHandle = iMA(_Symbol, PERIOD_CURRENT, FastMAPeriod,
0, MODE_EMA, PRICE_CLOSE);
slowMAHandle = iMA(_Symbol, PERIOD_CURRENT, SlowMAPeriod,
0, MODE_EMA, PRICE_CLOSE);
if(fastMAHandle == INVALID_HANDLE || slowMAHandle == INVALID_HANDLE)
{
Print("Error creating MA handles");
return INIT_FAILED;
}
// Set magic number for order identification
trade.SetExpertMagicNumber(MagicNumber);
lastBarTime = 0;
Print("MA Crossover EA initialized. Fast: ", FastMAPeriod,
" Slow: ", SlowMAPeriod);
return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
void OnTick()
{
// Only trade on new bars (not every tick)
datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0);
if(currentBarTime == lastBarTime) return;
lastBarTime = currentBarTime;
// Get MA values for the last 3 completed bars
double fastMA[], slowMA[];
ArraySetAsSeries(fastMA, true);
ArraySetAsSeries(slowMA, true);
CopyBuffer(fastMAHandle, 0, 1, 3, fastMA);
CopyBuffer(slowMAHandle, 0, 1, 3, slowMA);
// Detect crossover on the LAST COMPLETED bar
bool bullishCross = (fastMA[0] > slowMA[0]) &&
(fastMA[1] <= slowMA[1]);
bool bearishCross = (fastMA[0] < slowMA[0]) &&
(fastMA[1] >= slowMA[1]);
// Check if we have an open position
bool hasPosition = PositionSelect(_Symbol);
// BULLISH CROSSOVER — close sell, open buy
if(bullishCross)
{
if(hasPosition && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL)
trade.PositionClose(_Symbol);
if(!PositionSelect(_Symbol)) // no position after close
{
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double sl = ask - StopLoss * _Point;
double tp = ask + TakeProfit * _Point;
trade.Buy(LotSize, _Symbol, ask, sl, tp, "MA Cross Buy");
}
}
// BEARISH CROSSOVER — close buy, open sell
if(bearishCross)
{
if(hasPosition && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
trade.PositionClose(_Symbol);
if(!PositionSelect(_Symbol))
{
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double sl = bid + StopLoss * _Point;
double tp = bid - TakeProfit * _Point;
trade.Sell(LotSize, _Symbol, bid, sl, tp, "MA Cross Sell");
}
}
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
IndicatorRelease(fastMAHandle);
IndicatorRelease(slowMAHandle);
Print("MA Crossover EA removed. Reason: ", reason);
}
//+------------------------------------------------------------------+
Key Concepts Explained
The CTrade Class
The #include <Trade/Trade.mqh> line imports the standard trade library. The CTrade class provides clean methods for order operations:
trade.Buy(volume, symbol, price, sl, tp, comment);
trade.Sell(volume, symbol, price, sl, tp, comment);
trade.PositionClose(symbol);
trade.PositionModify(ticket, sl, tp);
This is much easier than the raw OrderSend() function.
Magic Number
The Magic Number is a unique identifier for your EA's trades. If you run multiple EAs on the same account, each should have a different Magic Number so they do not interfere with each other's positions.
New Bar Detection
We only check for signals when a new bar opens (not on every tick). This prevents the EA from repeatedly acting on the same signal and ensures we use completed bar data for analysis.
Crossover Detection
A crossover is detected by comparing two consecutive bars: if the fast MA was below the slow MA on bar[1] and is now above on bar[0], that is a bullish crossover. We use bars starting from index 1 (last completed bar) to avoid using the still-forming current bar.
Testing Your EA
In MT5, press Ctrl + R or go to View > Strategy Tester.
Select your EA, choose a symbol (e.g., EURUSD), set the timeframe (e.g., H1), and choose a date range with at least 1 year of data.
Click Start. After the backtest, examine the Results tab (trade list), Graph tab (equity curve), and Report tab (statistics).
Past performance does not predict future results. Backtests use perfect historical data without real-world issues like slippage, requotes, and variable spreads. Always forward-test on a demo account before going live.
Build a Complete Expert Advisor and Prove Every Guard Earns Its Place, in Five Steps
A first Expert Advisor is usually a signal wrapped in a trade call, and it usually behaves nothing like its author expects — sending dozens of orders for one crossover, stacking positions, or trading on a bar that had not finished. The fix is three guards, and rather than take them on trust you are going to remove each one and count the damage. In the next half hour you will run a complete EA, measure what happens with each guard missing, and see exactly what a trade request contains. Every line of output below came from running these files.
Go: open a terminal in a folder you can write to — cd ~/Desktop on macOS or Linux, cd %USERPROFILE%\Desktop on Windows.
Do: save this as mql5.h. If you already have it from an earlier page, reuse that
copy and skip to step 3.
// mql5.h - a small shim that lets a useful SUBSET of MQL5 compile and run
// with an ordinary C++ compiler, so you can practise the language without
// MetaTrader. It is NOT MetaTrader and does not trade anything.
#pragma once
#include <cstdio>
#include <cmath>
#include <string>
#include <vector>
#include <iostream>
#include <sstream>
typedef std::string string;
typedef long long datetime;
#define input static
template <typename T> int ArraySize(const std::vector<T> &a) { return (int)a.size(); }
template <typename T> void ArrayResize(std::vector<T> &a, int n) { a.resize(n); }
inline void Print() { std::cout << std::endl; }
template <typename T, typename... Rest>
void Print(const T &first, const Rest &...rest) {
std::cout << first;
if constexpr (sizeof...(rest) > 0) Print(rest...);
else std::cout << std::endl;
}
inline string DoubleToString(double v, int digits = 8) {
char buf[64]; snprintf(buf, sizeof buf, "%.*f", digits, v); return string(buf);
}
inline string IntegerToString(long long v) { return std::to_string(v); }
inline double MathAbs(double v) { return std::fabs(v); }
inline double MathMax(double a, double b) { return a > b ? a : b; }
inline double MathMin(double a, double b) { return a < b ? a : b; }
inline int MathRound(double v) { return (int)std::lround(v); }
You should see: nothing — this is a header file. Confirm with ls mql5.h (Windows:
dir mql5.h).
If not: no compiler? On Ubuntu or Debian sudo apt install g++; on macOS
xcode-select --install; on Windows use the Windows Subsystem for Linux or MSYS2.
Go: the same folder.
Do: save this as build.sh. It compiles a .mq5 file and executes the
result. Make it executable with chmod +x build.sh, or invoke it as
sh build.sh.
#!/bin/sh
# build.sh NAME -> compiles NAME.mq5 and runs it.
# MQL5 is close enough to C++ that a C++ compiler can run the subset used here,
# with mql5.h supplying the handful of built-ins MetaTrader would provide.
# Uses whichever compiler you have.
set -e
NAME="$1"
if command -v g++ >/dev/null 2>&1 && g++ -x c++ -std=c++17 -o "$NAME" "$NAME.mq5" 2>/dev/null; then
:
elif command -v clang++ >/dev/null 2>&1; then
clang++ -x c++ -std=c++17 -o "$NAME" "$NAME.mq5"
else
echo "No C++ compiler found. Install g++ (Linux: apt install g++, macOS: xcode-select --install)." >&2
exit 1
fi
"./$NAME"
You should see: nothing — this is the toolchain. ls should show both
mql5.h and build.sh.
If not: Permission denied means the chmod was skipped; use
sh build.sh NAME, which needs no execute bit.
Go: the same folder.
Do: save this as minimal_ea.mq5 and run sh build.sh minimal_ea.
#include "mql5.h"
//+------------------------------------------------------------------+
//| A complete EA, with every guard the previous pages argued for. |
//+------------------------------------------------------------------+
input int FastPeriod = 5;
input int SlowPeriod = 20;
input double RiskPercent = 1.0;
datetime g_last_bar = 0; // outside OnTick: must survive between ticks
int g_positions = 0; // stands in for PositionsTotal()
int g_orders_sent = 0;
int g_closed = 0;
double Sma(const std::vector<double> &c, int i, int period)
{
if(i < period - 1) return 0.0;
double s = 0.0;
for(int k = 0; k < period; k++) s += c[i - k];
return s / period;
}
bool IsNewBar(datetime bar_time)
{
if(bar_time == g_last_bar) return false;
g_last_bar = bar_time;
return true;
}
// Signals are computed on CLOSED bars only: index i is the last closed bar.
bool CrossedUp(const std::vector<double> &c, int i)
{
if(i < SlowPeriod) return false;
double f_now = Sma(c, i, FastPeriod), f_prev = Sma(c, i - 1, FastPeriod);
double s_now = Sma(c, i, SlowPeriod), s_prev = Sma(c, i - 1, SlowPeriod);
return (f_prev <= s_prev && f_now > s_now);
}
bool CrossedDown(const std::vector<double> &c, int i)
{
if(i < SlowPeriod) return false;
double f_now = Sma(c, i, FastPeriod), f_prev = Sma(c, i - 1, FastPeriod);
double s_now = Sma(c, i, SlowPeriod), s_prev = Sma(c, i - 1, SlowPeriod);
return (f_prev >= s_prev && f_now < s_now);
}
void OnTick(const std::vector<double> &closes, int last_closed, datetime bar_time)
{
if(!IsNewBar(bar_time)) return; // once per bar
if(g_positions > 0) // ask the terminal, not memory
{
if(CrossedDown(closes, last_closed))
{
g_positions--;
g_closed++;
Print(" bar ", bar_time, " CLOSE (position #", g_closed, ")");
}
return; // never stack entries
}
if(!CrossedUp(closes, last_closed)) return; // closed-bar signal
g_orders_sent++;
g_positions++;
Print(" bar ", bar_time, " BUY sent (order #", g_orders_sent, ")");
}
void OnStart()
{
std::vector<double> closes;
for(int i = 0; i < 120; i++)
closes.push_back(100.0 + 8.0 * (((i / 25) % 2 == 0) ? (i % 25) : (25 - i % 25)) / 25.0);
int ticks = 0;
for(int bar = SlowPeriod + 1; bar < 119; bar++)
for(int t = 0; t < 6; t++) // six ticks inside each bar
{
ticks++;
OnTick(closes, bar, 1700000000 + bar * 3600);
}
Print("");
Print("ticks processed : ", ticks);
Print("orders sent : ", g_orders_sent);
Print("positions closed: ", g_closed);
Print("still open : ", g_positions);
Print("");
Print("Every guard is doing visible work. Remove IsNewBar and the same");
Print("signal fires six times. Remove the position check and it fires on");
Print("every later signal while one is already open.");
}
int main() { OnStart(); return 0; }
You should see: 588 ticks producing two entries and one exit:
bar 1700208800 BUY sent (order #1)
bar 1700298800 CLOSE (position #1)
bar 1700388800 BUY sent (order #2)
ticks processed : 588
orders sent : 2
positions closed: 1
still open : 1
Every guard is doing visible work. Remove IsNewBar and the same
signal fires six times. Remove the position check and it fires on
every later signal while one is already open.
This is a whole EA in about fifty lines, and every part of it comes from an earlier
page. IsNewBar makes “once per bar” true. The signal reads only closed
bars. The position check asks the state rather than remembering it. And the state that must survive
between ticks lives outside OnTick.
In MetaEditor, three things change. OnTick() takes no arguments
and fetches its own data with CopyClose or iClose. The position count
comes from PositionsTotal() rather than a variable. And the entry line becomes an
OrderSend call with the request structure from step 5.
If not: if no orders are sent, the generated price series is not producing a crossover —
the zig-zag shape depends on the (i / 25) % 2 arithmetic, so keep it as written. If
hundreds are sent, g_last_bar was declared inside a function.
Go: the same folder.
Do: save this as guards.mq5 and run sh build.sh guards.
#include "mql5.h"
//+------------------------------------------------------------------+
//| Remove each guard in turn and count the orders. |
//+------------------------------------------------------------------+
int Run(bool use_newbar, bool use_position_check)
{
// 20 bars, 6 ticks each. The signal is true on bars 5, 6, 7 and 14.
bool signal[20] = {0,0,0,0,0,1,1,1,0,0,0,0,0,0,1,0,0,0,0,0};
datetime last_bar = 0;
int positions = 0, sent = 0;
for(int bar = 0; bar < 20; bar++)
for(int tick = 0; tick < 6; tick++)
{
datetime bar_time = 1700000000 + bar * 3600;
if(use_newbar)
{
if(bar_time == last_bar) continue;
last_bar = bar_time;
}
if(use_position_check && positions > 0) continue;
if(!signal[bar]) continue;
sent++;
positions++;
}
return sent;
}
void OnStart()
{
Print("120 ticks across 20 bars. The signal is genuinely true on 4 bars.");
Print("");
Print("GUARDS IN PLACE ORDERS SENT");
Print("------------------------------------------------------");
Print("both ", Run(true, true));
Print("new-bar only (no position check) ", Run(true, false));
Print("position check only (no new-bar guard) ", Run(false, true));
Print("neither ", Run(false, false));
Print("");
Print("With neither guard, four signals become ", Run(false, false), " orders.");
Print("");
Print("Note the third row. The position check alone still sends only one,");
Print("but only because nothing ever closes in this model -- in a real EA");
Print("with exits, it lets the same signal re-enter as soon as a position");
Print("closes mid-bar. The two guards answer different questions and you");
Print("need both.");
}
int main() { OnStart(); return 0; }
You should see: four genuine signals becoming twenty-four orders:
120 ticks across 20 bars. The signal is genuinely true on 4 bars.
GUARDS IN PLACE ORDERS SENT
------------------------------------------------------
both 1
new-bar only (no position check) 4
position check only (no new-bar guard) 1
neither 24
With neither guard, four signals become 24 orders.
Note the third row. The position check alone still sends only one,
but only because nothing ever closes in this model -- in a real EA
with exits, it lets the same signal re-enter as soon as a position
closes mid-bar. The two guards answer different questions and you
need both.
With no guards, four signals produce twenty-four orders — six per bar, one per tick — and on a live account that is twenty-four positions, twenty-four spreads, and a margin call rather than a strategy.
The third row is the one worth thinking about. The position check alone appears sufficient here, and it is not: this model never closes anything, so the check is doing all the work by accident. In a real EA with exits, a position that closes mid-bar leaves the check satisfied again while the same signal is still true, and the EA re-enters immediately.
Two guards, two different questions. “Is this a new bar?” and “do I already have a position?” are not substitutes for one another, and an EA needs both.
If not: if all four rows show the same number, the boolean arguments are not reaching the
guards — each one is tested with an if(use_...) so that removing it is a
parameter rather than an edit.
Go: the same folder.
Do: save this as request.mq5 and run sh build.sh request.
#include "mql5.h"
//+------------------------------------------------------------------+
//| What OrderSend actually receives, and what it gives back. |
//+------------------------------------------------------------------+
struct MqlTradeRequest
{
string action; // TRADE_ACTION_DEAL for a market order
string symbol;
double volume;
string type; // ORDER_TYPE_BUY / ORDER_TYPE_SELL
double price;
double sl, tp;
int deviation; // slippage tolerance, in points
long magic; // YOUR id, so you can tell your trades from others
string comment;
};
struct MqlTradeResult { int retcode; long order; double price; string comment; };
// Stands in for OrderSend: the first three checks are ones a real server
// applies. The magic-number check is NOT -- a real server accepts magic 0
// happily. It is here because it is a check YOU should apply to yourself,
// and the distinction matters (see the note at the end).
bool OrderSend_stub(const MqlTradeRequest &r, MqlTradeResult &res)
{
if(r.volume <= 0.0) { res.retcode = 10014; res.comment = "invalid volume"; return false; }
if(r.price <= 0.0) { res.retcode = 10015; res.comment = "invalid price"; return false; }
if(r.sl > 0 && r.type == "ORDER_TYPE_BUY" && r.sl >= r.price)
{ res.retcode = 10016; res.comment = "invalid stops"; return false; }
if(r.magic == 0) { res.retcode = -1; res.comment = "OUR OWN check: no magic set"; return false; }
res.retcode = 10009; res.order = 5551234; res.price = r.price; res.comment = "done";
return true;
}
void Try(string label, MqlTradeRequest r)
{
MqlTradeResult res;
bool ok = OrderSend_stub(r, res);
Print(" ", label);
Print(" returned ", ok ? "true " : "false", " retcode ", res.retcode,
" (", res.comment, ")");
}
void OnStart()
{
MqlTradeRequest good;
good.action="TRADE_ACTION_DEAL"; good.symbol="EURUSD"; good.volume=0.10;
good.type="ORDER_TYPE_BUY"; good.price=1.10250; good.sl=1.10050; good.tp=1.10650;
good.deviation=10; good.magic=20260822; good.comment="first EA";
Try("a complete, valid request", good);
MqlTradeRequest zero = good; zero.volume = 0.0;
Try("volume left at 0 (integer division!)", zero);
MqlTradeRequest badsl = good; badsl.sl = 1.10450;
Try("buy with the stop ABOVE the entry", badsl);
MqlTradeRequest nomagic = good; nomagic.magic = 0;
Try("no magic number", nomagic);
Print("");
Print("retcode 10009 is TRADE_RETCODE_DONE. Anything else is a refusal,");
Print("and the RETURN VALUE alone does not tell you which -- always read");
Print("res.retcode, and log it, because 'the EA did not trade' with no");
Print("recorded retcode is undiagnosable a week later.");
Print("");
Print("The last case is different from the other three. A real server");
Print("ACCEPTS magic 0 without complaint -- retcode -1 above is this");
Print("program's own invention, not something MetaTrader returns.");
Print("");
Print("It is here because the magic number is how your EA recognises its");
Print("OWN positions on an account that may also hold manual trades and");
Print("other EAs. Nothing enforces it, and 'close my position' without it");
Print("can close somebody else's. Some rules are the server's; some are");
Print("yours, and only you will ever check them.");
}
int main() { OnStart(); return 0; }
You should see: one accepted request and three refusals:
a complete, valid request
returned true retcode 10009 (done)
volume left at 0 (integer division!)
returned false retcode 10014 (invalid volume)
buy with the stop ABOVE the entry
returned false retcode 10016 (invalid stops)
no magic number
returned false retcode -1 (OUR OWN check: no magic set)
retcode 10009 is TRADE_RETCODE_DONE. Anything else is a refusal,
and the RETURN VALUE alone does not tell you which -- always read
res.retcode, and log it, because 'the EA did not trade' with no
recorded retcode is undiagnosable a week later.
The last case is different from the other three. A real server
ACCEPTS magic 0 without complaint -- retcode -1 above is this
program's own invention, not something MetaTrader returns.
It is here because the magic number is how your EA recognises its
OWN positions on an account that may also hold manual trades and
other EAs. Nothing enforces it, and 'close my position' without it
can close somebody else's. Some rules are the server's; some are
yours, and only you will ever check them.
Reading res.retcode is not optional. The return value
tells you the request was processed; the retcode tells you what happened. 10009 is
TRADE_RETCODE_DONE; everything else is a refusal with a reason, and an EA that logs
“did not trade” without the retcode has thrown away the only evidence.
The second case connects to the syntax page: a volume of zero is what integer division
produces, and this is where it finally surfaces — as 10014 invalid volume,
several layers away from the arithmetic that caused it.
The fourth case is deliberately different, and the program says so. A real server accepts a magic number of zero without complaint; that check is one you impose on yourself. It matters because the magic number is how an EA recognises its own positions on an account that may also hold manual trades and another EA's — and without it, “close my position” can close somebody else's. Some rules are the server's and some are yours, and telling them apart is part of writing trading code.
One more field to notice: deviation is how much slippage you will accept, in
points. Set it to zero and requests are rejected whenever the price moves between your decision and
the server's receipt, which on a fast market is most of the time.
If not: if every case is accepted, the guard conditions in the stub were removed. The
retcode -1 on the last case is this program's own invention and is not a MetaTrader
code — that is stated in the output on purpose.
Without scrolling up: an EA is meant to hold at most one position. Overnight it opened
eleven, all within four minutes, all on the same signal. It does have a check that returns early
when a position is open. What are the two most likely explanations, and how would you separate
them? Answer: the first is that the position check consults the EA's own counter rather than the
terminal, and the counter was reset — a setting change, a recompile or a terminal restart runs
OnDeinit and then OnInit, and every global variable goes back to zero
while the positions themselves carry on existing. The second is that there is no new-bar guard, so
the signal was evaluated on every tick; step 4 showed that turning four signals into twenty-four
orders, and if the position check is asynchronous — the order is sent, and the position does
not appear in PositionsTotal() until the server confirms — several ticks can pass
the check before the first position registers, which produces exactly a burst within a few minutes.
To separate them, look at the log: if the burst follows a restart or a parameter change, it is the
first. If it happens on a fresh signal with no restart nearby, it is the second, and the fix is a
new-bar guard plus tracking the in-flight order rather than only the confirmed position.
Now do it without the page: add a stop-loss and take-profit to minimal_ea.mq5 — not as
numbers, but computed from the entry price and a distance in points — and then add the
position-size function from the functions page so the volume is derived from risk rather than fixed.
That turns the EA from a demonstration into something with the right shape. Then, before it ever
sees a live account, run it in the strategy tester and check one thing: does the trade count change
substantially between “every tick” and “open prices only” modelling? If it
does, one of your guards is not working.
Improvements for Production
This EA is educational. A production EA would also need:
- Dynamic lot sizing based on account balance and risk percentage
- Trailing stop-loss to lock in profits
- Trading session filters (avoid low-liquidity hours)
- News event filters
- Error handling and retry logic for failed orders
- Magic number filtering to only manage its own positions
- Logging for diagnostics and auditing