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 done an earlier MQL5 page you already have the two setup files — reuse them and start at step 3.
This runs the MQL5 language, not MetaTrader, and nothing here can place a trade. The position-size function in step 5 is real and worth keeping, but it must be tested on a demo account before it decides anything with money attached.
Event-Driven Architecture
MQL5 programs are event-driven. Instead of running from top to bottom like a simple script, indicators and EAs respond to events — a new tick arrives, a new bar forms, the user changes settings, the chart timeframe changes, etc.
You define event handler functions that MT5 calls automatically when each event occurs. This is the core pattern of all MQL5 programming.
Event Handlers by Program Type
Scripts have one event handler:
void OnStart() // Called once when the script is attached to a chart
Indicators have these key handlers:
int OnInit() // Called once when indicator is loaded
int OnCalculate(const int rates_total,
const int prev_calculated,
const datetime &time[],
const double &open[],
const double &high[],
const double &low[],
const double &close[],
const long &tick_volume[],
const long &volume[],
const int &spread[]) // Called on every new tick
void OnDeinit(const int reason) // Called when indicator is removed
Expert Advisors have these key handlers:
int OnInit() // Called once when EA is loaded
void OnTick() // Called on every new tick (price change)
void OnDeinit(const int reason) // Called when EA is removed
void OnTrade() // Called when a trade event occurs
void OnTimer() // Called on timer events
OnInit — Initialization
OnInit() runs once when your program is first attached to a chart, or when the chart timeframe changes, or when input parameters are modified. Use it to:
int OnInit()
{
// Validate input parameters
if(MAPeriod < 1)
{
Print("Error: MA Period must be >= 1");
return INIT_PARAMETERS_INCORRECT;
}
// Create indicator handles
maHandle = iMA(_Symbol, PERIOD_CURRENT, MAPeriod, 0, MODE_SMA, PRICE_CLOSE);
if(maHandle == INVALID_HANDLE)
{
Print("Error creating MA indicator");
return INIT_FAILED;
}
// Set indicator properties (for custom indicators)
SetIndexBuffer(0, mainBuffer, INDICATOR_DATA);
PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_LINE);
PlotIndexSetInteger(0, PLOT_LINE_COLOR, clrDodgerBlue);
return INIT_SUCCEEDED; // must return this on success
}
OnCalculate — The Indicator Engine
This is the heart of every custom indicator. It runs on every new tick and receives the full price data arrays:
int OnCalculate(const int rates_total,
const int prev_calculated,
const datetime &time[],
const double &open[],
const double &high[],
const double &low[],
const double &close[],
const long &tick_volume[],
const long &volume[],
const int &spread[])
{
// rates_total = total number of bars on the chart
// prev_calculated = bars already processed (0 on first call)
// Only process new bars (efficiency optimization)
int start = (prev_calculated == 0) ? MAPeriod : prev_calculated - 1;
for(int i = start; i < rates_total; i++)
{
// Calculate your indicator value for bar i
double sum = 0;
for(int j = 0; j < MAPeriod; j++)
sum += close[i - j];
mainBuffer[i] = sum / MAPeriod;
}
return rates_total; // tell MT5 how many bars we processed
}
This function runs on every tick — potentially dozens of times per second. Only recalculate bars that are new or changed. The prev_calculated parameter tells you where you left off, so you can skip already-processed bars.
OnTick — The EA Engine
OnTick() fires every time a new price quote arrives for the chart symbol. This is where your EA logic lives:
void OnTick()
{
// Get current prices
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
// Read indicator values
double maValues[];
CopyBuffer(maHandle, 0, 0, 3, maValues);
// Check for trade signals
if(bid > maValues[0] && !HasOpenPosition())
{
// Price crossed above MA — open buy
OpenBuy(ask);
}
else if(bid < maValues[0] && HasOpenPosition())
{
// Price crossed below MA — close position
CloseAllPositions();
}
}
OnDeinit — Cleanup
Called when your program is removed from the chart. Use it to release resources:
void OnDeinit(const int reason)
{
// Release indicator handles
IndicatorRelease(maHandle);
// Remove chart objects you created
ObjectsDeleteAll(0, "MyIndicator_");
// Log the reason for removal
Print("Removed. Reason: ", reason);
}
Writing Your Own Functions
Break your code into reusable functions:
// Function with return value
double CalculateLotSize(double riskPercent, double stopLossPips)
{
double accountBalance = AccountInfoDouble(ACCOUNT_BALANCE);
double riskAmount = accountBalance * riskPercent / 100.0;
double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
double lotSize = riskAmount / (stopLossPips * tickValue);
return NormalizeDouble(lotSize, 2);
}
// Function with no return value
void LogTradeInfo(string action, double price, double lots)
{
Print(action, " | Price: ", DoubleToString(price, _Digits),
" | Lots: ", DoubleToString(lots, 2),
" | Time: ", TimeToString(TimeCurrent()));
}
// Usage
void OnTick()
{
double lots = CalculateLotSize(1.0, 50);
LogTradeInfo("BUY", SymbolInfoDouble(_Symbol, SYMBOL_ASK), lots);
}
Write MQL5 Functions the Way the Platform Expects, in Five Steps
MQL5's own functions almost all share one shape: the return value tells you whether the call
worked, and the answer comes back through a parameter marked with &.
CopyClose, OrderSend and SymbolInfoDouble are all built this
way. Once that clicks, a great deal of MQL5 stops being mysterious — and the most common
category of Expert Advisor bug, using data that was never filled in, becomes obvious. In the next
half hour you will compile and run that shape, watch what happens when a return value is ignored,
and write one function properly enough to keep. 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 it saved with ls mql5.h
(Windows: dir mql5.h).
This runs the MQL5 language, not MetaTrader. No charts, no prices, no orders — but a function you can compile and call in five seconds.
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 now 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 byref.mq5 and run sh build.sh byref.
#include "mql5.h"
//+------------------------------------------------------------------+
//| The single most confusing thing about MQL5 functions. |
//+------------------------------------------------------------------+
void TryToChange(double value) // a COPY arrives
{
value = 999.0;
Print(" inside TryToChange, value is now ", DoubleToString(value, 1));
}
void ReallyChange(double &value) // the ORIGINAL arrives
{
value = 999.0;
Print(" inside ReallyChange, value is now ", DoubleToString(value, 1));
}
bool GetPrice(double &out_price) // the MQL5 house style
{
out_price = 1.10250;
return true; // true means "out_price is valid"
}
void OnStart()
{
double price = 1.0;
Print("before : ", DoubleToString(price, 1));
TryToChange(price);
Print("after by value: ", DoubleToString(price, 1), " <- unchanged");
Print("");
price = 1.0;
Print("before : ", DoubleToString(price, 1));
ReallyChange(price);
Print("after by ref : ", DoubleToString(price, 1), " <- changed");
Print("");
double got = 0.0;
if(GetPrice(got))
Print("GetPrice returned true, and out_price is ", DoubleToString(got, 5));
Print("");
Print("MQL5 uses this everywhere: the RETURN value says whether it worked,");
Print("and the answer comes back through a & parameter. CopyClose, OrderSend");
Print("and SymbolInfoDouble all follow that shape -- which is why ignoring a");
Print("return value means using data that was never filled in.");
}
int main() { OnStart(); return 0; }
You should see: the same assignment changing the caller's variable only once:
before : 1.0
inside TryToChange, value is now 999.0
after by value: 1.0 <- unchanged
before : 1.0
inside ReallyChange, value is now 999.0
after by ref : 999.0 <- changed
GetPrice returned true, and out_price is 1.10250
MQL5 uses this everywhere: the RETURN value says whether it worked,
and the answer comes back through a & parameter. CopyClose, OrderSend
and SymbolInfoDouble all follow that shape -- which is why ignoring a
return value means using data that was never filled in.
Without the &, the function receives a copy: it can change the copy
all it likes and the caller never sees it. With the &, the function receives the
caller's variable itself.
This is why MQL5 functions are shaped the way they are. A function that must
report both a result and whether it succeeded has two things to return and only one return slot, so
the platform's convention is: return a bool or a count for success, and hand back the
data through a reference parameter. GetPrice above is the pattern in miniature, and
recognising it makes the reference documentation far easier to read.
One practical note: passing a large array by value copies the whole thing on every
call, which inside OnTick() is real work repeated thousands of times. Arrays are
normally passed by reference for that reason as well as for the ability to fill them.
If not: if both cases print 999.0 afterwards, the & was added to
both signatures — TryToChange must take a plain double for the
contrast to appear.
Go: the same folder. This is where most 'my EA behaves strangely' reports end up.
Do: save this as uninit.mq5 and run sh build.sh uninit.
#include "mql5.h"
//+------------------------------------------------------------------+
//| What happens when a function fails and you use the answer anyway. |
//+------------------------------------------------------------------+
bool CopyPrices(std::vector<double> &dest, int wanted, bool pretend_failure)
{
if(pretend_failure)
return false; // data not ready -- dest untouched
ArrayResize(dest, wanted);
for(int i = 0; i < wanted; i++)
dest[i] = 1.1000 + i * 0.0001;
return true;
}
double AverageOf(const std::vector<double> &v)
{
if(ArraySize(v) == 0) return 0.0;
double s = 0.0;
for(int i = 0; i < ArraySize(v); i++) s += v[i];
return s / ArraySize(v);
}
void OnStart()
{
Print("THE CARELESS VERSION -- return value ignored");
std::vector<double> a;
CopyPrices(a, 5, true); // fails, and nobody looks
Print(" ArraySize(a) : ", ArraySize(a));
Print(" average : ", DoubleToString(AverageOf(a), 5));
Print(" ...and a strategy now compares that 0.00000 with a real price.");
Print("");
Print("THE CHECKED VERSION");
std::vector<double> b;
if(!CopyPrices(b, 5, true))
Print(" copy failed -- returning early, doing nothing this tick");
else
Print(" average: ", DoubleToString(AverageOf(b), 5));
Print("");
Print("AND WHEN IT SUCCEEDS");
std::vector<double> c;
if(CopyPrices(c, 5, false))
Print(" got ", ArraySize(c), " prices, average ",
DoubleToString(AverageOf(c), 5));
Print("");
Print("Price data is genuinely not always available: a fresh chart, a");
Print("reconnect, a symbol whose history is still downloading. The copy");
Print("functions return -1 in those cases, and an EA that does not check");
Print("trades on zeros.");
}
int main() { OnStart(); return 0; }
You should see: an average of zero being computed from data that never arrived:
THE CARELESS VERSION -- return value ignored
ArraySize(a) : 0
average : 0.00000
...and a strategy now compares that 0.00000 with a real price.
THE CHECKED VERSION
copy failed -- returning early, doing nothing this tick
AND WHEN IT SUCCEEDS
got 5 prices, average 1.10020
Price data is genuinely not always available: a fresh chart, a
reconnect, a symbol whose history is still downloading. The copy
functions return -1 in those cases, and an EA that does not check
trades on zeros.
The careless version produces 0.00000 and no error. If a strategy then
asks “is the price above the average?”, the answer is yes — every price is above
zero — so the EA enters a trade on data it never received.
And this is not a hypothetical failure. Price data genuinely is unavailable at
times: the first ticks after a chart opens, after a reconnection, while a symbol's history is still
downloading, or on a symbol you have not opened before. The real
CopyClose/CopyRates functions return -1 in those cases,
which is easy to ignore precisely because it looks like an ordinary number.
The habit: every copy call is wrapped in an if, and the failure branch does
nothing at all — returns from OnTick() and waits for the next one.
Doing nothing is always a valid action for an EA; acting on zeros is not.
If not: if the careless version prints a real average, the pretend_failure
argument was passed as false — it must be true for the first case,
which is what simulates data not being ready.
Go: the same folder.
Do: save this as possize.mq5 and run sh build.sh possize.
#include "mql5.h"
//+------------------------------------------------------------------+
//| One function worth writing correctly once, then reusing forever. |
//+------------------------------------------------------------------+
// Everything a broker would tell you, gathered in one place so the
// arithmetic is visible rather than hidden behind platform calls.
struct SymbolSpec
{
double tick_size; // smallest price change
double tick_value; // money per tick per 1.0 lot
double min_lot;
double max_lot;
double lot_step;
};
double NormaliseLot(double lot, const SymbolSpec &s)
{
lot = MathMax(lot, s.min_lot);
lot = MathMin(lot, s.max_lot);
double steps = MathRound(lot / s.lot_step);
return steps * s.lot_step;
}
// Returns 0.0 when the trade cannot be sized safely -- caller must check.
double PositionSize(double balance, double risk_percent,
double stop_points, const SymbolSpec &s)
{
if(balance <= 0.0 || risk_percent <= 0.0 || stop_points <= 0.0) return 0.0;
if(s.tick_value <= 0.0 || s.tick_size <= 0.0) return 0.0;
double risk_money = balance * risk_percent / 100.0;
double loss_per_lot = stop_points * s.tick_value;
if(loss_per_lot <= 0.0) return 0.0;
return NormaliseLot(risk_money / loss_per_lot, s);
}
void OnStart()
{
SymbolSpec eurusd = {0.00001, 1.0, 0.01, 100.0, 0.01};
Print("balance 10000, tick value 1.00 per point per lot, lot step 0.01");
Print("");
Print("risk% stop(pts) risk money lot");
double cases[4][2] = {{1.0, 200}, {2.0, 200}, {2.0, 50}, {2.0, 1000}};
for(int i = 0; i < 4; i++)
{
double risk = cases[i][0], stop = cases[i][1];
double lot = PositionSize(10000.0, risk, stop, eurusd);
Print(" ", DoubleToString(risk, 1), " ", (int)stop,
" ", DoubleToString(10000.0 * risk / 100.0, 2),
" ", DoubleToString(lot, 2));
}
Print("");
Print("the cases that must return 0.00, not a guess:");
Print(" stop of 0 points : ", DoubleToString(PositionSize(10000, 2, 0, eurusd), 2));
Print(" negative balance : ", DoubleToString(PositionSize(-1, 2, 200, eurusd), 2));
Print("");
Print("Halving the stop distance DOUBLES the lot for the same risk. That");
Print("is the whole point: risk is decided by you, and the lot size is");
Print("whatever makes the stop cost exactly that much.");
}
int main() { OnStart(); return 0; }
You should see: the lot size changing with the stop distance, and zero where sizing is impossible:
balance 10000, tick value 1.00 per point per lot, lot step 0.01
risk% stop(pts) risk money lot
1.0 200 100.00 0.50
2.0 200 200.00 1.00
2.0 50 200.00 4.00
2.0 1000 200.00 0.20
the cases that must return 0.00, not a guess:
stop of 0 points : 0.00
negative balance : 0.00
Halving the stop distance DOUBLES the lot for the same risk. That
is the whole point: risk is decided by you, and the lot size is
whatever makes the stop cost exactly that much.
Read the last two lines of output as the definition of risk management. Risking 2% with a 200-point stop gives 1.00 lots; the same 2% with a 50-point stop gives 4.00. You choose the risk; the lot size is then whatever makes the stop cost exactly that. Sizing the other way round — picking a lot size and accepting whatever the stop costs — is how accounts are lost on a single wider-than-usual trade.
Three details in the code are worth copying into your own version. It returns 0.0
rather than guessing when the inputs make sizing impossible, so the caller must check —
exactly the convention from step 3. It normalises the result to the broker's lot step, because an
order for 0.4763 lots is rejected. And it clamps to the minimum and maximum, because those are real
limits that differ per symbol.
In a real EA the SymbolSpec values come from
SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE) and its siblings, read once in
OnInit(). Hard-coding them is the fourth-most-common cause of an EA that works on one
symbol and misbehaves on another.
If not: if every row prints the same lot, the stop distance is not reaching the calculation
— loss_per_lot must multiply the stop by the tick value. If the small-stop case
prints the maximum lot, the clamp is firing, which is correct behaviour and worth noticing.
Without scrolling up: a colleague's EA calls CopyClose into an array and then
uses the first element, and it works perfectly except for occasional trades at prices that make no
sense, always shortly after they restart the terminal. What is happening, and what would you change? Answer: the copy is failing on the first ticks after a restart, while the terminal is still
fetching history, and the return value is being ignored — step 4 showed exactly this shape,
with the array left empty and the calculation proceeding on zeros. It works “perfectly”
the rest of the time because the data is normally there, which is what makes the bug so hard to
catch: it is invisible except in the one condition nobody tests. The change is to wrap the call in
an if, treat a failure as “do nothing this tick and return”, and never let
the code below it run on an array that was not filled. It is also worth checking the number of
elements actually copied rather than only the success flag, because a partial copy is possible; the
function returns how many it wrote, and asking for 100 bars and getting 3 is a different situation
from getting none.
Now do it without the page: extend possize.mq5 with a second symbol specification — something
with a different tick value and a 0.1 lot step, such as a stock index — and run the same four
cases through it. The lots will differ substantially for identical risk, which is the point.
Then add a fifth guard to PositionSize: refuse to return a lot whose loss at the stop
would exceed the risk you asked for, even after normalising to the lot step. Rounding up to the
nearest step can push you over, and that guard is missing from most published position-size
functions.
Pass by Reference
Use the & operator to pass variables by reference — the function can modify the original variable:
bool GetPriceData(double &high, double &low, double &close, int shift)
{
high = iHigh(_Symbol, PERIOD_CURRENT, shift);
low = iLow(_Symbol, PERIOD_CURRENT, shift);
close = iClose(_Symbol, PERIOD_CURRENT, shift);
return (high != 0 && low != 0 && close != 0);
}
// Usage — variables are filled by the function
double h, l, c;
if(GetPriceData(h, l, c, 1))
Print("Yesterday: H=", h, " L=", l, " C=", c);