preview
Изучение MQL5 — от новичка до профи (Часть IV): О массивах, функциях и глобальных переменных терминала

Изучение MQL5 — от новичка до профи (Часть IV): О массивах, функциях и глобальных переменных терминала

MetaTrader 5Примеры | 5 августа 2024, 10:20
596 0
Oleh Fedorov
Oleh Fedorov

Введение

Эта статья — продолжение цикла для начинающих. В предыдущих статьях очень подробно были разобраны способы описания данных, хранящихся внутри нашей программы. К этому моменту читатель должен знать:

  • что данные можно хранить в переменных или константах;
  • что язык MQL5 является языком со строгой типизацией, и это значит, что каждый фрагмент данных в программе имеет свой тип, нужный компилятору для правильного распределения памяти и для того, чтобы избежать некоторых логических ошибок;
  • что типы данных бывают простые (базовые) и сложные (пользовательские);
  • что для использования данных в программе на MQL5 нужно объявить хоть одну функцию;
  • что любые блоки кода можно выносить в отдельные файлы, а затем подключать эти файлы к проекту с помощью директивы препроцессора #include.

В этой статье я рассмотрю три довольно глобальных темы:

  • Массивы данных, которые завершают основную историю о данных внутри программы.
  • Глобальные переменные терминала, позволяющие обмениваться простыми данными между разными программами MQL5. 
  • Кроме того, будут рассмотрены некоторые тонкости работы функций и их взаимодействия с переменными.

Основные сведения о массивах

Массив — это переменная, содержащая последовательность данных одного типа.

Для описания массива необходимо описать его тип и имя переменной, после чего записать квадратные скобки. В квадратных скобках можно указать количество элементов в данной последовательности.

int myArray[2]; // Описан массив целых чисел, содержащий два элемента

Пример 1. Описание статического массива.

Последовательности в программах на языке MQL5 приходится описывать довольно часто. Это и все цены истории, и время начала каждой свечи, и объёмы… В общем, всегда, когда есть наборы данных, массивы могут оказаться хорошим выбором.

Нумерация элементов внутри массива в MQL5 всегда начинается с 0. Поэтому номер последнего элемента в массиве всегда будет равен количеству его элементов минус единица ( lastElement = size — 1 ).

Чтобы обратиться к любому элементу массива, нужно просто указать номер этого элемента в квадратных скобках:

// Заполним массив значениями:
myArray[0] = 3;
myArray[1] = 315;

// И выведем последний элемент этого массива в журнал:
Print(myArray[1]); // 315

Пример 2. Использование элементов массива.

И, конечно, любой массив можно инициализировать при описании — так же точно, как структуру, с помощью фигурных скобок:

double anotherArray[2] = {4.0, 5.2};        // Массив из двух элементов инициализирован двумя значениями

Print( DoubleToString(anotherArray[0],2) ); // Выведет 4.00

Пример 3. Инициализация массива при описании.


Многомерные массивы

Массив может хранить в себе другие массивы. Такие вложенные массивы называют "многомерными".

Простым наглядным примером многомерных массивов могут служить страницы книг. Символы собираются в строку — первое измерение, строки собираются в абзацы — второе измерение, и страница — набор абзацев, третье измерение.

Одномерный массив символов

Рисунок 1. Символы собираются в строку — одномерный массив.

Двумерный массив символов

Рисунок 2. Строки собираются в абзацы — двумерный массив.

Трёхмерный массив

Рисунок 3. Абзацы собираются в страницы — трёхмерный массив.

Для описания таких массивов в MQL5 просто добавляются квадратные скобки для каждого нового измерения. Скобки для "внешних" контейнеров располагаются левее "внутренних". Например, массивы, изображённые на рисунках 1-3 можно было бы описать и использовать следующим образом:

char stringArray[21];        // Одномерный массив
char paragraphArray[2][22];  // Двумерный массив
char pageArray[3][2][22];    // Трёхмерный массив

// Заполнение двумерного массива 
paragraphArray[0][0]='T';
paragraphArray[0][1]='h';
// …
paragraphArray[1][20]='n';
paragraphArray[1][21]='.';

// Доступ к произвольному элементу двумерного массива
Print(CharToString(paragraphArray[1][3])); // Выведет букву "a" (почему?)

Пример 4. Описание многомерных массивов для рисунков 1-3.

Общее количество измерений в массиве не должно превышать 4. Максимальное количество элементов в любом измерении равно 2147483647.

Инициализация многомерных массивов при описании так же проста, как и для одномерных. В фигурных скобках просто перечисляются элементы каждого массива:

int arrayToInitialize [2][5] = 
  {
    {1,2,3,4,5},
    {6,7,8,9,10}
  }

Пример 5. Инициализация многомерных массивов.


Динамические массивы

Не для всех массивов можно сразу сказать, сколько элементов в них будет. Например, массивы, содержащие историю терминала или списки сделок, будут изменяться со временем. Поэтому кроме статических массивов, описанных в предыдущих разделах, MQL5 позволяет создавать и динамические, то есть такие, которые в процессе работы программы могут изменять количество своих элементов. Описываются такие массивы точно так же, как и статические, только в квадратных скобках не указывается количество элементов:

int dinamicArray [];

Пример 6. Описание динамических массивов

Описанный таким образом новый массив не содержит элементов, его длина равна 0, и, следовательно, к его элементам невозможно обратиться. Если программа всё-таки попытается это сделать, то возникнет критическая ошибка, и выполнение программы экстренно завершится. Поэтому прежде, чем работать с таким массивом, необходимо задать его размер с помощью специальной встроенной функции ArrayResize:

ArrayResize(dinamicArray, 1); // Первый параметр — массив, второй параметр — новый размер


ArrayResize(dinamicArray,1, 100); // Третий параметр — зарезервированный (избыточный) размер

Пример 7. Изменение размера динамического массива

В справке можно увидеть, что функция может принимать до трёх параметров, однако третий параметр имеет значение по умолчанию, поэтому его можно опустить, как я это сделал в первой строке своего примера.

Первым параметром данной функции обязательно будет массив, который мы меняем. Вторым — новый размер массива. Думаю, с этим никаких проблем не возникнет. Третьим параметром является "зарезервированный размер".

"Зарезервированный размер" используется, если нам известен предельный размер нашего массива. Например, по условиям нашей задачи в массиве не может быть больше 100 значений, но сколько конкретно — неизвестно. Тогда можно использовать параметр reserve_size в этой функции, и задать его равным 100, как это сделано в примере 7 во второй строке. В этом случае при вызове функция зарезервирует избыточный размер памяти для 100 элементов, хотя фактический размер массива останется тем, что указан во втором параметре (1 элемент).

Зачем такие сложности? Почему просто не добавлять по элементу, когда это нужно?

Простой ответ: для ускорения работы нашей программы.

