A C++ compiler and a terminal. No MetaTrader, no broker, no account and no
money. Most Linux machines already have g++; on macOS run
xcode-select --install once; on Windows use the Windows Subsystem for Linux or MSYS2.
Check with g++ --version.
If you have already done the getting-started page you have the two setup files — reuse them and start at step 2. If not, step 1 provides both.
This runs the MQL5 language, not MetaTrader. There are no charts, no prices and no orders, and nothing here can trade. Real Expert Advisors are compiled in MetaEditor and tested on a demo account; the point of running the language here is that a syntax mistake takes five seconds to find instead of a platform switch.
Data Types in MQL5
MQL5 is a strongly typed language — every variable must have a declared type. The most important types for trading:
Integer types:
int count = 10; // 32-bit signed integer (-2B to +2B)
long ticket = 12345678; // 64-bit signed integer (for order tickets)
bool isActive = true; // true or false
char grade = 'A'; // single character
Floating-point types:
double price = 1.23456; // 64-bit double precision (most common for prices)
float ratio = 0.5f; // 32-bit single precision (rarely used)
String type:
string symbol = "EURUSD";
string message = "Trade opened at " + DoubleToString(price, 5);
Datetime type:
datetime now = TimeCurrent(); // current server time
datetime specific = D'2026.03.07 12:00'; // date literal
Color type:
color lineColor = clrRed;
color custom = C'255,128,0'; // RGB notation
In MQL5, all price data (Open, High, Low, Close, Ask, Bid) uses the double type. Never use float for prices — the reduced precision will cause calculation errors.
Variable Declaration and Scope
Variables can be declared at different levels:
// Global variable — accessible everywhere in the file
int globalCounter = 0;
void OnStart()
{
// Local variable — only accessible inside this function
double localPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID);
// Block scope — only accessible inside this if block
if(localPrice > 1.0)
{
string msg = "Price is above 1.0";
Print(msg);
}
// msg is NOT accessible here
}
Constants and Input Parameters
Constants are values that never change:
#define MAX_ORDERS 100 // preprocessor constant
const double PI = 3.14159; // typed constant
Input parameters are special — they create user-configurable settings in the indicator/EA properties dialog:
input int MAPeriod = 14; // Moving Average Period
input double LotSize = 0.1; // Trade Lot Size
input color SignalColor = clrRed; // Signal Arrow Color
When a user attaches your indicator or EA to a chart, they see these parameters in a settings window and can adjust them without modifying code.
Arithmetic Operators
double a = 10.0, b = 3.0;
double sum = a + b; // 13.0
double difference = a - b; // 7.0
double product = a * b; // 30.0
double quotient = a / b; // 3.333...
int remainder = 10 % 3; // 1 (modulo — integers only)
int counter = 5;
counter++; // increment: counter is now 6
counter--; // decrement: counter is now 5
counter += 3; // compound assignment: counter is now 8
Comparison and Logical Operators
// Comparison — return true or false
bool isEqual = (a == b); // equal to
bool isNotEqual = (a != b); // not equal to
bool isGreater = (a > b); // greater than
bool isLessEq = (a <= b); // less than or equal
// Logical — combine conditions
bool both = (a > 5) && (b < 10); // AND: both must be true
bool either = (a > 5) || (b > 10); // OR: at least one true
bool negate = !(a > 5); // NOT: reverses the result
Floating-point arithmetic can produce tiny rounding errors. Instead of if(price == 1.23456), use: if(MathAbs(price - 1.23456) < _Point). The _Point variable holds the smallest price increment for the current symbol.
Control Flow
// if-else
if(rsiValue > 70)
{
Print("Overbought");
}
else if(rsiValue < 30)
{
Print("Oversold");
}
else
{
Print("Neutral");
}
// for loop — iterate through price data
for(int i = 0; i < 100; i++)
{
Print("Bar ", i, " Close: ", iClose(_Symbol, PERIOD_CURRENT, i));
}
// while loop
int attempts = 0;
while(attempts < 3)
{
// try something
attempts++;
}
// switch
switch(OrderType)
{
case ORDER_TYPE_BUY: Print("Buy order"); break;
case ORDER_TYPE_SELL: Print("Sell order"); break;
default: Print("Other order type"); break;
}
Arrays
Arrays are fundamental in MQL5 — indicator buffers, price data, and many built-in functions use arrays:
// Static array — fixed size
double prices[100];
// Dynamic array — resizable
double buffer[];
ArrayResize(buffer, 200);
// Array functions
ArraySetAsSeries(buffer, true); // index 0 = most recent
int size = ArraySize(buffer);
ArrayFree(buffer); // deallocate memory
Predefined Variables
MQL5 provides several predefined variables that are always available:
_Symbol // current chart symbol (e.g., "EURUSD")
_Period // current chart timeframe (e.g., PERIOD_H1)
_Point // smallest price increment (e.g., 0.00001)
_Digits // number of decimal places (e.g., 5)
_LastError // last error code (0 = no error)
Write MQL5 That Behaves, in Five Steps
MQL5's syntax is close enough to C and Java that most of it needs no explanation — braces,
semicolons, if and for all mean what you expect. What does need explaining
is the handful of places where a perfectly ordinary-looking line does something surprising in a
trading program specifically. In the next half hour you will compile and run four of them: choosing
the wrong type for a price, looping through bars in the wrong direction, declaring a counter in a
place that forgets it on every tick, and shipping an EA that accepts whatever a user types. 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. It supplies the built-ins MetaTrader would provide, so
real MQL5 compiles with an ordinary C++ compiler. If you already have it from the getting-started
page, use that copy and skip ahead.
// 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, not a program. Confirm it saved with
ls mql5.h (Windows: dir mql5.h).
This runs the MQL5 language on your own machine. It is not MetaTrader: no charts, no prices, no orders. Real Expert Advisors are still compiled in MetaEditor and tested on a demo account — but the language mistakes below are far quicker to meet here.
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. Check
with g++ --version.
Go: the same folder.
Do: save this as build.sh. It compiles a .mq5 file and executes
the result, using whichever C++ compiler you have. 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. Confirm both files are present with
ls (Windows: dir); you should see mql5.h and
build.sh.
It tries g++ and falls back to clang++, so one command
works on Linux, macOS and inside the Windows Subsystem for Linux. The command underneath is
g++ -x c++ -std=c++17 -o NAME NAME.mq5 — the -x c++ is what makes a
compiler accept a .mq5 file.
If not: Permission denied means the chmod was skipped; use
sh build.sh NAME instead, which needs no execute bit.
Go: the same folder.
Do: save this as choose.mq5 and run sh build.sh choose.
#include "mql5.h"
//+------------------------------------------------------------------+
//| Which type for which job. Getting this wrong is most EA bugs. |
//+------------------------------------------------------------------+
void OnStart()
{
double price = 1.10250; // prices ALWAYS double -- never int
double lot = 0.01; // volumes too
int bars = 500; // counts are whole numbers
int ticket = 123456789; // ids are whole numbers
long big_id = 9007199254740993;
datetime when = 1700000000; // seconds since 1970
string symbol = "EURUSD";
bool allowed = true;
Print("price ", DoubleToString(price, 5), " (double: keeps 5 decimals)");
Print("lot ", DoubleToString(lot, 2), " (double: 0.01 is a real volume)");
Print("bars ", bars, " (int: you cannot have 2.5 bars)");
Print("ticket ", ticket);
Print("big_id ", big_id, " (long: int would overflow)");
Print("when ", when);
Print("symbol ", symbol);
Print("allowed ", allowed ? "true" : "false");
Print("");
// What happens if you pick int for a price:
int as_int = (int)price;
Print("price stored as int : ", as_int, " <- 0.10250 is gone");
Print("");
Print("Rule of thumb: if it can have a fraction, it is a double.");
Print("In trading code that is prices, volumes, money and percentages --");
Print("which is nearly everything except bar counts and array indexes.");
}
int main() { OnStart(); return 0; }
You should see: each value printed with the precision its type allows:
price 1.10250 (double: keeps 5 decimals)
lot 0.01 (double: 0.01 is a real volume)
bars 500 (int: you cannot have 2.5 bars)
ticket 123456789
big_id 9007199254740993 (long: int would overflow)
when 1700000000
symbol EURUSD
allowed true
price stored as int : 1 <- 0.10250 is gone
Rule of thumb: if it can have a fraction, it is a double.
In trading code that is prices, volumes, money and percentages --
which is nearly everything except bar counts and array indexes.
The rule at the bottom is worth committing to memory, because it removes a whole
category of bug: if a value can have a fraction, it is a double. In
trading code that means prices, volumes, money, percentages, stop distances and profit —
nearly everything except bar counts and array indexes.
Two types deserve a note. long exists because order tickets and position
identifiers on some brokers exceed what an int can hold, and an overflowed ticket
number silently refers to the wrong position. And MQL5 has a color type for drawing,
which is a whole number underneath — it is not included in the shim, so use it in MetaEditor
rather than here.
If not: if big_id prints a smaller or negative number, it was declared as
int rather than long — which is precisely the overflow described
above, and worth causing on purpose once.
Go: the same folder.
Do: save this as loops.mq5 and run sh build.sh loops.
#include "mql5.h"
//+------------------------------------------------------------------+
//| Two loop directions, and why only one is right for series arrays. |
//+------------------------------------------------------------------+
void OnStart()
{
// A series array: index 0 is the NEWEST bar (ArraySetAsSeries(arr, true)).
std::vector<double> close = {14.0, 13.0, 12.0, 11.0, 10.0}; // now .. oldest
int total = ArraySize(close);
Print("forward loop (0 upward) -- walks BACK in time:");
for(int i = 0; i < total; i++)
Print(" i=", i, " close=", DoubleToString(close[i], 1),
i == 0 ? " <- the current bar" : "");
Print("");
Print("backward loop (total-1 down to 0) -- walks FORWARD in time:");
for(int i = total - 1; i >= 0; i--)
Print(" i=", i, " close=", DoubleToString(close[i], 1),
i == 0 ? " <- ends on the current bar" : "");
Print("");
Print("Indicators calculate OLDEST first, so they loop DOWNWARD from");
Print("total-1 to 0. Getting this backwards produces an indicator whose");
Print("values depend on bars that had not happened yet -- which is how");
Print("a backtest ends up looking miraculous.");
Print("");
// Every Nth bar, using modulo.
Print("acting only every 3rd bar:");
for(int i = total - 1; i >= 0; i--)
if(i % 3 == 0)
Print(" bar ", i, " -> act");
}
int main() { OnStart(); return 0; }
You should see: the same array walked in both directions:
forward loop (0 upward) -- walks BACK in time:
i=0 close=14.0 <- the current bar
i=1 close=13.0
i=2 close=12.0
i=3 close=11.0
i=4 close=10.0
backward loop (total-1 down to 0) -- walks FORWARD in time:
i=4 close=10.0
i=3 close=11.0
i=2 close=12.0
i=1 close=13.0
i=0 close=14.0 <- ends on the current bar
Indicators calculate OLDEST first, so they loop DOWNWARD from
total-1 to 0. Getting this backwards produces an indicator whose
values depend on bars that had not happened yet -- which is how
a backtest ends up looking miraculous.
acting only every 3rd bar:
bar 3 -> act
bar 0 -> act
In a series array index 0 is the current bar and higher indexes go further into the past. So counting up from zero walks backwards in time, and counting down to zero walks forwards.
Indicators must calculate oldest-first, which means looping downward from
total-1 to 0. That is not a style preference. A calculation that runs the
other way can use a value it has not legitimately computed yet, and the result is an indicator that
appears to predict the market in a backtest and does nothing in live trading — the single
most common cause of a strategy that “worked in testing”.
The modulo block at the end is the idiom for “act only every N bars”, which is how you stop an EA reacting to every tick without keeping a separate counter.
If not: if both loops print in the same order, one of the for headers was copied
twice — the second must start at total - 1, test i >= 0 and
decrement.
Go: the same folder. This one costs people real money.
Do: save this as scope.mq5 and run sh build.sh scope.
#include "mql5.h"
//+------------------------------------------------------------------+
//| The bug that makes an EA open a position on every single tick. |
//+------------------------------------------------------------------+
int global_trades = 0; // survives between ticks
static int static_trades = 0; // same effect, visible only in this file
void OnTick_wrong()
{
int trades_today = 0; // RESET on every tick -- this is the bug
if(trades_today < 1)
{
trades_today++;
Print(" WRONG: opening a trade. trades_today is now ", trades_today);
}
}
void OnTick_right()
{
if(global_trades < 1)
{
global_trades++;
Print(" RIGHT: opening a trade. global_trades is now ", global_trades);
}
else
Print(" RIGHT: already have ", global_trades, " -- doing nothing");
}
void OnStart()
{
Print("five ticks arrive, one after another.");
Print("");
Print("with the counter declared INSIDE the function:");
for(int tick = 1; tick <= 5; tick++)
OnTick_wrong();
Print("");
Print("with the counter declared OUTSIDE it:");
for(int tick = 1; tick <= 5; tick++)
OnTick_right();
Print("");
Print("A variable declared inside a function is created fresh each time");
Print("the function runs. OnTick() runs on every price change -- dozens of");
Print("times a minute -- so anything you want REMEMBERED between ticks must");
Print("live outside it, or be declared static.");
}
int main() { OnStart(); return 0; }
You should see: the same counter reaching 1 five times, then behaving:
five ticks arrive, one after another.
with the counter declared INSIDE the function:
WRONG: opening a trade. trades_today is now 1
WRONG: opening a trade. trades_today is now 1
WRONG: opening a trade. trades_today is now 1
WRONG: opening a trade. trades_today is now 1
WRONG: opening a trade. trades_today is now 1
with the counter declared OUTSIDE it:
RIGHT: opening a trade. global_trades is now 1
RIGHT: already have 1 -- doing nothing
RIGHT: already have 1 -- doing nothing
RIGHT: already have 1 -- doing nothing
RIGHT: already have 1 -- doing nothing
A variable declared inside a function is created fresh each time
the function runs. OnTick() runs on every price change -- dozens of
times a minute -- so anything you want REMEMBERED between ticks must
live outside it, or be declared static.
OnTick() runs on every price change — many times a minute on a
liquid instrument. A variable declared inside it is created fresh each time, so a counter written
that way is always zero when it is tested, the guard always passes, and the EA opens a position on
every tick until the account has no margin left.
Anything that must be remembered between ticks lives outside the function, or
is declared static inside it. That includes trade counters, the time of the last
signal, the bar number you last acted on, and any flag meaning “already done”.
The related habit worth adopting now: rather than counting your own trades, ask the terminal
what positions exist — PositionsTotal() and
PositionSelectByTicket(). A counter can disagree with reality after a restart; the
terminal cannot.
If not: if the “wrong” block prints an increasing number, the variable was declared outside the function — which is the fix, so move it back inside to see the failure it is fixing.
Go: the same folder.
Do: save this as inputs.mq5 and run sh build.sh inputs.
#include "mql5.h"
//+------------------------------------------------------------------+
//| input parameters: what the user types, and why you must check it. |
//+------------------------------------------------------------------+
// In MetaTrader these appear in the EA's settings dialog. The shim maps
// `input` to `static`, so the same declarations compile here.
input double RiskPercent = 2.0;
input int MaPeriod = 20;
input double LotSize = 0.10;
bool ValidateInputs(double risk, int period, double lot)
{
if(risk <= 0.0 || risk > 10.0)
{ Print(" REJECT: RiskPercent ", DoubleToString(risk, 2), " outside 0-10"); return false; }
if(period < 2)
{ Print(" REJECT: MaPeriod ", period, " must be at least 2"); return false; }
if(lot <= 0.0)
{ Print(" REJECT: LotSize ", DoubleToString(lot, 2), " must be positive"); return false; }
Print(" accepted");
return true;
}
void OnStart()
{
Print("the values you shipped as defaults:");
ValidateInputs(RiskPercent, MaPeriod, LotSize);
Print("");
Print("what a user might actually type:");
Print(" risk 200 (meaning 200%, or a typo for 2.00):");
ValidateInputs(200.0, MaPeriod, LotSize);
Print(" period 0 (left blank):");
ValidateInputs(RiskPercent, 0, LotSize);
Print(" lot -0.1 (a stray minus):");
ValidateInputs(RiskPercent, MaPeriod, -0.1);
Print("");
Print("Every one of those is something a person will type. An EA that");
Print("does not check its own inputs will happily compute a position size");
Print("from them -- and MaPeriod 0 divides by zero inside your average.");
Print("");
Print("Validate in OnInit() and return INIT_PARAMETERS_INCORRECT, so the");
Print("EA refuses to start rather than starting and behaving strangely.");
}
int main() { OnStart(); return 0; }
You should see: three plausible entries rejected:
the values you shipped as defaults:
accepted
what a user might actually type:
risk 200 (meaning 200%, or a typo for 2.00):
REJECT: RiskPercent 200.00 outside 0-10
period 0 (left blank):
REJECT: MaPeriod 0 must be at least 2
lot -0.1 (a stray minus):
REJECT: LotSize -0.10 must be positive
Every one of those is something a person will type. An EA that
does not check its own inputs will happily compute a position size
from them -- and MaPeriod 0 divides by zero inside your average.
Validate in OnInit() and return INIT_PARAMETERS_INCORRECT, so the
EA refuses to start rather than starting and behaving strangely.
input variables appear in the EA's settings dialog, which means their
values come from a person, at speed, possibly at two in the morning. All three rejected examples
are ordinary mistakes: 200 typed for 2.00, a field left at zero, a stray minus sign.
The zero is the dangerous one. A moving-average period of zero divides by zero inside the calculation, and depending on where that happens you get an infinity, a not-a-number, or a comparison that is false forever — none of which announces itself.
In a real EA this check belongs in OnInit(), returning
INIT_PARAMETERS_INCORRECT so the terminal refuses to start it and says why. An EA that
starts with impossible settings and then behaves oddly is far harder to diagnose than one that
declines to start at all.
If not: if everything is accepted, the comparison operators were relaxed — the risk check needs both bounds, since a negative risk and a 200% risk are different mistakes and both must fail.
Without scrolling up: an EA is supposed to open at most one position per day. In testing it
opened 340 positions in an hour. The signal logic is correct and the entry condition really was true
only once. What happened, and what two changes would you make? Answer: the guard variable was declared inside OnTick(), so it was recreated as zero on
every price change — step 4 showed exactly this, with a counter reaching 1 five times in a row.
The entry condition being true once is consistent with the symptom: the condition stayed true across
hundreds of ticks, and with no memory between them each tick was treated as the first. The two
changes are, first, move the state out of the function — declare it globally or as
static — so it survives between ticks. Second, and better, stop relying on your own
counter at all: ask the terminal how many positions are open with PositionsTotal()
before entering, since that survives a restart, a reconnection and a manual close, and your counter
does not. Many EAs also add a “one action per bar” guard by remembering the opening time
of the bar they last traded on, which is the idiom the modulo example in step 3 hints at.
Now do it without the page: add a fourth input to inputs.mq5 — a stop-loss
distance in points — and write its validation before you write anything that uses it. Decide
what the minimum should be and why; the honest answer involves the broker's minimum stop level,
which is a real constraint you can only discover at run time. Then go back to
scope.mq5 and convert the global counter to a static local, confirming the
behaviour is identical. Knowing both forms matters, because you will read code that uses each.
Practical Example: Simple Price Analysis
void OnStart()
{
// Get current prices
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double spread = (ask - bid) / _Point;
// Get yesterday's close
double yesterdayClose = iClose(_Symbol, PERIOD_D1, 1);
// Calculate change
double change = bid - yesterdayClose;
double changePct = (change / yesterdayClose) * 100;
// Output
Print("Symbol: ", _Symbol);
Print("Bid: ", DoubleToString(bid, _Digits));
Print("Ask: ", DoubleToString(ask, _Digits));
Print("Spread: ", DoubleToString(spread, 1), " points");
Print("Change from yesterday: ", DoubleToString(changePct, 2), "%");
}