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.
The price data here is generated by a small stand-in for CopyRates,
because the real one needs a running terminal. The shape of the data and the ordering are
exactly what MetaTrader delivers, which is what the lesson depends on.
Accessing Price Data in MQL5
There are two main ways to access price data in MQL5: individual price functions and the MqlRates structure. Understanding both is essential for building any indicator or EA.
Individual Price Functions
These functions return a single value for a specific bar:
// Get price data for bar at index 'shift' (0 = current bar)
double open = iOpen(_Symbol, PERIOD_CURRENT, 0); // current bar open
double high = iHigh(_Symbol, PERIOD_CURRENT, 0); // current bar high
double low = iLow(_Symbol, PERIOD_CURRENT, 0); // current bar low
double close = iClose(_Symbol, PERIOD_CURRENT, 1); // previous bar close
long vol = iVolume(_Symbol, PERIOD_CURRENT, 0); // current bar volume
datetime t = iTime(_Symbol, PERIOD_CURRENT, 0); // current bar time
The shift parameter counts backward from the current bar: 0 = current (forming) bar, 1 = last completed bar, 2 = the bar before that, etc.
The MqlRates Structure
For bulk data access, CopyRates() fills an array of MqlRates structures. Each element contains all OHLCVT data for one bar:
MqlRates rates[];
ArraySetAsSeries(rates, true); // index 0 = most recent bar
int copied = CopyRates(_Symbol, PERIOD_H1, 0, 100, rates);
if(copied > 0)
{
Print("Latest bar:");
Print(" Open: ", rates[0].open);
Print(" High: ", rates[0].high);
Print(" Low: ", rates[0].low);
Print(" Close: ", rates[0].close);
Print(" Volume:", rates[0].tick_volume);
Print(" Time: ", TimeToString(rates[0].time));
}
By default, MQL5 arrays index from oldest to newest (index 0 = oldest bar). Calling ArraySetAsSeries(array, true) reverses this so index 0 = most recent bar, which is the convention most traders expect.
CopyBuffer — Reading Indicator Values
To read values from any indicator (built-in or custom), use CopyBuffer():
// Step 1: Create an indicator handle (usually in OnInit)
int rsiHandle = iRSI(_Symbol, PERIOD_CURRENT, 14, PRICE_CLOSE);
// Step 2: Copy values into a buffer array
double rsiValues[];
ArraySetAsSeries(rsiValues, true);
int copied = CopyBuffer(rsiHandle, 0, 0, 10, rsiValues);
// Step 3: Use the values
if(copied > 0)
{
Print("Current RSI: ", DoubleToString(rsiValues[0], 2));
Print("Previous RSI: ", DoubleToString(rsiValues[1], 2));
if(rsiValues[0] > 70)
Print("RSI is OVERBOUGHT");
else if(rsiValues[0] < 30)
Print("RSI is OVERSOLD");
}
CopyBuffer Parameters Explained
CopyBuffer(
indicator_handle, // handle from iRSI(), iMA(), etc.
buffer_index, // which buffer (0 = main, 1 = signal, etc.)
start_position, // 0 = most recent bar
count, // how many values to copy
destination_array // array to fill
);
Common buffer indices:
- iMA() — buffer 0: MA values
- iRSI() — buffer 0: RSI values
- iMACD() — buffer 0: MACD line, buffer 1: signal line
- iBands() — buffer 0: base line, buffer 1: upper band, buffer 2: lower band
- iStochastic() — buffer 0: %K line, buffer 1: %D line
Multi-Symbol and Multi-Timeframe Data
You can access data from any symbol and any timeframe, not just the current chart:
// Get daily close of GBPUSD (even if your chart shows EURUSD on H1)
double gbpDaily = iClose("GBPUSD", PERIOD_D1, 1);
// Get H4 RSI for the current symbol
int h4Rsi = iRSI(_Symbol, PERIOD_H4, 14, PRICE_CLOSE);
double h4Values[];
ArraySetAsSeries(h4Values, true);
CopyBuffer(h4Rsi, 0, 0, 3, h4Values);
Detecting New Bars
A common pattern — only run logic once per new bar instead of on every tick:
datetime lastBarTime = 0;
bool IsNewBar()
{
datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0);
if(currentBarTime != lastBarTime)
{
lastBarTime = currentBarTime;
return true;
}
return false;
}
void OnTick()
{
if(!IsNewBar()) return; // skip if no new bar
// This code runs only once per bar
Print("New bar opened at ", TimeToString(TimeCurrent()));
}
Read Price Data Without Reading the Future, in Five Steps
Almost every strategy that works in a backtest and fails on a live account fails for the same reason, and it is not the strategy. It is that the newest bar on a chart is still being built — its close changes with every tick until the bar ends — so a condition tested against it has several different answers during one bar, and the backtest and the live account see different ones. In the next half hour you will see that happen, and write the four-line guard that prevents it. 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 rates.mq5 and run sh build.sh rates.
#include "mql5.h"
//+------------------------------------------------------------------+
//| MqlRates: one bar, six fields. Copying it is how you read a chart.|
//+------------------------------------------------------------------+
struct MqlRates
{
datetime time;
double open, high, low, close;
long tick_volume;
};
// Stands in for CopyRates(): fills `dest` newest-first, returns how many.
int CopyRates_stub(std::vector<MqlRates> &dest, int count)
{
ArrayResize(dest, count);
for(int i = 0; i < count; i++)
{
double base = 1.1000 + (count - 1 - i) * 0.0005;
dest[i].time = 1700000000 + (count - 1 - i) * 3600;
dest[i].open = base;
dest[i].close = base + 0.0003;
dest[i].high = base + 0.0006;
dest[i].low = base - 0.0002;
dest[i].tick_volume = 500 + i * 10;
}
return count;
}
void OnStart()
{
std::vector<MqlRates> rates;
int got = CopyRates_stub(rates, 5);
Print("CopyRates returned : ", got);
if(got <= 0) { Print(" nothing copied -- return and wait"); return; }
Print("");
Print("idx time open high low close volume");
for(int i = 0; i < got; i++)
Print(" ", i, " ", rates[i].time,
" ", DoubleToString(rates[i].open, 4),
" ", DoubleToString(rates[i].high, 4),
" ", DoubleToString(rates[i].low, 4),
" ", DoubleToString(rates[i].close, 4),
" ", rates[i].tick_volume);
Print("");
Print("index 0 is the CURRENT bar. Its time is the LATEST, and each higher");
Print("index steps one bar further into the past.");
}
int main() { OnStart(); return 0; }
You should see: five bars, newest first:
CopyRates returned : 5
idx time open high low close volume
0 1700014400 1.1020 1.1026 1.1018 1.1023 500
1 1700010800 1.1015 1.1021 1.1013 1.1018 510
2 1700007200 1.1010 1.1016 1.1008 1.1013 520
3 1700003600 1.1005 1.1011 1.1003 1.1008 530
4 1700000000 1.1000 1.1006 1.0998 1.1003 540
index 0 is the CURRENT bar. Its time is the LATEST, and each higher
index steps one bar further into the past.
Two things to fix in your mind. First, the return value is checked before anything
else touches the array — the real CopyRates returns -1 when history
is not yet available, which happens on a fresh chart, after a reconnection, and on any symbol you
have not opened before.
Second, the ordering. Index 0 carries the latest time and each higher index steps
further into the past. That is the arrangement ArraySetAsSeries(arr, true) produces,
and it is how nearly all MQL5 examples are written — but it is not automatic, so set it
yourself immediately after every copy.
If not: if the times increase as the index increases, the fill loop is running the wrong way
— the newest bar must be at index 0, which is what the count - 1 - i arithmetic
arranges.
Go: the same folder. This is the most consequential fact on the page.
Do: save this as incomplete.mq5 and run sh build.sh incomplete.
#include "mql5.h"
//+------------------------------------------------------------------+
//| Bar 0 is still being built. Deciding on it is deciding on noise. |
//+------------------------------------------------------------------+
void OnStart()
{
// The same forming bar, sampled four times during the hour.
double forming_close[4] = {1.1020, 1.1031, 1.1014, 1.1027};
double previous_close = 1.1018;
Print("during one hour, bar 0's close changes as ticks arrive:");
for(int t = 0; t < 4; t++)
{
bool above = forming_close[t] > previous_close;
Print(" minute ", t * 15, " close=", DoubleToString(forming_close[t], 4),
" 'above previous close' -> ", above ? "YES" : "no");
}
Print("");
Print("Four different answers to the same question, within one bar.");
Print("An EA testing that condition on every tick will act on whichever");
Print("answer happened when a tick arrived -- and in a backtest, on");
Print("whichever answer the tester happened to model.");
Print("");
Print("Using bar 1 instead -- the last CLOSED bar:");
Print(" close=", DoubleToString(previous_close, 4), " -> one answer, forever");
Print("");
Print("This is the single biggest cause of a strategy that backtests well");
Print("and fails live. It is not the strategy; it is that the backtest and");
Print("the live account see different versions of bar 0.");
Print("");
Print("Rule: signals are calculated on CLOSED bars. Index 1, not 0.");
Print("Detect a new bar by remembering the time of bar 0 and acting only");
Print("when it changes.");
}
int main() { OnStart(); return 0; }
You should see: the same question answered three different ways inside one bar:
during one hour, bar 0's close changes as ticks arrive:
minute 0 close=1.1020 'above previous close' -> YES
minute 15 close=1.1031 'above previous close' -> YES
minute 30 close=1.1014 'above previous close' -> no
minute 45 close=1.1027 'above previous close' -> YES
Four different answers to the same question, within one bar.
An EA testing that condition on every tick will act on whichever
answer happened when a tick arrived -- and in a backtest, on
whichever answer the tester happened to model.
Using bar 1 instead -- the last CLOSED bar:
close=1.1018 -> one answer, forever
This is the single biggest cause of a strategy that backtests well
and fails live. It is not the strategy; it is that the backtest and
the live account see different versions of bar 0.
Rule: signals are calculated on CLOSED bars. Index 1, not 0.
Detect a new bar by remembering the time of bar 0 and acting only
when it changes.
Bar 0's open is fixed the moment the bar starts, and its high and low can only widen — but its close is simply the latest price, and it moves continuously until the bar ends. Any comparison involving it is a comparison against a number that has not settled.
This explains a specific and very common experience. A strategy is described in terms of bars, tested in a tester that models bar 0 one way, and then run live where ticks arrive differently — and it behaves nothing like the test. The strategy was never the problem; the two environments were answering the question at different moments.
The fix is one word: use bar 1. It has closed, its values are final, and every environment agrees about them forever. You give up acting until the bar ends, which is exactly what a strategy described in terms of bars was always claiming to do.
If not: the numbers here are constants and cannot vary; if all four answers agree, the sample
closes were edited so that none of them dips below previous_close, which removes the
effect being shown.
Go: the same folder.
Do: save this as newbar.mq5 and run sh build.sh newbar.
#include "mql5.h"
//+------------------------------------------------------------------+
//| The new-bar guard: the four lines every EA should start with. |
//+------------------------------------------------------------------+
datetime last_bar_time = 0; // must survive between ticks
bool IsNewBar(datetime current_bar_time)
{
if(current_bar_time == last_bar_time) return false;
last_bar_time = current_bar_time;
return true;
}
void OnStart()
{
// Ten ticks. The bar changes twice.
datetime ticks[10] = {1700000000, 1700000000, 1700000000,
1700003600, 1700003600, 1700003600, 1700003600,
1700007200, 1700007200, 1700007200};
int acted = 0;
for(int i = 0; i < 10; i++)
{
if(IsNewBar(ticks[i]))
{
acted++;
Print("tick ", i, " bar time ", ticks[i], " -> NEW BAR, evaluate signal");
}
else
Print("tick ", i, " bar time ", ticks[i], " -> same bar, do nothing");
}
Print("");
Print("ticks processed : 10");
Print("signals evaluated: ", acted);
Print("");
Print("Without this guard the signal is evaluated ten times and can fire");
Print("ten times. With it, once per bar -- which is what a strategy");
Print("described in terms of bars actually means.");
}
int main() { OnStart(); return 0; }
You should see: ten ticks producing three evaluations:
tick 0 bar time 1700000000 -> NEW BAR, evaluate signal
tick 1 bar time 1700000000 -> same bar, do nothing
tick 2 bar time 1700000000 -> same bar, do nothing
tick 3 bar time 1700003600 -> NEW BAR, evaluate signal
tick 4 bar time 1700003600 -> same bar, do nothing
tick 5 bar time 1700003600 -> same bar, do nothing
tick 6 bar time 1700003600 -> same bar, do nothing
tick 7 bar time 1700007200 -> NEW BAR, evaluate signal
tick 8 bar time 1700007200 -> same bar, do nothing
tick 9 bar time 1700007200 -> same bar, do nothing
ticks processed : 10
signals evaluated: 3
Without this guard the signal is evaluated ten times and can fire
ten times. With it, once per bar -- which is what a strategy
described in terms of bars actually means.
Four lines, and they belong at the top of almost every OnTick() you
will write. Note that last_bar_time is declared outside the function —
declared inside it, it would reset on every tick and the guard would never fire, which is the scope
trap from the syntax page appearing again in a new costume.
In a real EA the bar time comes from
iTime(_Symbol, PERIOD_CURRENT, 0), or from rates[0].time after a
successful copy. Either way the logic is identical: remember it, compare it, and only evaluate the
signal when it has changed.
Combine this with using bar 1 and a whole class of problem disappears at once. The signal is calculated once per bar, from values that are final, and the backtest and the live account are finally answering the same question.
If not: if all ten ticks report a new bar, last_bar_time was declared inside
IsNewBar — move it back to file scope. If none do, the initial value was set to
the first tick's time rather than 0.
Without scrolling up: an EA's rule is “buy when the close crosses above the 20-bar moving average”. In the strategy tester it makes 40 trades a year and is profitable; live on the same settings it makes several hundred and loses. The code is identical. Explain the difference and give the two changes that would make live match the test. Answer: the rule is being evaluated on bar 0, whose close moves with every tick. Step 4 showed one bar giving different answers to the same comparison at different moments, so live the condition becomes true, then false, then true again within a single bar, and each time it becomes true the EA enters — hence hundreds of trades instead of dozens. The tester saw fewer because it models a limited number of price points inside each bar, so it simply had fewer opportunities to notice the condition flickering. The two changes are, first, calculate the signal on bar 1, the last closed bar, whose values are final and identical in both environments. Second, add the new-bar guard from step 5 so the signal is evaluated once per bar rather than once per tick. With both in place the trade count stops depending on how many ticks arrive, which is the property that made the test and the live account disagree.
Now do it without the page: combine the two ideas into one file: take newbar.mq5, add a
CopyRates-style call like the one in step 3, and write a signal that compares
rates[1].close with rates[2].close — deliberately never touching
index 0. Then, to feel the difference, change it to use index 0 and 1 and count how many times the
signal fires on the same tick sequence. The gap between those two counts is the gap between your
backtest and your live account.
Practical Example: Find Highest High
// Find the highest high of the last N bars
double FindHighestHigh(int lookback)
{
double highs[];
ArraySetAsSeries(highs, true);
CopyHigh(_Symbol, PERIOD_CURRENT, 0, lookback, highs);
int highestIndex = ArrayMaximum(highs, 0, lookback);
return highs[highestIndex];
}
void OnStart()
{
double highest20 = FindHighestHigh(20);
Print("Highest high of last 20 bars: ", DoubleToString(highest20, _Digits));
}