Более развёрнутый ответ можно писать долго. Но, если вкратце, дело в том, что каждый раз, когда мы используем функцию ArrayResize без третьего параметра, наша программа запрашивает у операционной системы дополнительную память. Такое выделение памяти — довольно долгая (с точки зрения процессора) операция, и не важно, нужна память лишь под один элемент или под много сразу. Чем реже нашей программе придётся этим заниматься, тем лучше. То есть лучше зарезервировать сразу много места и потом его заполнять, чем выделить мало места, а потом его расширять. Правда, и тут нужно учитывать, что оперативная память — ограниченный ресурс, и поэтому придётся всегда искать баланс между скоростью работы и размером данных.

Если вы знаете ограничения своих массивов, лучше указывайте это программе явно, описывая статические массивы либо резервируя память с помощью третьего параметра в функции ArrayResize. Если не знаете, тогда массив однозначно динамический, и третий параметр в функции ArrayResize указывать не обязательно, хотя и можно, так как если фактический размер массива больше зарезервированного, MQL5 просто выделит нужную фактически память.

Когда массив получил новый размер, как в примере 7, можно изменять данные внутри него:

dinamicArray[0] = 3; // Сейчас наш массив содержит ровно один элемент (см. пример 7), и номер этого элемента равен 0

Пример 8. Использование изменённого массива

Когда мы работаем с динамическими массивами, чаще всего стоит задача добавить данные в конец этого массива, а не поменять что-то в середине (хотя и такое бывает). Поскольку в каждый конкретный момент времени программа не знает, сколько элементов содержится в массиве именно сейчас, нужна специальная функция, позволяющая это узнать. Называется она ArraySize. Функция принимает один параметр — массив — и возвращает целое значение — количество элементов в этом массиве. И когда мы знаем точный размер динамического массива (который предоставит нам эта функция), добавить элемент становится довольно просто:

int size,           // Количество элементов в массиве
    lastElemet;     // Номер последнего элемента

char stringArray[]; // Наш "подопытный" динамический массив. 
                    //   Сразу после описания его размер равен 0 (массив не может содержать элементов)

// Добавляем элемент в  конец.

size = ArraySize(stringArray);     // Узнаём текущий размер массива
size++;                            // Размер массива должен увеличиться на 1
ArrayResize(stringArray, size, 2); // Изменяем размер массива. В нашем примере в массиве будет не больше двух элементов 
lastElemet = size — 1;             // Нумерация начинается с 0, поэтому номер последнего элемента на 1 меньше размера массива

stringArray[lastElement] = `H`;    // Записываем значение

// А теперь добавим ещё один элемент. Последовательность действий — абсолютно та же самая

size = ArraySize(stringArray);     // Узнаём текущий размер массива
size++;                            // Размер массива должен увеличиться на 1
ArrayResize(stringArray, size, 2); // Изменяем размер массива. В нашем примере в массиве будет не больше двух элементов 
lastElemet = size — 1;             // Нумерация начинается с 0, поэтому номер последнего элемента на 1 меньше размера массива

stringArray[lastElement] = `i`;    // Записываем значение

// Заметьте, что при добавлении второго элемента таким образом меняется только одна строка  - та, 
//   которая записывает в конкретную ячейку фактическое значение. 
//
// Значит, запись решения можно сократить. Например, создав для него отдельную пользовательскую функцию.

Пример 9. Добавление элемента в конец динамического массива.

Функции ArraySize и ArrayResize при работе с динамическими массивами используются постоянно, обычно в таком сочетании, как в примере 9. С другими функциями, реже употребимыми, но не менее полезными, я рекомендую ознакомиться самостоятельно.

И, завершая данный раздел, хочу заметить, что сделать многомерный динамический массив в языке MQL5 тоже можно, но неопределённым может быть только первый индекс.

int a [][12]; // Всё в порядке

// int b [][]; // Ошибка компиляции: динамическим может быть только первый индекс

Пример 10. Многомерный динамический массив.

Если всё-таки очень нужно сделать динамическими несколько индексов, можно создать структуру, единственным полем которой будет динамический массив, а потом сделать массив таких структур.

struct DinArray                              // Структура, содержащая динамический массив
     {
      int a          [];
     };

DinArray dinamicTwoDimensions [];            // Динамический массив структур

ArrayResize( dinamicTwoDimensions, 1 );      // Задали размер внешнему измерению 
ArrayResize( dinamicTwoDimensions[0].a, 1 ); // Задали размер внутреннему массиву

dinamicTwoDimensions[0].a[0] = 12; // Использовали ячейку для записи данных

Пример 11. Массив с двумя динамическими индексами.

В принципе, существуют и другие методы решения этой проблемы. Например, можно создать свой класс или воспользоваться уже существующим в стандартной библиотеке. Однако тему работы с классами я оставлю для будущих статей.


Массивы-серии

Цены открытия, закрытия, максимума и минимума, объёмы тиковые и реальные, спреды, время начала свечи и значения индикаторов на каждой свече в MQL5 называются сериями или временными рядами.

У программиста MQL5 нет прямого доступа к этим последовательностям, однако язык предоставляет возможность с помощью набора специальных предопределённых функций (перечисленных в таблице 1) скопировать эти данные в любые переменные внутри нашей программы.

Если нам нужны, скажем, цены закрытия, сначала нужно создать свой массив, в котором эти цены будут храниться, затем вызвать функцию CopyClose и передать созданный массив ей в качестве последнего параметра. Функция скопирует стандартную серию в нашу переменную, и потом эти данные можно будет использовать обычным образом: с помощью индексов в квадратных скобках.

Вот только работа с временными рядами несколько отличается от работы со всеми остальными массивами. Так сложилось исторически.

В памяти серии хранятся так же, как и все остальные: от более старых к более новым. Но функции по работе с сериями из таблицы 1 нумеруют элементы во временных рядах наоборот, справа налево. Для всех этих функций нулевой свечой будет самая правая, текущая, та, что ещё не завершена. Но "обычные" массивы об этом не знают, и поэтому для них эта свеча будет самой последней. Вот такая путаница…

Давайте попробуем разобраться в этом с помощью рисунков.

Нумерация элементов в массиве

Рисунок 4. Направление нумерации в обычных массивах (зелёная стрелка) и в сериях (синяя стрелка).

Копирование серий в обычные массивы

Рисунок 5. Копирование серий в обычные массивы.

На рисунке 4 показано отличие направления нумерации серий и обычных массивов.

Рисунок 5 схематически отображает копирование серий в обычные массивы, например, с помощью функции CopyRates или ей подобной (см. таблицу 1).  Физический порядок элементов в памяти одинаков для обычных массивов и для серий, а вот нумерация изменяется, и первый в серии элемент после копирования становится последним в обычном массиве.

