Skip to content

MQL5 Variables, Types & Operators

Data types, variable declarations, arithmetic and comparison operators in MQL5.

💡
Before you start

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
💡
Always use double for prices

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
⚠️
Never compare doubles with ==

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.

1
Set up, if you have not already

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.

2
Save the build script

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.

3
Pick the right type for each job

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.

4
Loop through bars in the direction that cannot cheat

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.

5
Find the bug that opens a trade on every tick

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.

6
Refuse the settings a user will actually type

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.

🎉
Check yourself before moving on

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), "%");
}