
Master MQL5 from beginner to pro (Part IV): About Arrays, Functions and Global Terminal Variables
Introduction
This article is a continuation of the series for beginners. In previous articles, we discussed in detail the methods for describing data stored inside our program. By this point the reader should know the following:
- Data can be stored in variables or constants.
- The MQL5 language is a strongly typed language, which means that each piece of data in a program has its own type, which compiler uses to correctly allocate memory and to avoid logical errors.
- Data types can be simple (basic) and complex (user-defined).
- In order to use data in an MQL5 program, you need to declare at least one function.
- Any blocks of code can be moved to separate files, and then these files can be included in the project using the #include preprocessor directive.
In this article I will cover three global topics:
- Data arrays, which complete the main part about the data inside the program.
- Global terminal variables, allowing the exchange of simple data between different MQL5 programs.
- Some of the feature of functions and their interactions with variables.
Basic information about arrays
An Array is a variable that contains a sequence of same-type data.
To describe an array, you need to describe its type and variable name, and then write square brackets. The number of elements in a given sequence can be specified in square brackets.
int myArray[2]; // Describes an array of integers containing two elements
Example 1. Description of a static array.
We describe sequences in MQL5 programs quite often. These include historical prices, candlestick opening times, volumes, and others. In general, whenever there is a datasets, arrays may be a good choice.
The numbering of elements within an array in MQL5 always starts from 0. Therefore, the number of the last element in the array will always be equal to the number of its elements minus one (lastElement = size - 1).
To access an array element, simply specify the index of that element in square brackets:
// Fill the array with values: myArray[0] = 3; myArray[1] = 315; // Output the last element of this array to the log: Print(myArray[1]); // 315
Example 2. Using array elements.
Naturally, any array can be initialized when it is declared, just like a structure, using curly braces:
double anotherArray[2] = {4.0, 5.2}; // A two-element array is initialized with two values Print( DoubleToString(anotherArray[0],2) ); // Output 4.00
Example 3. Array initialization during description.
Multidimensional Arrays
An array can store other arrays within itself. Such nested arrays are called "multidimensional".
A simple visual example of multidimensional arrays can be book pages. Characters are collected into a line, which is the first dimension, lines are collected into paragraphs and form the second dimension, and a page is a set of paragraphs, i.e. the third dimension.
Figure 1. Characters are collected into a line - a one-dimensional array.
Figure 2. Lines are collected into paragraphs - a two-dimensional array.
Figure 3. Paragraphs are collected into pages - a three-dimensional array.
To describe such arrays in MQL5, simply add square brackets for each new dimension. The brackets for "outer" containers are placed to the left of the "inner" ones. For example, the arrays shown in Figures 1-3 could be described and used as follows:
char stringArray[21]; // One-dimensional array char paragraphArray[2][22]; // Two-dimensional array char pageArray[3][2][22]; // Three-dimensional array // Filling a two-dimensional array paragraphArray[0][0]='T'; paragraphArray[0][1]='h'; // … paragraphArray[1][20]='n'; paragraphArray[1][21]='.'; // Access to an arbitrary element of a two-dimensional array Print(CharToString(paragraphArray[1][3])); // Will print "a" (why?)
Example 4. Description of multidimensional arrays for Figures 1-3.
The total number of dimensions in an array should not exceed 4. The maximum number of elements in any dimension is equal to 2147483647.
Initialization of multidimensional arrays is as simple as that of one-dimensional ones. The curly braces simply list the elements of each array:
int arrayToInitialize [2][5] = { {1,2,3,4,5}, {6,7,8,9,10} }
Example 5. Initialization of multidimensional arrays.
Dynamic arrays
Not for all arrays we can tell right away how many elements they will have. For example, arrays containing terminal history or trade lists will change over time. Therefore, in addition to the static arrays described in the previous sections, MQL5 allows you to create dynamic ones, that is, those that can change the number of their elements during the program's operation. Such arrays are described in exactly the same way as static ones, only the number of elements is not indicated in square brackets:
int dinamicArray [];
Example 6. Description of dynamic arrays
A new array declared in this way contains no elements, its length is 0, and therefore its elements cannot be accessed. If the program tries to do this, a critical error will occur and the program will terminate. Therefore, before working with such an array, it is necessary to set its size using a special built-in function ArrayResize:
ArrayResize(dinamicArray, 1); // The first parameter is the array and the second is the new size ArrayResize(dinamicArray,1, 100); // The third parameter is the reserved (excess) size
Example 7. Resizing a dynamic array
In the language documentation, you can see that the function can take up to three parameters, however the third parameter has a default value, so you can omit it, as I did in the first line of my example.
The first parameter of this function will necessarily be the array that we are changing. The second is the new size of the array. I don't think there will be any problems with this. The third parameter is the "reserved size".
A reserved size is used if we know the ultimate size of our array. For example, according to the conditions of our problem, there cannot be more than 100 values in the array, but how many exactly is unknown. Then you can use the reserve_size parameter in this function and set it to 100, as done in example 7 on the second line. In this case, the function will reserve excess memory size for 100 elements though the actual array size will remain the one specified in the second parameter (1 element).
Why such complications? Why not just add one element at a time when needed?
The simple answer is to speed up our program.
A more detailed answer could take a long time to write. But, in short, the point is that every time we use the ArrayResize function without the third parameter, our program asks the operating system for additional memory. This memory allocation is a rather long (from the processor's point of view) operation, and it does not matter whether the memory is needed for only one element or for many at once. The less often our program has to do this, the better. That is, it is better to reserve a lot of space at once and then fill it, than to allocate a little space and then expand it. However, here too it is necessary to take into account that RAM is a limited resource, and therefore you will always have to find a balance between operating speed and data size.
If you know the limitations of your arrays, it is better to indicate this explicitly to the program by declaring static arrays or reserving memory using the third parameter in the ArrayResize function. If you don't know, then the array is definitely dynamic, and the third parameter in the ArrayResize function does not necessarily need to be specified, although it can, since if the actual size of the array is larger than the reserved one, MQL5 will simply allocate the actual memory needed.
Once the array has been resized, as in example 7, you can change the data inside it:
dinamicArray[0] = 3; // Now our array contains exactly one element (see example 7), its index is 0
Example 8. Using a modified array
When we work with dynamic arrays, most often the task is to add data to the end of this array, and not to change something in the middle (although this also happens). Since at any given moment in time the program does not know how many elements are contained in the array at that particular moment, a special function is needed to find out. It's called ArraySize. The function takes one parameter - an array - and returns an integer value for the number of elements in this array. And once we know the exact size of the dynamic array (which this function will return), adding an element becomes quite simple:
int size, // Number of elements in the array lastElemet; // Index of the last element char stringArray[]; // Our example dynamic array. // Immediately description its size is 0 (array cannot contain elements) // add an element to the end. size = ArraySize(stringArray); // Find the current size of the array size++; // Array size should increase by 1 ArrayResize(stringArray, size, 2); // Resize the array. In our example, the array will have no more than two elements. lastElemet = size — 1; // Numbering starts from 0, so the number of the last element is 1 less than the size of the array stringArray[lastElement] = `H`; // Write the value // Now add one more element. The sequence of actions is absolutely the same. size = ArraySize(stringArray); // Find the current size if the array size++; // Array size should increase by 1 ArrayResize(stringArray, size, 2); // Resize the array. In our example, the array will have no more than two elements. lastElemet = size — 1; // Numbering starts from 0, so the number of the last element is 1 less than the size of the array stringArray[lastElement] = `i`; // Write the value // Note that when adding the second element in this way, only on line changes: // the one that writes the actual value to a specific cell. // // This means that the solution can be written in a shorter form. For example, by creating a separate custom function for it.
Example 9. Adding an element to the end of a dynamic array.
The ArraySize and ArrayResize functions are used constantly when working with dynamic arrays, usually in combination as in example 9. For other functions that are less commonly used but no less useful, please see the documentation.
And, in conclusion of this section, I would like to note that the MQL5 language also supports multidimensional dynamic arrays, however, only the first index can be undefined in it.
int a [][12]; // It's ok // int b [][]; // Compilation error: only the first index can be dynamic
Example 10. Multidimensional dynamic array.
If you really need to make several indexes dynamic, you can create a structure whose only field will be a dynamic array, and then make an array of such structures.
struct DinArray // Structure containing a dynamic array { int a []; }; DinArray dinamicTwoDimensions []; // Dynamic array of structures ArrayResize( dinamicTwoDimensions, 1 ); // Set size of the outer dimension ArrayResize( dinamicTwoDimensions[0].a, 1 ); // Set size of the internal dimension dinamicTwoDimensions[0].a[0] = 12; // Use cell to write data
Example 11. Array with two dynamic indices.
There are other methods for solving this problem. For example, you can create your own class or use one that already exists in the standard library. However, I will leave the topic of working with classes for future articles.
Time Series Arrays
Open, Close, High, and Low prices, tick and real volumes, spreads, candlestick start time and indicator values on each candlestick in MQL5 are referred to as series or time series.
An MQL5 programmer has no direct access to these time series, but the language provides the ability to copy this data into any variables inside our program using a set of special predefined functions (listed in Table 1).
If we need, say, close prices, you first need to create your own array, in which these prices will be stored, and then call the CopyClose function and pass the created array to it as the last parameter. The function will copy the standard time series into our variable, and then this data can be used in the usual way: using indices in square brackets.
However, operations with time series are somewhat different from other arrays. This is now a traditionally settled form.
In memory, the time series are stored in the same way as all the other data: from oldest to newest. But the functions for series data operations from table 1 number elements in time series in the reverse order, from right to left. For all these functions, the zero candlestick will be the rightmost one, the current one, the one that is not yet completed. But "ordinary" arrays do not know about this, and therefore for them this candlestick will be the last one. This is kind of a confusion…
Let's try to understand this using the following images.
Figure 4. Direction of indexing in regular arrays (green arrow) and in time series (blue arrow).
Figure 5. Copying series into regular arrays.
Figure 4 shows the difference in the indexing direction of time series and regular arrays.
Figure 5 visualizes how time series data can be copied into regular arrays, for example, using the CopyRates or similar function (see Table 1). The physical order of elements in memory is the same for regular arrays and for time series, but indexing changes, and the first element in the time series after becomes the last one in the regular array after copying.
Sometimes it can be inconvenient to program while constantly keeping these nuances in mind. There are two ways to combat this inconvenience:
- The standard ArraySetAsSeries function allows you to change the indexing direction for any dynamic array. It takes two parameters: the array itself and the indication of whether it is a time series (true/false). If your algorithm involves copying data that always starts from the last candlestick, most often you can assign the target array as a series, and then the standard functions and your algorithm will use the same indices to work.
- If your algorithm involves copying small data fragments from an arbitrary place on the chart, especially if their number is known exactly at each step of the algorithm (for example, we take the close prices of three bars: the first closed one - that is, with series index 1 - and the two following it, with series indexes 2 and 3), then it is best to accept it as is. It's better to accept that the indexing will be in different directions and just be more careful while programming. One possible solution is to create a separate function to check the required values and try to use it in any expressions.
In the following example I tried to illustrate all of the above with code.
datetime lastBarTime; // We'll try to write the last candle's time into this variable datetime lastTimeValues[]; // The array will store the time of the last two candles. // It's dynamic so that it can be made into a time series to test indices // Get the start time of the current candlestick using the iTime function lastBarTime = iTime ( Symbol(), // Use the current symbol PERIOD_CURRENT, // For the current timeframe 0 // Current candlestick ); Print("Start time of the 0 bar is ", lastBarTime); // Get the start time of the last two candlesticks using the CopyTime function CopyTime ( Symbol(), // Use the current symbol PERIOD_CURRENT, // For the current timeframe 0, // Start with position 0 2, // Take two values lastTimeValues // Write them to array lastTimeValues ("regular") array ); Print("No series"); ArrayPrint(lastTimeValues,_Digits,"; "); // Print the entire array to log. The separator between elements is a semicolon ArraySetAsSeries(lastTimeValues,true); // Convert the array into a time series Print("Series"); ArrayPrint(lastTimeValues,_Digits,"; "); // Print the entire array again. Note the order of the data /* Script output: 2024.08.01 09:43:27.000 PrintArraySeries (EURUSD,H4) Start time of the 0 bar is 2024.08.01 08:00:00 2024.08.01 09:43:27.051 PrintArraySeries (EURUSD,H4) No series 2024.08.01 09:43:27.061 PrintArraySeries (EURUSD,H4) 2024.08.01 04:00:00; 2024.08.01 08:00:00 2024.08.01 09:43:27.061 PrintArraySeries (EURUSD,H4) Series 2024.08.01 09:43:27.061 PrintArraySeries (EURUSD,H4) 2024.08.01 08:00:00; 2024.08.01 04:00:00 */
Example 12. Testing functions for working with time series.
Table 1. List of functions for accessing time series. For all these functions, the indexing of elements begins on the right, from the last (unfinished) candle.
Function | Action |
---|---|
CopyBuffer | Get the data of the specified indicator buffer into an array |
CopyRates | Get historical data for the specified symbol and timeframe into an array of MqlRates structures |
CopySeries | Get several synchronized time series for the specified symbol/timeframe in the specified quantity. The list of all arrays to be filled is passed at the end, and their order must correspond to the fields of the MqlRates structure. |
CopyTime | Get historical data on bar opening times for the corresponding symbol and timeframe into an array |
CopyOpen | Get historical data on bar opening prices for the corresponding symbol and timeframe into an array |
CopyHigh | Get historical data on bar High prices for the corresponding symbol and timeframe into an array |
CopyLow | Get historical data on bar Low prices for the corresponding symbol and timeframe into an array |
CopyClose | Get historical data on bar closing prices for the corresponding symbol and timeframe into an array |
CopyTickVolume | Get historical data on tick volumes for the corresponding symbol and timeframe into an array |
CopyRealVolume | Get historical data on trading volumes for the corresponding symbol and timeframe into an array |
CopySpread | Get historical data on spreads for the corresponding symbol and timeframe into an array |
CopyTicks | Get ticks in the MqlTick format in an array |
CopyTicksRange | Get an array of ticks in the specified date range |
iBarShift | Return the index of the bar in the series that contains the specified time |
iClose | Return the Close price of the bar (indicated by the 'shift' parameter) on the corresponding chart |
iHigh | Return the High price of the bar (indicated by the 'shift' parameter) on the corresponding chart |
iHighest | Return the index of the highest value found on the corresponding chart (shift relative to the current bar) |
iLow | Return the Low price of the bar (indicated by the 'shift' parameter) on the corresponding chart |
iLowest | Return the index of the smallest value found on the corresponding chart (shift relative to the current bar) |
iOpen | Return the Open price of the bar (indicated by the 'shift' parameter) on the corresponding chart |
iTime | Return the opening time of the bar (indicated by the 'shift' parameter) on the corresponding chart |
iTickVolume | Return the tick volume of the bar (indicated by the 'shift' parameter) on the corresponding chart |
iRealVolume | Return the real volume of the bar (indicated by the 'shift' parameter) on the corresponding chart |
iSpread | Return the spread value for the bar specified by the 'shift' parameter on the corresponding chart |
Creating Functions (in detail)
Any function in an MQL5 program is created using the same template, which we briefly discussed in the first article of the series:
ResultType Function_Name(TypeOfParameter1 nameOfParameter1, TypeOfParameter2 nameOfParameter2 …) { // Description of the result variable and other local variables ResultType result; // … //--- // Main actions are performed here //--- return resut; }
Example 13. Function description template.
ResultType (and TypeOfParameter) represent any permissible data type. This could be an int, a double, a class or enumeration name, or anything else you know.
There can also be no parameters or no explicit result of function operation. Then, instead of the result type or instead of the parameter list inside the parentheses the word 'void' is inserted. It stands for an empty result.
Obviously, if the ResultType is void, then there is no need to return data, and accordingly, the last line inside the curly brackets (return result) does not need to be specified, and it is also not necessary to describe the result variable.
Here are a few more simple rules:
- Function_Name and parameter names must comply with identifier conventions.
- The return operator returns only one value. It cannot return more. But there are workarounds, which we will discuss a little later.
- A function cannot be described inside another function, only outside of all functions.
- You can describe multiple functions with the same name, but with a different number (or different types) of parameters and/or different types of the return value. Just make suer you can understand exactly which function should be used in a specific case. If you can distinguish and explain these differences to someone unfamiliar with your code, then so can the compiler.
Here are some examples illustrating how functions can be described:
//+------------------------------------------------------------------+ //| Example 1 | //| Comments are often used to describe what a function does, | //| what date it needs and why. For example, like this: | | //| | //| | //| The function returns difference between two integers. | //| | //| Parameters: | //| int a is a minuend | //| int b is a subtrahend | //| Return value: | //| difference between a and b | //+------------------------------------------------------------------+ int diff(int a, int b) { // The action is very simple, we do not create a variable for the result. return (a-b); } //+------------------------------------------------------------------+ //| Example 1a | //| The function returns the difference between two real numbers. | //| | //| Function name is as in the previous example, but parameter type | //| differs | //| | //| Parameters: | //| double a is a minuendе | //| double b is a subtrahend | //| Return value: | //| difference between a and b | //+------------------------------------------------------------------+ double diff(double a, double b) { return (a-b); } //+------------------------------------------------------------------+ //| Example 2 | //| Illustrates the use of "void". | //| Calls (uses) the diff function | //+------------------------------------------------------------------+ void test() { // You can do whatever you want. // For example, use the function from Example 1. Print(diff(3,4)); // the result is -1 // Since when calling the diff function, integer parameters were // passed in parentheses, the result is also int. // Now let's try to call the same function with double precision parameters Print(diff(3.0,4.0)); // the result is -1.0 // Since the function is declared as "void", the "return" statement is not needed } //+------------------------------------------------------------------+ //| Example 3 | //| The function has no parameters to process. We could use | //| empty parentheses as in the previous example or explicitly use | //| the word "void" | //| Return value: | //| string nameForReturn is some name , always the same | | //+------------------------------------------------------------------+ string name(void) { string nameForReturn="Needed Name"; return nameForReturn; }
Example 14. Examples of function descriptions by template.
It is important to understand that working with a function consists of two stages: description and use. When we describe a function, this function does not yet do anything. It's just a formal algorithm of actions. For example, the diff function from Example 14 could be described in words as follows:
- Take two integers (any, unknown in advance).
- Inside the algorithm, denote one of them as a and the other one as b.
- Subtract b from a.
- Give the result of the computation (any, unknown in advance) to the caller (return to call point).
Where we speak of "any, unknown in advance" value, this expression can be replaced by a "formal parameter". Functions are created to formally perform certain actions with any data.
Parameters that are used when describing functions are referred to as "formal".
In example 14, the diff function contains two formal parameters, the others have none. In general, a function can have many formal parameters (up to 63).
But in order to get a specific result, the function must be call (that is, used). See the function test in example 14, where it called the Print and diff functions. Here, the function uses very specific values which are actual at the moment of the call: the contents of variables or constants, literals (as in my example), the results of other functions.
The parameters we pass to the function at the call time are referred to as "actual".
To call any function, you need to specify its name and list actual parameters in parentheses. Actual parameters should correspond to formal parameters in terms of type and quantity.
In example 14, the 'test' function uses exactly two integers or exactly two doubles to call the 'diff' function. If I made a mistake and tried to write one or three parameters, I would get a compilation error.
Variable Scope
When declaring variables, you need to consider where exactly they are declared.
-
If the variable is declared inside a function (including the formal parameters of this function), other functions will not see this variable (and therefore will not be able to use it). Typically, such a variable is "born" at the moment the function is called and "dies" when the function finishes its work. Such a variable is referred to as local.
In general, we can say that the scope of a variable is determined by the entire "entity" in our code. For example, if a variable is declared inside curly braces, it will only be visible inside those braces that form the block, but not outside that block. Formal parameters of a function belong to the "entity" of the function, so they will be visible only within that function. And so on. The lifetime of such a variable is equal to the lifetime of the "entity" to which it belongs. For example, a variable declared inside a function is created when that function is called and is destroyed when the function terminates.
void OnStart() { //--- Local variable inside a function is visible to all blocks of that function, but not beyond it int myString = "This is local string"; // Curly braces describe a block inside a function { int k=4; // Block local variable - visible only inside curly braces Print(k); // It's ok Print (myString); } // Print(k); // Compilation error. The variable k does not exist outside the curly brace block. }
Example 15. Local variable inside curly brace block
- If a variable is declared outside the description of any functions, it can be used bu all functions of our application. In this case, the lifetime of such a variable is equal to the lifetime of the program. Such a variable is referred to as global.
int globalVariable = 345; void OnStart() { //--- Print (globalVariable); // It's ok }
Example 16. A global variable is visible to all functions in our program.
- A local variable can have the same name as a global variable, but in this case the local variable hides the global variable for the given function.
int globalVariable=5; void OnStart() { int globalVariable=10; // The variable is described according to all the rules, including the type. // If the type were not declared, this expression would change the global variable //--- Print(globalVariable); // The result is 10 - that is, the value of the local variable Print(::globalVariable); // The result is 5. To print the value of a global variable, not the local one, // we use two colons before the name }
Example 17. Matching names of local and global variables. The local variable hides the global one.
Static Variables
There is a special case in the description of local variable.
As mentioned above, local variables lose their values after the function completes. This is usually exactly the behavior that is expected. However, there are situations when you need to save the value of a local variable even after the function has completed its execution.
For example, it is sometimes necessary to keep a counter of calls to a function. Another task, which is more common for traders, is to organize a function for checking the start of a new candlestick. This requires retrieving the current time value at each tick and comparing it with the previously known value. You can, of course, create a global variable for each of these counters. But each global variable increases the probability of an error, since these counters are used by one function, while other functions should not change and even see them.
In such cases, when a local variable should live as long as a global one, static variables are used. They are described just as ordinary ones, only the word static is added before the description. This use is shown in the HowManyCalls function in the following example:
//+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- HowManyCalls(); HowManyCalls(); HowManyCalls(); } //+------------------------------------------------------------------+ //| The function counts the number of requests | //+------------------------------------------------------------------+ void HowManyCalls() { //--- Variable description. The variable is local, but its lifetime is long. static int counter=0; // Since 'static' keyword is used, the variable is initialized only // before he first function call // (more precisely, before the OnInit function call) //--- Main actions counter++; // During program execution, the value will be stored till the end //--- Operation result Print( IntegerToString(counter)+" calls"); } // Script output: // 1 calls // 2 calls // 3 calls
Example 18. Using a static variable.
The example contains two functions: HowManyCalls, which uses a static variable to count the number of calls to it and prints the result to the log, and OnStart, which calls HowManyCalls three times in a row.
Passing Function Parameters by Value and by Reference
By default, a function uses only data copies, which are passed to it as parameters (programmers say that in this case the data is transferred "by value"). Therefore, even if something is written into a variable-parameter inside the function, nothing will happen to source data.
If we want source data to be changed inside the function, the changeable formal parameter must be designated with a special icon & (ampersand). This method of describing parameters is referred to as passing by reference.
To illustrate how a function can modify external data, let's create a new script file containing the following code:
//+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart(void) { //--- Declare and initialize two local variables int first = 3; int second = 77; //--- Print their values BEFORE all changes Print("Before swap: first = " + first + " second = " + second); //--- Use the Swap function, which takes data by reference Swap(first,second); //--- See what happened Print("After swap: first = " + first + " second = " + second); //--- //--- Apply the CheckLocal function to the received data //--- This function takes parameters by value CheckLocal(first,second); //--- Print the result again Print("After CheckLocal: first = " + first + " second = " + second); } //+------------------------------------------------------------------+ //| Swaps the values of two integer variables | //| Data is passed by reference, so the originals will be modified | //+------------------------------------------------------------------+ void Swap(int &a, int& b) // It can be done in any way, both positions are correct { int temp; //--- temp = a; a = b; b = temp; } //+------------------------------------------------------------------+ //| Takes parameters by value, that is why changes happen | //| only locally | //+------------------------------------------------------------------+ void CheckLocal(int a, int b) { a = 5; b = 10; } // Script output: // Before swap: first = 3 second = 77 // After swap: first = 77 second = 3 // After CheckLocal: first = 77 second = 3
Example 19. pass parameters by reference.
This code defines three short functions: OnStart, Swap, and CheckLocal.
CheckLocal takes data by value and therefore works with copies, Swap takes two parameters by reference, therefore, works with the source data. The OnStart function declares two local variables, then prints the value of those variables, calls the Swap and CheckLocal functions, and shows the results of the interaction by printing the value of its local variables to the log after each interaction. Once again, I draw your attention to the fact that the Swap function ha changed the data passed to it, but CheckLocal was unable to do this.
It is important to note that all variables of complex types (such as enumerations, structures, objects, etc., as well as any arrays) must always be passed by reference.
When attempting to pass such variables by value, the compiler will generate an error.
And once again I will briefly list the basic rules of interaction of variables and functions:
- Global variables in the MQL5 language can be directly used from within any function, including changing their values.
- Local variables are accessible only within the block where they are declared.
- If a formal parameter describes data transfer "by value", the function cannot change the original data, even if it changes the value of the parameter variable internally. But data passed "by reference" may change in its original location.
- If a global variable and a local variable have the same name, the local variable takes precedence (in other words, the local variable overrides the global one).
- The lifetime of global variables is equal to the lifetime of the program, and local variables are equal to the lifetime of the block where they are described.
Default values for formal function parameters
Formal parameters can be assigned default values.
For example, if we create a logging function, we may need to distinguish the function's messages from all other terminal messages. The easiest way to do this is to add a prefix to the beginning of the original message and a suffix to the end. The string itself must always be specified, otherwise the meaning of the function is lost. The "additions" can be standard or changed.
The simplest code to illustrate this idea is given below:
//+------------------------------------------------------------------+ //| Add a prefix and suffix to a sting | //+------------------------------------------------------------------+ string MakeMessage( string mainString, string prefix="=== ", string suffix=" ===" ) { return (prefix + mainString + suffix); }
Example 20. Description of a function with default formal parameters
When calling this function, one or both parameters that have default values can be omitted. If these parameters are not specified explicitly, the function will use the values specified in the description. For example:
Print ( MakeMessage("My first string") ); // Default prefix and suffix Print ( MakeMessage("My second string", "~~ ") ); // Prefix changed, suffix remains unchanged Print ( MakeMessage("My third string", "~~ ", " ~~") ); // Both actual parameter have been changed // Script output: // === My first string === // ~~ My first string === // ~~ My first string ~~
Example 21. Actual parameters that have default values can be omitted
Parameters with default values can only be consecutive and must be described after all other parameters that do not have such values.
How to make a function return multiple results
As mentioned above, the return operator can return only one result. Moreover, the function cannot return arrays. But what if you really need more return values? What if, for example, you need to use one function to calculate both the candlestick close time and price or get a list of available instruments? First, please try to find a solution on your own, and then compare with what is written below.
To solve the problem stated in the title, you can use one of the following methods:
- Create a complex data type (for example, a structure) and return a variable of that type.
- Use parameter passing by reference. This, of course, will not help you return this data using the 'return' statement, but it will allow you to write down any values and then use them.
- Use global variables (not recommended). This method is similar to the previous one, but is potentially more dangerous for the code. It is better to use global variables to a minimum, only where it is absolutely impossible to do without them. But if you really need to, you can try to do this.
Global Variable Modifiers: input and extern
There are also "special cases" when using global variables. These include:
- Describing program input parameters using the 'input' modifier
- Using the 'extern' modifier
Each input parameter of a program written in MQL5 is described as a global variable (outside all functions) and is designated by the input keyword, which is placed at the beginning of the description.
input string smart = "The smartest decision"; // The window will contain this description
Example 22. Description of input parameters
Typically, the left column of the properties window displays the variable name. However, if the same line, where this variable is described, contains a comment, as in example 22, this comment will be displayed instead of the variable name.
In programs written in MQL5, variables marked as input, can be accessed as read only, while you can't write anything in them. The values of these variables can only be set in the description (in the code) or from the program properties dialog box.
If you are creating an Expert Advisor or indicator, you can typically optimize the values of such variables using a strategy tester. However, if you want to exclude some parameters from optimization, at the beginning of the word input you need to add the letter s or the modifier 'static':
input double price =1.0456; // optimize sinput int points =15; // NOT optimize static input int unoptimizedVariable =100; // NOT optimize
Example 23. Using the 'sinput' modifier to exclude a variable from optimization in the tester
If you want the user to select values from a list in the input field, you need to add an enumeration for each such field. Inline comments for enumeration elements also work, so instead of names like POINT_PRICE_CLOSE you can display "Point Close Price" in any human language. Unfortunately, there is no easy way to select the text language for the field name (comments). For each language you use, you will have to compile a separate file, which is why most experienced programmers prefer to use the universal (English) language.
Parameters can be visually grouped to make them easier to use. To specify a group name, a special description is used:
input group "Group Name"
Example 24. Parameter group header
Here is a complete example illustrating all these possibilities:
#property script_show_inputs // Enumeration. Allows you to create a list box. enum ENUM_DIRECTION { // All inline comments next to lines describing parameters, // will be displayed instead of the names in the parameters window DIRECTION_UP = 1, // Up DIRECTION_DN = -1, // Down DIRECTION_FL = 0 // Unknown }; input group "Will be optimized" input int onlyExampleName = 10; input ENUM_DIRECTION direction = DIRECTION_FL; // Possible directions list input group "Will not be optimized" sinput string something = "Something good"; static input double doNotOptimizedMagickVariable = 1.618; // Some magic //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- } //+------------------------------------------------------------------+
Example 25. Different options for describing input parameters
Figure 6. Parameters dialog. Groups (input group) are highlighted in color. Green arrows indicate the values of comments substituted in place of variable names.
Figure 6 shows the options dialog that was generated from the code in Example 25. It may look slightly different on different computers, but in any case, the group headers will be highlighted (I highlighted them in blue in the picture). You can also see that parameters that don't have inline comments will use variable names. If there are comments, the compiler uses them instead of variable names, as in my cells indicated by the green arrows. Compare the code from Example 25 with the picture; I hope its helps you understand everything.
And one more thing. Not all beginners notice the icons on the left side of the data type of each parameter. For example, the parameter called "Possible directions list" in the figure has a data type of enumeration, and its icon ( ) suggests that it's a list. Data for this field can only be selected from a limited enumeration. The rest of the icons are also self-explanatory.
The name of any parameter should not be longer than 63 characters (which is a lot, since real names are usually much shorter).
String parameter length cannot exceed 254 characters. Also, the longer the name of the parameter, the shorter the content, since they are stored in memory as one continuous line.
It is important to remember this limitation, especially if you are specifying the address of some web page that is important for the program. Sometimes addresses are really long, and if this is your case, try passing the address in some other way, for example, hard-code it as a global variable, but not a parameter. Of course, there are better solutions, such as using files or "gluing" an address from several fragments, just remember the limit of 254 characters for parameter values.
The second special case is 'extern' variables.
When developers write a large program divided into several files, there are cases when a global variable is described in one file, and the program needs to access it from other files. And we don't want to include files using the #include directive. MetaEditor perceives each file separately and, therefore, in this case, cannot help.
Most often, this situation arises when using input parameters (which are described in the previous subsection).
This is where the 'extern' keyword can be used.
extern bool testComplete;
Example 26. Description of external variables
Such variables may not be initialized in this file, and when compiled, the memory address of this variable will most likely be replaced by a "real" global variable with the same name, if the compiler can find one. However, functions can freely access this "formal" data, including modifying it, and the IDE will not have any difficulties with auto-substitution.
Terminal global variables
Both local and global variables described in the previous sections are accessible only to the current program. All other programs cannot use this data. But there are situations when programs need to exchange data with each other, or it is necessary to ensure that the values of variables are saved even after the terminal is turned off.
An example of data exchange case can be a very simple indicator, in which you need to output the amount of funds in the deposit currency required to open a position. It seems that everything is simple. After searching through the help table of contents, we find out that MQL5 has a special function OrderCalcMargin, which calculates the required amount. We try to apply it, and we get... disappointed. This is because you can't use trading functions in indicators. This is prohibited physically, at the compiler level. OrderCalcMargin is a trading function.
Therefore, we will have to find workarounds. One option is to write a script or service that will calculate the required amounts and then write these amounts into terminal variables. And then our indicator will read this data, not calculate it. This trick is possible because, unlike indicators, scripts and services are allowed to trade (see Table in the first article in the series).
Let's see how such data exchange could be implemented. First, let's create a script file using the wizard. Let's name this file "CalculateMargin.mq5".
There is a whole set of predefined functions for accessing terminal variables, whose names begin with the prefix GlobalVariable.
Using these and the OrderCalcMargin function to make the required data available to the indicators, we will create a new script:
//+------------------------------------------------------------------+ //| CalculateMargin.mq5 | //| Oleg Fedorov (aka certain) | //| mailto:coder.fedorov@gmail.com | //+------------------------------------------------------------------+ #property copyright "Oleg Fedorov (aka certain)" #property link "mailto:coder.fedorov@gmail.com" #property version "1.00" #property script_show_inputs //--- Script input parameters input double requiredAmount = 1; // Number of lots //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- Description of local variables string symbolName = Symbol(); // Name of the current symbol string terminalVariableName; // Terminal variable name double marginBuy, marginSell; // Margin values (buy and sell) double currentPrice = iClose(symbolName,PERIOD_CURRENT,0); // Current price to calculate margin bool okCalcBuy, okCalcSell; // Indication of success when calculating margin up or down //--- Main operations // Calculate Buy margin okCalcBuy = OrderCalcMargin( ORDER_TYPE_BUY, // Order type symbolName, // Symbol name requiredAmount, // Required volume in lots currentPrice, // Order open price marginBuy // Result (by reference) ); // Calculate Sell margin okCalcSell = OrderCalcMargin( ORDER_TYPE_SELL, // Sometimes different amounts are needed for opening up and down symbolName, requiredAmount, currentPrice, marginSell ); //--- Operation result // Create a terminal variable name for Buy details terminalVariableName = symbolName + "BuyAmount"; // Write the data. If the global terminal variable does not exist, it will be created. GlobalVariableSet ( terminalVariableName, // Where to write marginBuy // What to write ); // Now we create another name - for the Sell details terminalVariableName = symbolName + "SellAmount"; // Write data for Sell. If there was no variable with the name stored in terminalVariableName, // create one GlobalVariableSet(terminalVariableName,marginSell); } //+------------------------------------------------------------------+
Example 31. Script for calculating funds in the deposit currency required to buy or sell 1 lot and saving this data in the terminal's global variables
Here we used the standard GlobalVariableSet function to write data to terminal variables. I think in the given example the use of these functions is obvious. Additional note: name length for the global terminal variable must not exceed 63 characters.
If you run this script on any chart, you won't see any obvious results right away. However, you can see what happened using the <F3> key or by selecting "Tools -> Global Variables" from the terminal menu.
Figure 7. Terminal variables menu.
After selecting this menu item, a window will appear with a list of all terminal variables:
Figure 8. Window with a list of global terminal variables
In Figure 8 you can see that I only ran the script on the EURUSD pair, so only two variables are visible: the amounts to buy and sell, which in this case are the same.
Now let's create an indicator, which will use this data, and at the same time we will see how standard functions use the principles of working with variables discussed above.
Let's name this file "GlobalVars.mq5". The main operations in this indicator will be performed inside the OnInit function, which is executed once immediately after the program starts. We also add the OnDeinit function, which removes comments when we remove the indicator from the chart. The OnCalculate function, which is mandatory for each indicator and is executed on each tick, is also present in this indicator but is not used.
//+------------------------------------------------------------------+ //| GlobalVars.mq5 | //| Oleg Fedorov (aka certain) | //| mailto:coder.fedorov@gmail.com | //+------------------------------------------------------------------+ #property copyright "Oleg Fedorov (aka certain)" #property link "mailto:coder.fedorov@gmail.com" #property version "1.00" #property indicator_chart_window //+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Description of local variables string symbolName = Symbol(); // Symbol name string terminalVariableName; // Name of global terminal value double buyMarginValue, sellMarginValue; // Buy and Sell value bool okCalcBuy; // Indication that everything is OK when calling one of the variants of the GlobalVariableGet function //--- Main operations // Create a terminal variable name for Buy details terminalVariableName = symbolName + "BuyAmount"; // Use the first method to get the value of a global variable. // To get the result, the parameter is passed by reference okCalcBuy = GlobalVariableGet(terminalVariableName, buyMarginValue); // Change the name of the terminal variable - for Sell details terminalVariableName = symbolName + "SellAmount"; // Second way to get the result: return value sellMarginValue = GlobalVariableGet(terminalVariableName); //--- Output the result as a comment on the chart Comment( "Buy margin is " + DoubleToString(buyMarginValue) // Buy margin value, the second parameter // of the DoubleToString function is omitted +"\n" // Line break +"Sell margin is " + DoubleToString(sellMarginValue,2) // Margin value for sale, indicated the number of // decimal places ); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| The function will be called when the program terminates. | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { // Очищаем комментарии Comment(""); } //+------------------------------------------------------------------+ //| Custom indicator iteration function (not used here) | //+------------------------------------------------------------------+ 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[]) { //--- //--- return value of prev_calculated for the next call return(rates_total); } //+------------------------------------------------------------------+
Example 32. Using global terminal variables in the indicator.
In this demo indicator we need to read the terminal variables once and finish operation. Therefore, the main code is placed in the OnInit function. The example may seem large and scary, but in fact it is very easy to read, especially since most of it is comments. Let me describe again in words what happens in the OnInit function:
- In the first block of this function we declare all variables, which we will need later.
- Then we create a name for the global terminal variable.
- We read the value of the required global terminal variable into the corresponding local variable.
- Then we generate the name of the second variable and read its value in the next line.
- And the last action is to output a message for the user in the form of a comment in the upper left corner (see Figure 9).
Please note that the GlobalVariableGet function has two call options: using the return value or a parameter passed by reference, and the DoubleToString function has a parameter with a default value. If you type the example text in the editor to check the code functionality, rather than copying it via the clipboard, MetaEditor will prompt you for these nuances.
Figure 9. Result of the indicator operation
Because I used different ways of calling the DoubleToString function to generate the output, the comments at the top and bottom lines look slightly different. When formatting a message for the top line, I omitted the second parameter of the DoubleToString function. This parameter must specify the number of characters after the decimal point, and by default it is 8. For the bottom line, I specified this value explicitly and instructed the program to output two characters.
Please note that the indicator must be launched on the chart where the script was applied, and only after the script - to make sure that the global variables of the terminal exist when it is running. Otherwise, an error will occur when the indicator is running and the comments will not appear.
Thus, the GlobalVariableSet function is used to write terminal variables, and GlobalVariableGet is used to read them. These functions are the most frequently used by programmers, but the others are also useful, so I recommend reading at least their list in the language documentation (link at the beginning of the section).
Conclusion
Let's go over the list of topics covered today again. If any point in the list remains unclear to you, please return to the corresponding place in the article and read it once again, since this material is the basis for the rest of the work (well, maybe with the exception of global terminal variables, you can often do without them - but understanding them usually does not cause difficulties). So, in this article we talked about:
- Arrays:
- Can be static and dynamic
- Can be one-dimensional and multidimensional
- Static arrays can be initialized using literals (in curly braces)
- To work with dynamic arrays, you need to use standard functions for changing sizes and finding out the current size
- Variables in relation to functions can be
- Local (short-lived except for static); the static modifier can be added to them
- Global (long-lived); you can add extern and input modifiers to them
- Function parameters can be passed
- by reference
- by value
- There are global terminal variables. Unlike simply global program variables, they can be used to exchange data between different programs. There is a special set of functions for their use.
If you remember all of this and none of the items on the list confused you, then you can no longer be considered a newbie: you already have a good base to work on. All that remains is to figure out how to use the basic operators, as well as understand what features there are in the MQL5 language when writing indicators and experts in relation to other languages - and you can start writing useful programs. However, to reach the "professional" level, you will need to understand a dozen more topics, including object-oriented programming, but all these topics will still, one way or another, rely on the foundation, which is already half ready.
And may the language documentation be with you...
Previous articles in the series:
- Mastering MQL5 from beginner to pro (Part I): Getting started with programming
- Master MQL5 from beginner to pro (Part II): Basic data types and use of variable
- Master MQL5 from beginner to pro (Part III): Complex data types and include files
Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/15357





- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use
Thank you, kind man.
With pleasure...