Иногда программировать, постоянно держа в голове эти нюансы, бывает неудобно. Для того чтобы бороться с этим неудобством, существует два пути:

  1. Встроенная функция ArraySetAsSeries позволяет поменять направление нумерации для любого динамического массива. Она принимает два параметра: сам массив и признак его "серийности" (true/false). Если ваш алгоритм предполагает копирование данных, которые всегда начинаются с последней свечи, чаще всего можно назначить целевой массив серией, и тогда окажется, что стандартные функции и ваш алгоритм используют одни и те же индексы для работы.
  2. Если ваш алгоритм предполагает копирование небольших фрагментов данных из произвольного места графика, особенно если точно известно их количество на каждом шаге алгоритма (например, берём цены закрытия трёх баров: первого закрывшегося — то есть с индексом серии 1 — и двух следующих за ним, с индексами серии 2 и 3), то лучше всего — не бороться. Проще смириться с тем, что нумерация будет направлена в разную сторону, и просто программировать более внимательно. Как вариант решения — создать отдельную функцию для проверки нужных значений и в любых выражениях стараться применить именно её.

В следующем примере я постарался проиллюстрировать всё вышесказанное с помощью кода.

datetime lastBarTime;      // В эту переменную попробуем записать время последней свечи
datetime lastTimeValues[]; // В массиве будет храниться время двух последних свечей. 
                           //   Он динамический, чтобы его можно было сделать серией и проверить работу индексов

// Получаем значение времени начала текущей свечи с помощью функции iTime
lastBarTime = iTime
              (
                 Symbol(),       // Используем текущий символ
                 PERIOD_CURRENT, // На текущем временном интервале
                 0               // Интересует текущая свеча
              );

Print("Start time of the 0 bar is ", lastBarTime);

// Получаем значение времени начала двух последних свечей с помощью функции CopyTime
CopyTime
(
   Symbol(),       // Используем текущий символ
   PERIOD_CURRENT, // На текущем временном интервале
   0,              // Начинаем с позиции 0
   2,              // Берём два значения
   lastTimeValues  // Записываем их в массив lastTimeValues (это "обычный") массив
);

Print("No series");
ArrayPrint(lastTimeValues,_Digits,"; "); // Выводим весь массив в журнал. Разделитель между элементами — точка с запятой


ArraySetAsSeries(lastTimeValues,true);   // Превращаем массив в серию

Print("Series");
ArrayPrint(lastTimeValues,_Digits,"; "); // Снова выводим весь массив. Обратите внимание на порядок данных

/* Вывод скрипта:

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

*/

Пример 12. Проверка функций работы с временными сериями.

Таблица 1.  Список функций для доступа к временным сериям. Для всех этих функций нумерация элементов начинается справа — с последней (незавершённой) свечи.

Функция Действие
CopyBuffer Получает в массив данные указанного буфера от указанного индикатора
CopyRates Получает в массив структур MqlRates исторические данные для указанных символа и периода
CopySeries Получает сразу несколько синхронизированных временных серий для указанного символа/периода в указанном количестве. Список всех заполняемых массивов передаётся в конце, и их порядок должен соответствовать полям структуры MqlRates
CopyTime Получает в массив исторические данные по времени открытия баров по соответствующим символу и периоду
CopyOpen Получает в массив исторические данные по цене открытия баров по соответствующим символу и периоду
CopyHigh Получает в массив исторические данные по максимальной цене баров по соответствующим символу и периоду
CopyLow Получает в массив исторические данные по минимальной цене баров по соответствующим символу и периоду
CopyClose Получает в массив исторические данные по цене закрытия баров по соответствующим символу и периоду
CopyTickVolume Получает в массив исторические данные по тиковым объемам для соответствующих символа и периода
CopyRealVolume Получает в массив исторические данные по торговым объемам для соответствующих символа и периода
CopySpread Получает в массив исторические данные по спредам для соответствующих символа и периода
CopyTicks Получает в массив тики в формате MqlTick
CopyTicksRange Получает в массив тики в указанном диапазоне дат
iBarShift Возвращает индекс бара в серии, в который попадает указанное время
iClose Возвращает значение цены закрытия бара (указанного параметром shift) соответствующего графика
iHigh Возвращает значение максимальной цены бара (указанного параметром shift) соответствующего графика
iHighest Возвращает индекс наибольшего найденного значения (смещение относительно текущего бара) соответствующего графика
iLow Возвращает значение минимальной цены бара (указанного параметром shift) соответствующего графика
iLowest Возвращает индекс наименьшего найденного значения (смещение относительно текущего бара) соответствующего графика
iOpen Возвращает значение цены открытия бара (указанного параметром shift) соответствующего графика
iTime Возвращает значение времени открытия бара (указанного параметром shift) соответствующего графика
iTickVolume


iVolume

Возвращает значение тикового объема бара (указанного параметром shift) соответствующего графика
iRealVolume Возвращает значение реального объема бара (указанного параметром shift) соответствующего графика
iSpread Возвращает значение спреда для бара, указанного параметром shift, на соответствующем графике

Создание функций (в подробностях)

Любая функция в программе MQL5 создаётся по одному шаблону, о котором мы немного говорили в первой статье цикла:

ТипРезультата Имя_Функции(ТипПараметра1 имяПараметра1, ТипПараметра2 имяПараметра2 …)
  {
   // Описание переменной результата и других локальных переменных 
   ТипРезультата result;

   // …

   //---
      // Здесь происходят основные действия
   //---
   return resut;
  }

Пример 13. Шаблон описания функции.

ТипРезультата (и ТипыПараметров) — это любой допустимый тип данных. Тут может быть int, double, имя класса или перечисления или что угодно другое, известное вам.

Параметров или явного результата работы функции может и не быть. Тогда вместо типа результата или внутри круглых скобок — вместо списка параметров — вставляется слово void. В буквальном переводе оно обозначает "пустое место, дыра".

Понятно, что если ТипРезультата — void, то и возвращать данные не нужно, соответственно, последнюю строку внутри фигурных скобок (return result) указывать не надо, да и описывать переменную результата тоже не обязательно.

Ну и ещё несколько простых правил:

  • Имя_Функции и имена параметров должны соответствовать соглашениям об идентификаторах.
  • Оператор return возвращает только одно значение. Больше нельзя. Но есть обходные пути, о которых чуть дальше.
  • Функцию нельзя описывать внутри другой функции, только снаружи, вне всех функций.
  • Можно описывать несколько функций с одинаковым именем, но разным числом (или разными типами) параметров и/или разными типами возвращаемого результата. Главное, чтобы можно было точно понять, когда какую функцию надо использовать. Если вы сможете различить и объяснить эти различия кому-то не сведущему в вашем коде, то и компилятор сможет.

Вот несколько примеров, иллюстрирующих, как можно описывать функции:

