Skip to content

Working with Price Data & Arrays

Access OHLCV data, use built-in arrays, and iterate through price history 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 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));
}
⚠️
Always call ArraySetAsSeries before CopyRates

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.

1
Save the header that supplies MetaTrader's built-ins

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.

2
Save the build script

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.

3
Copy some bars and look at what a bar contains

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.

4
Watch the newest bar change its mind

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.

5
Write the guard that makes 'once per bar' true

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.

🎉
Check yourself before moving on

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));
}