//+------------------------------------------------------------------+
//| Пример 1                                                         |
//| Очень часто в комментариях описывают, что делает функция,        |
//|   какие данные ей нужны и зачем. Например, так:                  |                                                    |
//|                                                                  |           
//|                                                                  |
//| Функция возвращает разность между двумя целыми числами.          |
//|                                                                  |
//| Параметры:                                                       |
//|   int a — уменьшаемое                                            |
//|   int b — вычитаемое                                             |
//| Возвращаемое значение:                                           |
//|   разность между a и b                                           |
//+------------------------------------------------------------------+
int diff(int a, int b)
 {
// Действие очень простое, переменную для результата не создаём.
  return (a-b);
 }

//+------------------------------------------------------------------+ 
//| Пример 1a                                                        | 
//| Функция возвращает разность между двумя действительными числами. | 
//|                                                                  |
//| Имя функции совпадает с предыдущим примером, но тип параметров   |
//| другой.                                                          |
//|                                                                  | 
//| Параметры:                                                       | 
//|   double a — уменьшаемое                                         | 
//|   double b — вычитаемое                                          | 
//| Возвращаемое значение:                                           | 
//|   разность между a и b                                           | 
//+------------------------------------------------------------------+ 
double diff(double a, double b)
  {
   return (a-b);
  }

//+------------------------------------------------------------------+
//| Пример 2                                                         |
//| Иллюстрирует возможности использования "void".                   |
//| Вызывает (использует) функцию diff                               |
//+------------------------------------------------------------------+
void test()
 {
// Можно делать что угодно.

// Например, использовать функцию из Примера 1.
  Print(diff(3,4)); // результат равен -1

// Поскольку при вызове функции diff в скобках ей были переданы 
//  целые параметры, результат тоже будет целым.

// И теперь попробуем вызвать ту же функцию с параметрами двойной точности
  Print(diff(3.0,4.0)); // результат равен -1.0

// Поскольку функция объявлена как "void", оператор "return" не нужен
 }

//+------------------------------------------------------------------+
//| Пример 3                                                         |
//| У функции нет параметров для обработки. Можно было оставить      |
//|   пустые скобки, как в предыдущем примере а можно явно написать  |
//|   слово "void"                                                   |
//| Возвращаемое значение:                                           |
//|   string nameForReturn — какое-то имя, всегда одинаковое         |                                                 |
//+------------------------------------------------------------------+
string name(void)
 {
  string nameForReturn="Needed Name";
  return nameForReturn;
 }

Пример 14. Примеры описания функций согласно шаблону.

Важно понимать, что работа с функцией состоит из двух стадий: описания и использования. Когда мы описываем функцию, эта функция ещё ничего не делает. Это просто формальный алгоритм, схема действий. Например, функцию diff из примера 14 можно было бы описать словами примерно так:

    1. Взять два целых числа (любых, каких угодно, заранее они нам не известны).
    2. Обозначить внутри алгоритма одно из них именем a, а другое — b.
    3. Вычесть из числа а число b.
    4. Результат вычислений (какой угодно, заранее неизвестный) отдать тому, кто об этом попросил (вернуть в точку вызова).

    Там, где мы говорим о "любом, каком угодно" значении, это выражение можно заменить словами "формальный параметр". Функции для того и создаются, чтобы формально выполнять некоторые действия с любыми данными.

    Параметры, которые используются при описании функций, называются "формальными".

    В примере 14 функция diff содержит два формальных параметра, остальные — ни одного. В общем случае формальных параметров у функции может быть много (до 63).

    А вот для того, чтобы получить конкретный результат, функцию нужно вызвать (то есть — использовать), как, например, функция test в примере 14 вызывала функции Print и diff. Вот тут уже используются совершенно конкретные значения, фактические в момент вызова: содержимое переменных или констант, литералы (как в моём примере), результаты работы других функций…

    Параметры, которые мы передаём функции в момент вызова, называются "фактическими".

    Для вызова любой функции необходимо указать её имя и в скобках перечислить фактические параметры. Фактические  параметры должны соответствовать формальным параметрам по типу и по количеству.

    Так, в примере 14 функция test использует ровно два целых числа или ровно два числа двойной точности для вызова функции diff. Если бы я ошибся и попробовал написать один или три параметра, то получил бы ошибку компиляции.


    Область видимости переменных

    При объявлении переменных нужно учитывать, где именно они объявляются.

    • Если переменная объявлена внутри функции (в том числе — формальные параметры этой функции), другие функции эту переменную "увидеть" (и, следовательно, использовать) не смогут. Как правило, такая переменная "рождается" в момент вызова функции и "умирает", когда функция заканчивает работу. Такая переменная называется локальной.

      В общем случае можно сказать, что область видимости переменной определяется цельной "сущностью" в нашем коде. Например, если переменная объявлена внутри фигурных скобок, то она будет видна только внутри этих скобок, образующих блок, но не снаружи этого блока. Формальные параметры функции относятся к "сущности" функции, поэтому видны будут только внутри этой функции. И так далее. Время жизни такой переменной равно времени жизни "сущности", к которой она принадлежит. Например, переменная, описанная внутри функции, создаётся, когда эта функция вызывается, и уничтожается, когда функция завершает работу.

    void OnStart()
      {
    //--- Локальная переменная внутри функции — видна для всех блоков этой функции, но не снаружи
        int myString = "This is local string";
        // Фигурные скобки описывают блок внутри функции
        {
          int k=4;  // Локальная переменная блока — видна только внутри фигурных скобок
          Print(k);  // Всё в порядке
          Print (myString); 
        }
    // Print(k);    // Ошибка компиляции. Вне блока фигурных скобок переменной k не существует.
      }

    Пример 15. Локальная переменная внутри блока фигурных скобок

    • Если переменная описана снаружи, вне описания любых функций, ею могут пользоваться все функции нашего приложения. При этом время жизни такой переменной равно времени жизни программы. Такая переменная называется  глобальной.
    int globalVariable = 345;
    
    void OnStart()
      {
    //---
        Print (globalVariable); // Всё в порядке
      }
    

    Пример 16. Глобальная переменная видна всем функциям нашей программы.

    • Локальная переменная может называться так же, как и глобальная, однако в этом случае локальная переменная скрывает глобальную для данной функции.
    int globalVariable=5;
    
    void OnStart()
      {
        int globalVariable=10;  // Переменная описана по всем правилам, включая тип. 
                                //   Если бы тип не был описан, это выражение изменило бы глобальную переменную
    //---
       
        Print(globalVariable);  // Результат 10 — то есть, значение локальной переменной
    
        Print(::globalVariable); // Результата 5. Чтобы вывести значение глобальной, а не локальной переменной, 
                                 //   использованы два символа двоеточия перед именем
      }

    Пример 17. "Перекрывающиеся" имена локальной и глобальной переменных. Локальная скрывает глобальную.

    Статические переменные

    При описании локальных переменных есть один специальный случай.

    Как было сказано выше, локальные переменные после завершения работы функции теряют свои значения. Обычно это именно то поведение, которое и ожидается. Однако бывают ситуации, когда нужно сохранить значение локальной переменной даже после того, как функция отработала.

    Например, организовать счётчик обращений к функции. Или — более часто встречающаяся задача трейдера — организовать функцию проверки начала новой свечи, для чего нужно на каждом тике получать значение текущего времени и сравнивать его с предыдущим известным значением. Можно, конечно, создать глобальную переменную для каждого из этих счётчиков. Но каждая глобальная переменная повышает вероятность ошибки, поскольку эти счётчики нужны только одной функции, а остальным их менять — и даже "видеть" — не желательно.

    Вот в таких случаях, когда локальная переменная должна жить так же долго, как глобальная, используют статические переменные. Описываются они так же точно, как обычные, только перед описанием добавляется слово static, как это сделано внутри функции HowManyCalls в следующем примере:

    //+------------------------------------------------------------------+
    //| Script program start function                                    |
    //+------------------------------------------------------------------+
    void OnStart()
     {
    //---
      HowManyCalls();  
      HowManyCalls();  
      HowManyCalls();  
     }
    
    //+------------------------------------------------------------------+
    //| Функция подсчитывает количество обращений                        |
    //+------------------------------------------------------------------+
    void   HowManyCalls()
     {
    //--- Описание переменной. Переменная локальная, но жить будет долго
      static int counter=0;  // Из-за ключевого слова static инициализация этой переменной происходит  
                             //   только перед первым вызовом функции 
                             //   (точнее — перед вызовом функции OnInit) 
    //--- Основные действия
      counter++;             // При работе программы значение сохранится до конца работы
    
    //--- Результат работы
      Print( IntegerToString(counter)+" calls");
     }
    
    // Вывод скрипта:
    // 1 calls
    // 2 calls
    // 3 calls
    

    Пример 18. Использование статической переменной.

    Пример содержит две функции: HowManyCalls, которая с помощью статической переменной подсчитывает количество обращений к ней и выводит в журнал результат подсчётов, и OnStart, вызывающую HowManyCalls три раза подряд.

    Передача параметров функции по значению и по ссылке

    По умолчанию функция использует лишь копии данных, которые переданы ей в качестве параметров (программисты говорят, что в этом случае данные передаются "по значению"). Поэтому даже если внутри функции в переменную-параметр что-то записать, с исходными данными ничего не случится.

    Если мы хотим, чтобы изнутри функции менялись исходные данные, изменяемый формальный параметр надо обозначить специальным значком "&"  (амперсанд). Такой способ описания параметров называют передачей "по ссылке".

    Для того чтобы проиллюстрировать то, как функция может изменять внешние данные, создадим новый файл скрипта, содержащий следующий код:

    //+------------------------------------------------------------------+
    //| Script program start function                                    |
    //+------------------------------------------------------------------+
    void OnStart(void)
      {
    //--- Объявили и инициализировали две локальные переменные
       int first = 3;
       int second = 77;
    //--- Вывели их значения ДО всех изменений
       Print("Before swap: first = " + first + " second = " + second);
    //--- Применили функцию Swap, принимающую данные по ссылке
       Swap(first,second);
    //--- И посмотрели, что получилось
       Print("After swap: first = " + first + " second = " + second);
    //---
    
    //--- К полученным данным применили функцию CheckLocal
    //---  принимающую данные по значению
       CheckLocal(first,second);
    //--- И снова вывели результат
       Print("After CheckLocal: first = " + first + " second = " + second);
      }
    
    //+------------------------------------------------------------------+
    //| Меняет местами значения двух целых переменных                    |
    //|   Данные передаются по ссылке, поэтому будут изменены оригиналы  |
    //+------------------------------------------------------------------+
    void Swap(int &a, int& b) // Можно и так, и эдак, оба положения правильные
      {
       int temp;
    //---
       temp = a;
       a = b;
       b = temp;
      }
    
    //+------------------------------------------------------------------+
    //| Принимает параметры по значению, поэтому изменения происходят    |
    //|   только локально                                                |
    //+------------------------------------------------------------------+
    void CheckLocal(int a, int b)
      {
       a = 5;
       b = 10;
      }
    
    
    
    // Вывод скрипта:
    
    // Before swap: first = 3 second = 77
    // After swap: first = 77 second = 3
    // After CheckLocal: first = 77 second = 3
    

    Пример 19. Передача параметров по ссылке.

    В этом коде описаны три короткие функции: OnStart, Swap и CheckLocal.

    CheckLocal принимает данные по значению и, следовательно, работает с копиями, Swap принимает два параметра по ссылке, следовательно, работает с оригиналами. Функция OnStart описывает две локальные переменные, а затем выводит значение этих переменных, вызывает функции Swap и CheckLocal и показывает результаты взаимодействия, выводя значение своих локальных переменных в журнал после каждого взаимодействия. Еще раз обращаю ваше внимание на то, что функция Swap изменила переданные ей данные, а CheckLocal этого сделать не смогла.

    Важно отметить, что все переменные сложных типов (такие, как перечисления, структуры, объекты и т.д., а также любые массивы) всегда должны передаваться по ссылке.

    При попытке предать такие переменные по значению компилятор выдаст ошибку.

    И ещё раз кратко перечислю списком основные правила взаимодействия переменных и функций:

    • Глобальные переменные в языке MQL5 можно напрямую использовать изнутри любой функции, в том числе — изменять их значения.
    • Локальные переменные доступны только внутри того блока, где они описаны.
    • Если формальный параметр описывает передачу данных "по значению", функция не может менять исходные данные, даже если внутри себя изменит значение переменной-параметра. Но данные, переданные "по ссылке", могут меняться в исходном месте.
    • Если глобальная и локальная переменные имеют одинаковое имя, главнее будет локальная переменная (иначе говоря, локальная переменная перекрывает глобальную).
    • Время жизни глобальных переменных равно времени жизни программы, локальных — времени жизни того блока, где они описаны.

    Значения по умолчанию для формальных параметров функции

    Формальным параметрам можно назначить значение по умолчанию.

    Например, если мы создаём функцию логирования, нам может потребоваться выделять сообщения этой функции из всех остальных сообщений терминала. Проще всего это сделать, добавив к исходному сообщению какой-нибудь префикс в начале и суффикс в конце. Сама строка всегда должна быть задана, иначе смысл функции теряется. А вот "дополнения" могут быть стандартными, хотя могут и меняться.

    Простейший код, иллюстрирующий эту идею, приведён ниже:

    //+------------------------------------------------------------------+
    //| Добавляет к строке префикс и суффикс                             |
    //+------------------------------------------------------------------+
    string MakeMessage(
      string mainString,
      string prefix="=== ",
      string suffix=" ==="
    )
     {
      return (prefix + mainString + suffix);
     }

    Пример 20. Описание функции с заданием формальных параметров по умолчанию

    При вызове этой функции один или оба параметра, имеющих значения по умолчанию, можно будет пропустить. Если эти параметры не будут указаны явно, функция будет использовать заданные при описании значения. Например:

    Print ( MakeMessage("My first string") );               // Префикс и суффикс оставлены по умолчанию 
    Print ( MakeMessage("My second string", "~~ ") );       // Префикс изменён, суффикс остался неизменным 
    Print ( MakeMessage("My third string", "~~ ", " ~~") ); // Изменены оба фактических параметра
    
    // Результат работы скрипта:
     
    // === My first string ===
    // ~~ My first string ===
    // ~~ My first string ~~

    Пример 21. Фактические параметры, имеющие значения по умолчанию, можно опустить

    Параметры со значениями по умолчанию могут идти только подряд, и должны быть описаны после всех остальных параметров, которые таких значений не имеют.


    Как сделать так, чтобы функция вернула несколько результатов

    Выше было сказано, что оператор return может вернуть лишь один результат. Более того, функция не может возвращать массивы. Но если очень надо? Если, например, нужно с помощью одной функции рассчитать одновременно время и значение цены закрытия какой-нибудь свечи или получить список доступных инструментов? Сначала, пожалуйста, попробуйте ответить самостоятельно, а потом сравните с тем, что написано ниже.

    Для решения задачи, вынесенной в заголовок, можно использовать один из  следующих способов:

      • Создать сложный тип данных (например, структуру) и вернуть переменную этого типа.
      • Использовать передачу параметров по ссылке. Это, конечно, не поможет вернуть эти данные с помощью оператора return, но позволит записать любые значения и затем их использовать.
      • Использовать глобальные переменные (не рекомендуется). Этот способ похож на предыдущий, но потенциально более опасен для кода. Глобальные переменные лучше использовать по минимуму, только там, где без них совсем никак не обойтись. Но, если очень хочется — можно попробовать…

      Модификаторы глобальных переменных: input и extern

      При использовании глобальных переменных тоже есть "особые случаи". К ним я отношу:

      • описание входных параметров нашей программы с помощью модификатора input;
      • использование модификатора extern.

      Входные параметры (input переменные)

      Каждый входной параметр программы, написанной на языке MQL5, описывается как глобальная переменная (за пределами всех функций) и обозначается ключевым словом input, которое ставится в начале описания.

      input string smart = "The smartest decision"; // В окне данных будет это описание

      Пример 22. Описание входных параметров

      Обычно в левом столбце окна свойств отображается имя переменной. Однако, если в той же строке, где описана эта переменная, есть комментарий, как в примере 22, вместо имени переменной отображаться будет именно он.

      В программах, написанных на MQL5, переменные, обозначенные как input, можно только читать, записывать в них ничего нельзя. Задать значения этих переменных можно только при описании (в коде) или из диалогового окна свойств программы.

      Если создаётся эксперт или индикатор, значения таких переменных обычно можно оптимизировать с помощью тестера стратегий. Однако если вы хотите исключить из оптимизации какие-то параметры, в начало слова input надо добавить букву s или модификатор static:

      input double price =1.0456;                // БУДЕТ оптимизироваться 
      sinput int points =15;                     // НЕ будет оптимизироваться 
      static input int unoptimizedVariable =100; // НЕ будет оптимизироваться

      Пример 23. Использование модификатора sinput для исключения переменной из оптимизации в тестере

      Если необходимо, чтобы пользователь в каком-то поле ввода выбирал значения из списка, нужно сделать перечисление для каждого такого поля. Строчные комментарии для элементов перечислений также работают, то есть вместо названий вроде POINT_PRICE_CLOSE вы можете отображать "Цена закрытия точки" на любом человеческом языке. К моему сожалению, язык текста для названия поля (комментариев) простыми способами выбрать нельзя. Для каждого используемого вами языка придётся компилировать отдельный файл, поэтому большинство опытных программистов и предпочитают использовать универсальный (английский) язык.

      Параметры можно визуально сгруппировать, чтобы ими было удобнее пользоваться. Для задания имени группы используется специальное описание:

      input group "Имя группы"

      Пример 24. Заголовок группы параметров

      Вот полный пример, иллюстрирующий все эти возможности:

      #property script_show_inputs
      
      // Перечисление. Позволит создать поле со списком.
      enum ENUM_DIRECTION
        {
      
      // Все строчные комментарии рядом со строками, описывающими параметры, 
      //   будут отображаться вместо названий в окне параметров, поэтому я их пишу на английском языке
      
         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()
        {
      //---
        }
      //+------------------------------------------------------------------+
      

      Пример 25. Разные варианты описания входных параметров

      Диалог параметров

      Рисунок 6. Диалог параметров. Цветом выделены группы (input group). Зелёные стрелки указывают значения комментариев, подставленных вместо имён переменных.

      На рисунке 6 показан диалог параметров, который был сгенерирован из кода примера 25. На разных компьютерах он может выглядеть чуть по-разному, но в любом случае у него будут выделяться заголовки групп (я подсветил на рисунке синим). Также вы можете увидеть, что параметры, у которых нет строчных комментариев, будут использовать имена переменных. Если же комментарии есть, компилятор  использует их вместо названия переменных, как в моих ячейках, на которые указывают зелёные стрелки. В общем, сравните код примера 25 с рисунком — и, я надеюсь, вы всё поймёте.

      И ещё — не все новички замечают с левой стороны значки типа данных каждого параметра. Например, у параметра, который на рисунке называется "Possible directions list" тип данных — перечисление, и его значок ( ) намекает на список. Данные для этого поля можно выбрать только из ограниченного списка вариантов, что мне и было нужно. Остальные значки тоже "говорящие", думаю, разобраться с ними будет несложно.

      Имя любого параметра не должно быть длиннее 63 символов (это очень много, в реальности имена, как правило, значительно короче).

      Речь именно об имени параметра (то есть той строки, которая видна в диалоговом окне), а не переменной. Комментарий-то можно написать дли-и-инный… Если размер в 63 символа будет превышен, компилятор просто обрежет "ненужное".

      Длина значения строкового параметра не может быть больше 254 символов, причём чем длиннее имя этого параметра, тем короче может быть содержимое, поскольку в памяти они хранятся одной слитной строкой.

      Вот об том ограничении важно помнить, особенно если задаётся адрес какой-нибудь важной для программы web-страницы. Иногда адреса бывают действительно длинными, и если это ваш случай, попробуйте передавать этот адрес как-нибудь иначе, например, жестко "зашейте" его в коде в виде глобальной переменной, но не параметра. Конечно, есть и лучшие решения, например, использовать файлы или "склеивать" адрес из нескольких фрагментов, но главное — помнить о числе 254 для значений параметров…

      Внешние (extern) переменные

      Второй специальный случай — "внешние" переменные.

      Когда разработчики пишут большую программу, разделённую на несколько файлов, бывают случаи, когда глобальная переменная описана в одном файле, а программе нужно обратиться к ней из других файлов. При этом подключать файлы директивой #include по каким-то причинам не хочется… MetaEditor каждый файл воспринимает отдельно и, следовательно, в этом случае не может помочь при наборе.

      Чаще всего такая ситуация возникает при использовании входных параметров (которые описаны в предыдущем подразделе).

      Вот тут и приходит на помощь ключевое слово extern (внешняя).

      extern bool testComplete;

      Пример 26. Описание внешних переменных

      Такие переменные не могут быть инициализированы в данном файле, и при компиляции адрес памяти этой переменной, скорее всего, будет заменен на "настоящую" глобальную переменную с тем же именем, если компилятор сможет такую найти. Однако функции свободно могут обращаться к этим "формальным" данным, в том числе — изменять их, а у IDE не возникнет никаких сложностей при автоподстановке.


      Глобальные переменные терминала

      Как локальные, так и глобальные переменные, описанные в предыдущих разделах, доступны только текущей программе. Все остальные программы пользоваться этими данными не могут. Но бывают ситуации, когда программам нужно обмениваться данными между собой, либо надо сделать так, чтобы значения переменных сохранялись даже после выключения терминала.

      Примером задачи для обмена может служить очень простой индикатор, в котором нужно вывести количество средств в валюте депозита, необходимое для открытия позиции. Кажется, что всё просто. Немного побродив по оглавлению справки, выясняем, что в MQL5 есть специальная функция OrderCalcMargin, которая как раз и вычисляет нужную сумму. Пробуем её применить, и получаем… разочарование. Дело в том, что в индикаторах нельзя использовать торговые функции. Запрещено физически, на уровне компилятора. А OrderCalcMargin — именно торговая…

      Поэтому придётся действовать обходными путями. Один из вариантов — написать скрипт или сервис, который будет вычислять нужные суммы, после чего записывать эти суммы в переменные терминала. А уже затем наш индикатор будет эти данные считывать, а не вычислять. Этот трюк возможен, поскольку, в отличие от индикаторов, скриптам и сервисам позволено торговать (см. таблицу в первой статье цикла).

      Посмотрим, как мог бы быть реализован такой обмен. Сначала создадим файл скрипта с помощью мастера. Пусть этот файл называется "CalculateMargin.mq5".

      Для доступа к переменным терминала есть целый набор предопределённых функций, названия которых начинаются с префикса GlobalVariable.
      Используя их и функцию OrderCalcMargin, чтобы сделать доступными нужные нам данные для индикаторов, создадим новый скрипт:

      //+------------------------------------------------------------------+
      //|                                              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
      
      //--- Входные параметры скрипта
      input double requiredAmount = 1; // Количество лотов
      
      //+------------------------------------------------------------------+
      //| Script program start function                                    |
      //+------------------------------------------------------------------+
      void OnStart()
       {
      //--- Описание локальных переменных
        string symbolName = Symbol();  // Имя текущего символа
        string terminalVariableName;   // Имя переменной терминала
      
      
        double marginBuy, marginSell;  // Значения маржи (на покупку и на продажу
        double currentPrice = iClose(symbolName,PERIOD_CURRENT,0);  // Текущая цена для расчёта маржи 
      
        bool okCalcBuy, okCalcSell;    // Признаки, что всё прошло хорошо при расчете маржи вверх и вниз
      
      //--- Основная работа
      
      // Вычисляем маржу для покупки
        okCalcBuy = OrderCalcMargin(
                      ORDER_TYPE_BUY, // Тип ордера
                      symbolName,     // Имя символа
                      requiredAmount, // Необходимый объём, в лотах
                      currentPrice,   // Цена открытия ордера
                      marginBuy       // Результат (по ссылке)
                    );
      // Вычисляем маржу для продажи
        okCalcSell = OrderCalcMargin(
                       ORDER_TYPE_SELL, // Иногда нужны разные суммы на открытие вверх и вниз
                       symbolName,
                       requiredAmount,
                       currentPrice,
                       marginSell
                     );
      
      //--- Результат работы
      // Создаём имя переменной терминала для сведений о покупке
        terminalVariableName = symbolName + "BuyAmount";
      
      // И записываем данные. Если глобальной переменной терминала не существует, она будет создана
        GlobalVariableSet
          (
            terminalVariableName, // Куда пишем
            marginBuy             // Что пишем
          );
      
      // Теперь создаём другое имя — для сведений о продаже
        terminalVariableName = symbolName + "SellAmount";
      
      // И снова записываем данные — о продаже. Если переменной с именем, хранимым в terminalVariableName не было, 
      //   она будет создана
        GlobalVariableSet(terminalVariableName,marginSell);
       }
      //+------------------------------------------------------------------+

      Пример 31. Скрипт для расчёта средств в валюте депозита, необходимых для покупки или продажи 1 лота и сохранения этих данных в глобальные переменные терминала

      Здесь для записи данных в переменные терминала мы использовали стандартную функцию GlobalVariableSet. Думаю, в приведённом примере использование данных функций очевидно. Единственное уточнение: длина имени глобальной переменной терминала не должна превышать 63 символа.

      Если бросить этот скрипт на любой график, явных результатов сразу видно не будет. Однако посмотреть, что получилось, можно с помощью клавиши <F3> или выбрав в меню терминала "Инструменты -> Глобальные переменные".

      Меню переменных терминала

      Рисунок 7. Меню переменных терминала.


      После выбора этого пункта меню появится окно со списком всех переменных терминала:

      Окно со списком глобальных переменных терминала

      Рисунок 8. Окно со списком глобальных переменных терминала


      На рисунке 8 видно, что я запускал скрипт только на паре EURUSD, поэтому видны лишь две переменных: суммы для покупки и для продажи, которые в данном случае совпадают.

      Теперь создадим индикатор, который будет использовать эти данные, и заодно посмотрим, как стандартные функции используют рассмотренные выше принципы работы с переменными.

      Пусть файл индикатора  называется "GlobalVars.mq5". Основная работа в этом индикаторе будет проходить внутри функции OnInit, выполняющейся один раз сразу после старта программы. Также добавлена функция OnDeinit, которая убирает комментарии, когда мы удаляем индикатор с графика. Функция OnCalculate, которая обязательна для каждого индикатора и выполняется на каждом тике, в данном индикаторе тоже, конечно, присутствует, но не используется.

      //+------------------------------------------------------------------+
      //|                                                   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()
        {
      
      //--- Описание локальных переменных
         string symbolName = Symbol();            // Имя символа
         string terminalVariableName;             // Имя глобальной переменной терминала
      
         double buyMarginValue, sellMarginValue;  // Сумма для покупки и для продажи
      
         bool okCalcBuy; // Признак, что всё в порядке при вызове одного из вариантов функции GlobalVariableGet   
      
      //--- Основная работа
      // Создаём имя переменной терминала для сведений о покупке
         terminalVariableName = symbolName + "BuyAmount";
      
      // Используем первый способ получить значение глобальной переменной. 
      //   Для получения результата применяется передача параметра по ссылке
         okCalcBuy = GlobalVariableGet(terminalVariableName, buyMarginValue);
      
      // Меняем имя переменной терминала — для сведений о продаже
         terminalVariableName = symbolName + "SellAmount";
      
      // Второй способ получить результат: возвращаемое значение
         sellMarginValue = GlobalVariableGet(terminalVariableName);
      
      //--- Вывод результата в виде комментария на графике
         Comment(
            "Buy margin is " + DoubleToString(buyMarginValue)       // Значение маржи для покупки, второй параметр 
                                                                    //   функции DoubleToString опущен
            +"\n"                                                   // Перенос строки
            +"Sell margin is " + DoubleToString(sellMarginValue,2)  // Значение маржи для продажи, указали количество знаков 
                                                                    //   после запятой
         );
      //---
         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 next call
         return(rates_total);
        }
      //+------------------------------------------------------------------+
      

      Пример 32. Использование глобальных переменных терминала в индикатор.

      В данном демонстрационном индикаторе нам нужно один раз прочесть переменные терминала и завершить работу. Поэтому основной код помещён в функцию OnInit. Пример может показаться большим и страшным, но на самом деле он очень легко читается, тем более, что основной его объём составляют комментарии. Еще раз опишу словами, что происходит в функции OnInit:

      • В первом блоке этой функции мы объявляем все переменные, которые нам понадобятся в дальнейшем. 
      • Затем создаём имя для глобальной переменной терминала.
      • И читаем значение нужной нам глобальной переменной терминала в соответствующую локальную переменную. 
      • Потом генерируем имя второй переменной и также читаем её значение в следующей строке. 
      • И последнее действие — вывод сообщения для пользователя в виде комментария в левом верхнем углу (см. рисунок 9).

      Обратите внимание, что функция GlobalVariableGet имеет два варианта вызова: используя возвращаемое значение или параметр, переданный по ссылке, а функция DoubleToString имеет параметр со значением по умолчанию. Если вы для проверки работоспособности кода будете набирать текст примера в редакторе, а не копировать его через буфер обмена, MetaEditor подскажет эти нюансы.

      Результаты работы индикатора

      Рисунок 9. Результат работы индикатора

      Поскольку я использовал разные способы вызова функции DoubleToString для формирования результата работы, комментарии в верхней и нижней строчке выглядят немного по-разному. При форматировании сообщения для верхней строки я опустил второй параметр функции DoubleToString. Этот параметр должен указывать количество символов после запятой, и по умолчанию он равен 8. Для нижней строки я указал это значение явно и попросил программу вывести два знака.

      Обратите внимание, что индикатор нужно бросить именно на тот график, где был применён скрипт, и только после скрипта — чтобы при его работе глобальные переменные терминала гарантированно существовали. Иначе при работе индикатора возникнет ошибка и комментарии не появятся.

      Таким образом для записи переменных терминала используется функция GlobalVariableSet,  для чтения — GlobalVariableGet. Эти функции наиболее часто используются программистами, однако остальные тоже бывают полезны, поэтому рекомендую почитать хотя бы их список в справке (по ссылке в начале раздела).


      Заключение

      Давайте еще раз пройдёмся списком по рассмотренным сегодня темам. Если какой-то пункт списка вам остался непонятен, вернитесь, пожалуйста, к соответствующему месту статьи и разберитесь, поскольку этот материал является базой для всей остальной работы (ну, может, за исключением глобальных переменных терминала, без них часто можно обойтись — но как раз их понимание обычно затруднений не вызывают). Итак, в этой статье было рассказано про:

      1. Массивы:
        • бывают статические и динамические;
        • одномерные и многомерные;
        • статические массивы можно инициализировать с помощью литералов (в фигурных скобках);
        • для работы с динамическими массивами надо использовать стандартные функции изменения размеров и узнавания текущего размера.
      2. Переменные по отношению к функциям бывают
        • локальные (живут коротко кроме статических); к ним можно добавлять модификатор static;
        • и глобальные (живут долго); к ним можно добавлять модификаторы extern и input.
      3. Параметры функции можно передавать
        • по ссылке;
        • по значению.
      4. Существуют глобальные переменные терминала. В отличие от просто глобальных переменных программы их можно использовать для обмена данными между разными программами. Для их использования есть специальный набор функций.

      Если вы всё это помните, и ни один элемент списка не вызвал у вас замешательства, значит, новичком вас уже назвать нельзя: у вас уже появилась хорошая база для работы. Осталось разобраться с тем, как использовать основные операторы, а также понять, какие особенности при написании индикаторов и экспертов есть в языке MQL5 по отношению к другим языкам — и можно начинать писать полезные программы. Правда, до уровня "профессионала" нужно будет разобраться еще с десятком тем, включая объектно-ориентированное программирование, но все эти темы всё равно так или иначе будут опираться на базу, которая после прочтения этой статьи уже наполовину готова.

      И да пребудет с вами справка…


      Предыдущие статьи цикла:


      Теория хаоса в трейдинге (Часть 2): Продолжаем погружение Теория хаоса в трейдинге (Часть 2): Продолжаем погружение
      Продолжаем погружение в теорию хаоса на финансовых рынках, и рассмотрим ее применимость к анализу валют и иных активов.
      Нейросети в трейдинге: Практические результаты метода TEMPO Нейросети в трейдинге: Практические результаты метода TEMPO
      Продолжаем знакомство с методом TEMPO. И в данной статье мы оценим фактическую эффективность предложенных подходов на реальных исторических данных.
      Нейросети в трейдинге: Инъекция глобальной информации в независимые каналы (InjectTST) Нейросети в трейдинге: Инъекция глобальной информации в независимые каналы (InjectTST)
      Большинство современных методов прогнозирования мультимодальных временных рядов используют подход независимых каналов. Тем самым игнорируется природная зависимость различных каналов одного временного ряда. Разумное использование 2 подходов (независимых и смешанных каналов) является ключом к повышению эффективности моделей.
      Разработка системы репликации (Часть 43): Проект Chart Trade (II) Разработка системы репликации (Часть 43): Проект Chart Trade (II)
      Большинство людей, которые хотят или мечтают научиться программировать, на самом деле не имеют представления о том, что делают. Их деятельность заключается в попытках создавать вещи определенным образом. Однако программирование – это вовсе не подгонка под ответ подходящих решений. Если действовать таким образом, можно создать больше проблем, чем решений. Здесь мы будем делать нечто более продвинутое и, следовательно, другое.