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

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

MetaTrader 5Примеры | 28 августа 2024, 13:36
615 2
Oleh Fedorov
Oleh Fedorov

Введение

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

  • Внешним баром будем считать бар, который пробил диапазон предыдущей свечи в обе стороны.
  • Внутренним баром будем считать такой, который после закрытия остался внутри диапазона предыдущей свечи.
  • Отмечать будем каждую закрытую свечу. Если свеча еще изменяется, ставить на ней отметки не будем.
  • Маркеры будут ставиться только на "длинных" свечах, то есть для "внешнего" бара — на текущем баре, для "внутреннего" — на предыдущем. Это решение может быть спорным, поэтому, если индикатор покажется  вам полезным, но с данным решением вы не согласны — смело меняйте соответствующие индексы в соответствующем месте кода.
  • Сделаем наш индикатор универсальным. Пусть он умеет отображать оба варианта, и какой выбрать, будет определять пользователь с помощью входных параметров.

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

  • с использованием других типов циклов;
  • с использованием оператора switch-case вместо некоторых (или всех) операторов условия.

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

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


Логические выражения в подробностях

Напомню, данные булевского типа (bool) имеют всего два значения: либо true (истина), либо false (ложь).

Можно сказать, что в каждом случае применения логических операторов программа задаёт вопрос своим текущим данным: "А правда ли, что (условие)?". ("Правда ли, что сейчас (prev_calculated == rates_total)? Если да, то мне пора завершить работу текущей функции! А иначе, мне придётся снова что-то считать…".)

В языке MQL5 для представления значения false используется число 0, соответственно, все остальные числа в логических выражениях будут преобразованы к значению true, и неважно, положительные они или отрицательные.

Строки к типу bool преобразовать нельзя, при попытке такой конвертации получим сообщение компилятора об ошибке. Но их тоже можно сравнивать.

Чаще всего логические данные получают из логических выражений. Простейший случай такого выражения — операция сравнения. Напомню, в MQL5 используются такие же операторы сравнения, как в школьной математике: больше (>), меньше(<), равно(==), не равно (!=), больше или равно (>=), меньше или равно (<=). Соответственно, запись (a==b) примет значение "истина" тогда и только тогда, когда оба элемента выражения  равны между собой. Это, думаю, понятно.

При сравнении строк, больше будет та строка, которая имеет в начале символы с большими номерами в таблице символов. Например, ("A" > "1") истинно, а ("Y" <  "A") ложно. Если в строке много символов, принцип сохраняется.  Просто сначала сравниваются самые первые символы, потом — вторые, и так далее, до расхождения. При этом пустая строка ("") меньше всех остальных, то есть, если в какой-то строке меньше символов, но начальные символы совпадают, то при сравнении она будет считаться меньшей.

Существуют еще три логических операции:

  • Логическое отрицание (оно же — операция "не", оно же — инверсия), обозначается восклицательным знаком (!);
    превращает логическое значение в противоположное.
  • Логическое умножение (оно же — операция "и", оно же — конъюнкция), обозначается двумя символами амперсанда, идущими подряд (&&);
    выражение с этим оператором будет истиной только в том случае, если обе части выражения — и левая, и правая — истинны.
  • Логическое сложение (оно же — операция "или", оно же — дизъюнкция), обозначается двумя символами вертикальной черты (||);
    выражение с этим оператором будет истиной, если хотя бы одна его часть истинна.
Рассмотреть все случаи взаимодействия переменных с использованием этих операторов можно с помощью таблиц:
Таблица 1. Логическое отрицание (!)
Таблица 2. Логическое умножение (&&)
Таблица 3. Логическое сложение (||)
not
and
or

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

int a = 3;
int b = 5; 

bool e = ( a*b ) > ( a+b+b );          // true

Print ( (a>b) );                       // false
Print ( (a<b) || ((a/b) > (3/2)) );    // true
Print ( !a  );                         // false
Print ( ! (!e) );                      // true
Print ( (b>a) && ((a<=3) || (b<=3)) ); // true
Print ( "Trust" > "Training" );        // true
Print ( a==b );                        // false
Print ( b=a );                         // 3 (!!!) (следовательно, для любого логического оператора это всегда true)
//  В последнем примере допущена очень частая и трудноуловимая ошибка. 
//    Вместо сравнения использовано присваивание, поэтому тип результата — int! 
//  К счастью, компилятор чаще всего предупредит о такой подмене.

Пример 1. Примеры использования логических операторов

Если в выражении встречается несколько операторов без скобок, они, как правило, будут выполняться слева направо (кроме присваивания), и порядок выполнения будет следующим:

  1. Сначала будут выполнены все арифметические операторы
    • *, /, %
    • +, -
  2. Затем будут выполняться операторы сравнения (==, !=, >, <, >=, <=).
  3. И затем будут выполняться остальные логические операторы, в том порядке, в котором они перечислены:
    • Инверсия (!);
    • Конъюнкция (&&);
    • Дизъюнкция (||);
  4. Оператор присваивания (=) будет выполнен самым последним.

Если вы не уверены, что в вашем коде будет выполняться раньше — используйте скобки, как в моём примере 1. Они всегда помогут, потому, что операции в скобках выполняются в первую очередь. Да и наглядней это, если делать отступы…


Оператор if

Оператор if также называют оператором условия, ветвления, развилки… В переводе на русский "if" обозначает "если", "else" — "иначе".

По форме он очень прост:

if (условие) действие_если_условие_истинно;
else действие_во_всех_остальных_случаях; // Эту ветку можно пропустить

Пример 2. Шаблон оператора условия.

Почему "ветвление"? Если представить этот оператор графически, получится что-то такое:

Оператор ветвления

Рисунок 1. Оператор ветвления

Если у вас хорошее воображение, можно увидеть, как из основного "ствола", благодаря этому оператору, "вырастает" дополнительный "отросток" (ветвь). На более старых (с точки зрения времени, когда их придумали) схемах так и вообще алгоритмы напоминают целые "кусты". Например, что делать, если вы хотите почитать книгу, но не горит настольная лампа? Сначала попробовать лампу включить — вдруг загорится? Если не загорится, проверить, а есть ли лампа в патроне — вдруг ребёнок выкрутил, потому, что ему было нужнее? Если лампа есть, и она не включается, проверить, не перегорела ли она. Если перегорела, попробовать поменять. Ну и если все эти меры не помогли, можно попробовать взять другую лампу. Этот алгоритм можно нарисовать в виде такой схемы:

Иллюстрация ветвлений

Рисунок 2. Иллюстрация ветвлений

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

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

Поясню. В языке MQL5 действия можно заключать (а можно и нет) в фигурные скобки. Фигурные скобки являются как бы отдельной "сущностью" и ограничивают отдельный блок кода. Переменные, объявленные внутри этого блока, другим блокам не видны. В некоторых случаях фигурные скобки жестко предписаны синтаксисом, но очень часто их можно расставлять по коду почти произвольно. Содержимое этих скобок будет восприниматься остальными частями программы единым целым. Например, переменные, описанные внутри блока, не будут видны никаким операторам снаружи. Вот такой "блок" из фигурных скобок я и называю "составным оператором". Внутри него может быть сколько угодно действий, но команды типа if будут выполнять их, пока не встретят закрывающую основной блок фигурную скобку. Однако если фигурных скобок нет, при выполнении условия будет исполнен только один оператор, а все остальные будут выполняться "на общих основаниях".

Часто про команды, подчинённые какому-нибудь оператору (в нашем случае if или else), говорят "тело оператора".

В шаблоне из примера 2 телом оператора if будет "действие_если_условие_истинно", а телом оператора else, соответственно, — "действие_во_всех_остальных_случаях".

Вот практический пример. Этот код проверяет новую свечу в индикаторе, шаблон которого приведён в примере 16. Алгоритм основан на сравнении количества уже рассчитанных свечей (prev_calculated) и общего количества свечей на графике (rates_total):

if (prev_calculated == rates_total) // _Если_ эти значения действительно равны, 
    
  {                                 //    _то_ делать ничего не нужно, ждём новую свечу
    Comment("Nothing to do");       // Сообщаем, что делать нечего, в комментарии (чтобы не забивать журнал)
    return(rates_total);            // Раз делать ничего не нужно, выходим из функции 
                                    //   и сообщаем терминалу, что все бары уже посчитаны (возвращая rates_total)
  }

// Пришла новая свеча. Всё, что надо делать, делаем здесь
Comment("");                        // Очищаем комментарии
Print("I can start to do anything");   // Выводим сообщение в журнал


// Поскольку _всё_, что совершается в данной функции, будет выполняться 
//   только в случае неравенства  prev_calculated и rates_total, 
//   специальная ветка else в данном примере не нужна.
// Просто начинаем выполнять нужную работу.

Пример 3. Пример ожидания новой свечи при помощи сравнения prev_calculated и rates_total

Здесь в тело оператора if входит две команды: Comment и return. Оператор else здесь опущен.

Экспериментируя с этим примером, можно увидеть тот же принцип, что был описан выше. Внутри фигурных скобок мы можем использовать сколько угодно операторов. Но если эти скобки убрать, то по условию будет выполнен только вызов функции Comment("Nothing to do"). Оператор return в этом случае будет выполняться безусловно, и сообщение о готовности к работе так никогда и не появится (рекомендую проверить на минутном графике).

Моя рекомендация по кодированию тела операторов такова:

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

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


Тернарный оператор

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

int a = 9;
int b = 45;
string message;

if( (b%a) == 0 ) // (1)
  {
    message = "b divides by a without remainder"; // (2)
  }
else 
  {
    message = "b is NOT divisible by a"; // (3)
  }

Print (message);

Пример 4. Пример условия, подходящего для преобразования в тернарный оператор

В этом случае можно слегка сократить запись с помощью тернарного (состоящего из трёх частей) оператора:

int a = 9;
int b = 45;
string message;

message = ( (b%a) == 0 ) /* (1) */ ? 
          "b divides by a without remainder" /* (2) */ : 
          "b is NOT divisible by a" /* (3) */ ;
  
Print (message);

Пример 5. Тернарный оператор

Здесь та же форма, что и в операторе if: условие -> (знак вопроса)  -> значение_если_условие_истинно -> (двоеточие — вместо else) -> значение_ если_условие_ложно. Понятно, что никто не может помешать вам использовать вложенные тернарные операторы, если это уместно для вашей задачи.

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


Операторы switch — case

Бывают задачи, когда нужно выбирать не из двух, а из гораздо большего числа вариантов. Можно, конечно, организовать много вложенных операторов if, но наглядность кода при этом заметно упадёт. Для таких задач существует оператор switch (переключатель). Он строится по следующему шаблону:

switch (переменная_целого_типа) 
  {
    case значение_1:
      список_операций_1;
    case значение_2
      список_операций_2;
    // …
    default:
      список_операций_по_умолчанию;
  }

Пример 6. Структура оператора switch-case

Работает это так. Если значение "переменная_целого_типа" совпадает с одним из значений ("значение_1", "значение_2" …), то выполняется список операций после этого  совпадения, а потом — все последующие списки операций. Чаще всего нам это не нужно, достаточно выполнить только один блок. Поэтому после  каждого списка операций чаще всего мы будем добавлять оператор break, который сразу завершит работу оператора switch. Если ни один из вариантов не совпал, будет выполнена секция default. Эта секция всегда должна располагаться в конце списка.

В MQL5 оператор switch-case чаще всего используется для обработки ошибок торговли в советниках, а также для работы с событиями типа нажатия клавиш на клавиатуре или движения мыши. В качестве примера приведу типичный фрагмент кода функции для обработки ошибок:

void PrintErrorDescription()
{

  int lastError = GetLastError();

// Если (lastError == 0), значит, ошибок нет…
  if(lastError == 0)
    {
      return;  // …и незачем нагружать процессор  лишними вычислениями.
    }

// А вот если ошибки есть, выведем в журнал сообщение с расшифровкой.        
  switch(lastError)
    {
      case ERR_INTERNAL_ERROR:         
        Print("Unexpected internal error"); // Конечно, здесь можно выполнять любые другие необходимые действия
        break;                
      case ERR_WRONG_INTERNAL_PARAMETER:
        Print("Wrong parameter in the inner call of the client terminal function");
        break;
      case ERR_INVALID_PARAMETER:
        Print("Wrong parameter when calling the system function");
        break;
      case ERR_NOT_ENOUGH_MEMORY:
        Print("Not enough memory to perform the system function");
        break;

      default: 
        Print("I don't know anything about this error");
    } 
}

Пример 7. Одна из типичных схем для обработки ошибок


Оператор цикла с предусловием (while)

Цикл — это оператор, совершающий повторяющиеся действия.

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

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

Первый оператор — цикл while (пока). Часто его называют "цикл с предусловием", потому, что проверка условия в нём идёт до выполнения всех остальных действий из тела цикла. Графически он выглядит так:

Схема цикла while

Рисунок 3. Схема цикла while

Шаблон кода этого оператора очень прост:

while (условие)
    действие_если_условие_истинно;

Пример 8. Шаблон цикла while

Здесь "действие_если_условие_истинно" — один оператор, простой или составной.

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

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

В любом случае внутри тела цикла должен меняться минимум один параметр, который влияет на условие в круглых скобках. Если такого параметра не будет, цикл станет бесконечным, и тогда прервать его можно будет только закрыв окно терминала. При работе бесконечного цикла, закрыть программу, его выполняющую, иногда может оказаться не совсем тривиальной задачей, потому что процессор может оказаться занят только нашей программой, и на другие задачи вроде реакции на нажатия кнопок на клавиатуре или движения мыши ему времени не останется. В этом, самом плохом, случае поможет только аппаратная перезагрузка компьютера (кнопкой). Поэтому внимательно проверяйте свои циклы, убедитесь, что оставили программе хоть одну возможность его завершить. Например, добавьте к проверке условие (&& ! IsStopped() ), которое остановит работу цикла, если программа будет завершаться.

Чтобы сравнить форму записи разных циклов, для примера возьмём одну и ту же задачу суммирования чисел от 1 до 5. Допустим, что мы не знаем о прогрессиях.

С помощью цикла while эта задача решается так:

//--- Объявляем переменные
int a  = 1;  // инициализируем (!) изменяемый параметр, используемый в условии
int sum = 0; // результат работы
string message = "The sum of "+ (string) a; // сообщение для пользователя

//--- Выполняем основную работу
while (a <= 5)  // Пока a меньше или равно 5
  {
    sum += a; // Добавляем значение к сумме
    if ( a != 1) // первое значение уже добавлено в момент инициализации
    {
      message += " + " + string (a); // Остальные добавляем в процессе работы
    }
    
    a++; // Изменяем параметр (очень важно об этом помнить!)
  }

//-- После завершения цикла выводим сообщение пользователю
message += " is " + (string) sum;  // Дооформляем текст
Comment (message); // Показываем комментарий

Пример 9. Использование цикла while

Здесь, конечно, количество проходов можно легко посчитать. Но чтобы понять форму работы, думаю, пример всё-таки подходит.


Цикл с постусловием (do… while)

Цикл do… while (выполнять… пока) использует проверку условия после отработки всех действий своего тела. Графически этот цикл можно изобразить следующим образом:

Схема цикла do... while

Рисунок 4. Схема цикла do-while

Шаблон оператора в коде ничуть не сложнее предыдущего:

do
  действия_если_условие_истинно;
while (условие);

Пример 10. Шаблон оператора do-while

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

Задач, в которых удобна именно такая форма цикла, не так много, как для цикла с предусловием, но они тоже встречаются. В нашей области это могла бы быть, например, проверка торговых инструментов в окне обзора рынка (Market watch). Алгоритм мог бы выглядеть следующим образом:

  • взять первый инструмент из списка (если мы твёрдо уверены, что он точно существует), 
  • сделать с ним то, что нам надо (например, проверить установленные приказы) 
  • и потом проверить, есть ли в списке еще и другие инструменты;
  • если есть, нужно взять второй инструмент, потом — третий… Ну, и так — до тех пор, пока список не исчерпается.

Задача с суммой с этой формой цикла будет выглядеть почти так же, как в предыдущем случае. Будут заменены только две строки — описание самого цикла:

//--- Объявляем переменные
int a  = 1;  // инициализируем (!) изменяемый параметр, используемый в условии
int sum = 0; // результат работы
string message = "The sum of "+ (string) a; // сообщение для пользователя

//--- Выполняем основную работу
do // Выполнять
  {
    sum += a;    // Добавляем значение к сумме
    if ( a != 1) // первое значение уже добавлено в момент инициализации
    {
      message += " + " + string (a); // Остальные добавляем в процессе работы
    }
    
    a++;         // Изменяем параметр (очень важно об этом помнить!)
  }
while (a <= 5)  // Пока a меньше или равно 5

//-- После завершения цикла выводим сообщение пользователю
message += " is " + (string) sum;  // Дооформляем текст
Comment (message);                 // Показываем комментарий

Пример 11. Использование цикла do-while


Цикл for

Цикл for (для) является самым часто употребимым, потому что используется для перебора всяких последовательностей, в ситуациях, где можно легко вычислить количество перебираемых элементов. Вот так можно описать шаблон для его записи:

for (инициализация_счетчика ; условие ; изменение_счетчика)
  действие_если_условие_истинно;

Пример 12. Шаблон для цикла for

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

По своей сути этот цикл — такой же, как while, то есть в нём проверка выполняется также до начала выполнения операторов тела. Задача о сумме чисел с применением этого цикла могла бы быть решена так:

//--- Объявляем переменные
int a;   // НЕ инициализируем изменяемый параметр, просто описываем 
         //   (и то — не обязательно, можно описать прямо в заголовке цикла)
int sum = 0;                    // результат работы
string message = "The sum of "; // сообщение для пользователя здесь чуть сокращаем, 
                                //   поскольку значение a еще неизвестно

//--- Выполняем основную работу
for (a=1; a<=5; a++)// Для (каждого a от 1 до 5) /заголовок цикла/
  {
    sum += a;    // Добавляем значение a к сумме
    if ( a != 1) // первое значение не содержит знака "+" 
    {
      message += " + " // Добавляем знак "+" перед всеми членами последовательности, начиная с 2
    } 
   message += string (a); // Добавляем к сообщению элементы последовательности
    
            // Изменения счетчика описываются не здесь, а в заголовке цикла последним параметром.
  }

//-- После завершения цикла выводим сообщение пользователю
message += " is " + (string) sum;  // Дооформляем текст
Comment (message);                 // Показываем комментарий

Пример 13. Использование цикла for

В этом примере мы видим гораздо больше изменений по сравнению с предыдущими (выделены жёлтым цветом). Еще раз обращаю ваше внимание на то, что здесь все шаги по работе со счетчиком (изменяемым параметром) вынесены в заголовок: инициализация (a=1), проверка условия для выполнения цикла (a<=5) (если условие истинно, выполняется основная работа — тело цикла) — и затем, в конце, меняется значение (a++) и снова выполняется проверка условия.


Операторы break и continue

Иногда бывает, что нам не нужно выполнять все шаги цикла.

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

string message = "";

for (int i=0; i<=3; i++)
  {
    if ( i == 2 )
      {
        break;
      }
    
    message += " " + IntegerToString( i );
  }

Print(message); // Результат: 0 1 (остальные повторы не выполнятся)

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

А еще бывают случаи, когда при определённых условиях не нужно выполнять тело цикла, но цикл не завершается — просто пропускаются определённые ситуации. Например, если в примере 14 мы не хотим выводить двойку, но все остальные цифры нам нужны, код нужно немного подправить, заменив break на continue (продолжить). Оператор continue сразу заставляет программу перейти к проверке следующего условия для циклов while и do-while или к следующему изменению параметра для цикла for.

string message = "";

for (int i=0; i<=3; i++)
  {
    if ( i == 2 )
      {
        continue;
      }
    
    message += " " + IntegerToString( i );
  }

Print(message); // Результат: 0 1 3 (двойка пропущена)

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

Оператор break можно использовать или внутри цикла, или в операторах case; оператор continue — только внутри циклов.

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


Пара слов о мастере создания файлов (в режиме создания индикаторов)

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

Первое окно мастера я пропущу. Там ничего интересного или нового по сравнению с первой статьёй нет. Второе окно тоже не ново, но теперь вы уже знаете о глобальных настройках программы, которые можно организовать с помощью input-параметров. Иногда эти параметры нагляднее ввести именно с помощью мастера, раз уж им всё равно воспользовались… Напомню, что имя параметра может быть любым. Я предпочитаю добавлять префикс inp_, чтобы при просмотре кода сразу видеть, где именно я эти параметры использую.

Добавление входных параметров

Рисунок 5. Добавление параметров программы с помощью мастера

Второй акцент, который я хочу сделать — это выбор формы метода OnCalculate (рисунок 6). Напомню, метод OnCalculate — это основной метод любого индикатора на языке MQL5. Он вызывается каждый раз, когда терминал рассчитывает этот индикатор. Так вот, эта функция может принимать разное количество входных параметров, в зависимости от вашего выбора на этом шаге мастера.

Выбор формы метода OnCalculate

Рисунок 6. Выбор формы метода OnCalculate

Выбор верхнего варианта подходит для большинства ситуаций, поскольку функция в этом случае принимает автоматически генерируемые массивы open, high, low, close, volume, time — и пользователь дальше может делать с ними что хочет.

Однако бывают специальные случаи, когда мы хотим предоставить пользователю возможность выбирать, на основе какой кривой рассчитывать данный индикатор. Например, индикатор скользящей средней можно рассчитать на основе цен минимума, максимума, или вообще на основе кривой уже существующего на графике индикатора. Вот в этом случае мы должны выбрать нижний вариант, и тогда в OnCalculate будет передан массив с данными кривой, а в скомпилированном индикаторе в окне входных параметров появится вкладка "параметры" — именно для выбора нужной кривой. На рисунке 7 показаны стартовые окна готовых индикаторов для верхнего (Upper) и нижнего (Lower) вариантов.

Сравнение стартовых окон индикаторов с разной формой OnCalculate

Рисунок 7. Сравнение стартовых окон индикаторов с разной формой OnCalculate

И последний момент в мастере, на который я хочу обратить ваше внимание. На последнем шаге диалогового окна при создании индикатора можно выбрать, как именно индикатор будет отображаться (рисунок 8). Для отображения индикатора необходимо добавить так называемый "буфер" — специальный массив, содержащий данные для рисования. Вариантов рисования много: от простой линии (как у скользящей средней) до разных многоцветных гистограмм и свечей. А при очень большом желании можно даже рисовать свои картинки (но этого, конечно, мастер не предлагает).

Параметры рисования индикатора

Рисунок 8. Параметры рисования индикатора

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

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

Буферы можно добавить и вручную, но тогда нужно будет проделать дополнительную несложную, но монотонную работу. Сейчас, для начинающих программистов, я рекомендую буферы всё же добавлять с помощью мастера.

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

И вот теперь просто создадим индикатор

  • с названием InsideOutsideBar, 
  • с одним параметром, который будет называться inp_barsTypeSelector, иметь тип int и значение по умолчанию 0, 
  • выберем для индикатора верхний формат функции OnCalculate (там, где список массивов) на третьем экране мастера,
  • и добавим два буфера рисования в разделе "Отрисовка" ("Plots" на рисунке 8) с названиями  Up и Down и типом отрисовки "Стрелка" (Arrow).


Разбор сгенерированного мастером кода индикатора

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

//+------------------------------------------------------------------+
//|                                             InsideOutsideBar.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
#property indicator_buffers 2
#property indicator_plots   2
//--- plot Up
#property indicator_label1  "Up"
#property indicator_type1   DRAW_ARROW
#property indicator_color1  clrMediumPurple
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- plot Down
#property indicator_label2  "Down"
#property indicator_type2   DRAW_ARROW
#property indicator_color2  clrMediumPurple
#property indicator_style2  STYLE_SOLID
#property indicator_width2  1
//--- input parameters
input int      inp_barsTypeSelector=0;
//--- indicator buffers
double         UpBuffer[];
double         DownBuffer[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,UpBuffer,INDICATOR_DATA);
   SetIndexBuffer(1,DownBuffer,INDICATOR_DATA);
//--- setting a code from the Wingdings charset as the property of PLOT_ARROW
   PlotIndexSetInteger(0,PLOT_ARROW,159);
   PlotIndexSetInteger(1,PLOT_ARROW,159);
   
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
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);
  }
//+------------------------------------------------------------------+

Пример 16. Код индикатора, сгенерированный мастером создания кода MQL5

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

#property indicator_buffers 2
#property indicator_plots   2

Пример 17. Описание буферов индикатора для вычисления и для рисования

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

//--- plot Up
#property indicator_label1  "Up"             // Отображаемое название буфера 
#property indicator_type1   DRAW_ARROW       // Тип рисунка - стрелка
#property indicator_color1  clrMediumPurple  // Цвет стрелки
#property indicator_style1  STYLE_SOLID      // Стиль линии - сплошная
#property indicator_width1  1                // Толщина линии (размер стрелки)
//--- plot Down
#property indicator_label2  "Down"
#property indicator_type2   DRAW_ARROW
#property indicator_color2  clrMediumPurple
#property indicator_style2  STYLE_SOLID
#property indicator_width2  1

Пример 18. Параметры отрисовки

В следующем блоке кода описаны входные параметры нашего индикатора. В нашем случае параметр всего один: inp_barsTypeSelector:

//--- input parameters
input int      inp_barsTypeSelector=0;

Пример 19. Входные параметры индикатора

Потом идёт код, создающий переменную для буферов отрисовки. Обратите внимание, что для этого буфера используется динамический массив с элементами типа double. Фактически, это массив уровней цен, по которым будет строиться кривая индикатора:

//--- indicator buffers
double         UpBuffer[];
double         DownBuffer[];

Пример 20. Переменная для буфера отрисовки

И дальше идёт описание двух функций: OnInit и OnCalculate.

OnInit — такая же точно функция, что и в скриптах. Она точно так же запускается сразу после старта индикатора — до выполнения всех остальных функций — и выполняется ровно один раз. В данном случае в ней мастер прописал два вызова стандартной функции SetIndexBuffer, предназначенной для связывания нашего массива с буфером рисования индикатора. Кроме того, обеим стрелкам были назначены значки с помощью функции PlotIndexSetInteger. Функция OnInit не принимает никаких параметров и возвращает статус успешности инициализации (в данном случае — всегда "успешно").

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

Функция принимает в качестве параметров множество переменных:

  • общее количество баров на графике (rates_total);
  • количество ранее рассчитанных индикатором баров (prev_calculated). При первом запуске значение prev_calculated равно нулю, однако та  реализация функции, что создал мастер, в конце работы возвращает общее количество баров на данном тике, и в начале следующего запуска терминал передаст функции уже это значение в качестве prev_calculated. На этом, кстати, строится один из известных мне алгоритмов для определения новой свечи: если rates_total==prev_calculated, то значит, свеча та же самая и считать ничего не надо, а вот если не равны — то запускаем дальнейшие шаги;
  • множество массивов: цены, время, объёмы, спреды… По сути, это всё, что может понадобиться трейдеру, если он пишет свой индикатор и не использует чужие. Обратите внимание, что массивы передаются по ссылке (поскольку по значению массивы передавать нельзя), однако имеют модификатор const, сообщающий компилятору, что данные в этих массивах неизменяемые.

И теперь мы можем начать программировать наш индикатор.


Программирование индикатора

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

enum BarsTypeSelector
 {
  Inside  = 0, // Inside bar
  Outside = 1  // Outside bar
 };

Пример 21. Перечисление для типа расчётов

И поменяем тип и значение по умолчанию для нашего входного параметра:

//--- input parameters
input BarsTypeSelector      inp_barsTypeSelector=Inside;

Пример 22. Изменения типа и значения по умолчанию для входного параметра

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

//--- input parameters
input BarsTypeSelector      inp_barsTypeSelector=Inside;
input int                   inp_arrowCode=159; 
input int                   inp_arrowShift=5;

Пример 23. Дополнительные переменные для настроек внешнего вида

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

int OnInit()
 {
//--- indicator buffers mapping
  SetIndexBuffer(0,UpBuffer,INDICATOR_DATA);
  SetIndexBuffer(1,DownBuffer,INDICATOR_DATA);
//--- setting a code from the Wingdings charset as the property of PLOT_ARROW
  PlotIndexSetInteger(0,PLOT_ARROW,inp_arrowCode);
  PlotIndexSetInteger(1,PLOT_ARROW,inp_arrowCode);
  
  PlotIndexSetInteger(0, PLOT_ARROW_SHIFT, inp_arrowShift); 
  PlotIndexSetInteger(1, PLOT_ARROW_SHIFT, -inp_arrowShift);

//---
  return(INIT_SUCCEEDED);
 }

Пример 24. Изменения функции OnInit

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

/********************************************************************************************
 *                                                                                          *
 * Внимание! Все массивы в коде НЕ являются сериями, поэтому справа у нас — бОльшие номера. *
 *                                                                                          *
 ********************************************************************************************/

//--- Описание переменных
  int i,     // Счетчик цикла
      start; // Начальный бар для исторических данных

  const int barsInPattern = 2; // Для правильной подготовки нам нужно знать,
                               //   сколько баров будет задействовано при одной проверка
                               //   Константу я ввёл, чтобы не было в коде 
                               //   непонятно откуда взявшихся "магических чисел",
                               //   и вместо неё можно было бы воспользоваться директивой #define

//--- Проверим граничные условия и установим начало отсчёта
  if(rates_total < barsInPattern)      // Если не хватает баров для работы
    return(0);                         // Просто ничего не делаем

  if(prev_calculated < barsInPattern+1)// Если ничего еще не было посчитано
   {
    start = barsInPattern;     // Устанавливаем минимально возможное количество баров для начала поиска 
   }
  else
   {
    start = rates_total — barsInPattern; // Если же индикатор работает какое-то время,
                                         //   То достаточно посчитать два последних бара
   }

//--- Чтобы избежать странных артефактов на последней свече, инициализируем последние элементы массивов 
//      значением  EMPTY_VALUE
  UpBuffer[rates_total-1] = EMPTY_VALUE;
  DownBuffer[rates_total-1] = EMPTY_VALUE;

//---
  for(i = start; i<rates_total-1; i++) // Начинаем считать от стартовой позиции 
                                       //   и продолжаем до тех пор, пока закрытые бары не кончатся
                                       // (Если бы хотели охватить и последний (незакрытый) бар, 
                                       //   поставили бы условие i<=rates_total-1)
   {
    // Для начала очистим оба буфера индикатора (инициализируем пустым значением) 
    UpBuffer[i] = EMPTY_VALUE;
    DownBuffer[i] = EMPTY_VALUE;
    
    if(inp_barsTypeSelector==Inside) // Если пользователь хочет отображать внутренние бары
     {
      // Проверяем, не является ли текущий бар внутренним
      if(high[i] <= high[i-1] && low[i] >= low[i-1])
       {
        // И если да, отмечаем предыдущую (бОльшую) свечу
        UpBuffer[i-1] = high[i-1];
        DownBuffer[i-1] = low[i-1];
       }
     }
    else // Если же нужны внешние бары
     {
      // Проверяем, не является ли текущий бар внешним
      if(high[i] >= high[i-1] && low[i] <= low[i-1])
       {
        // И отмечаем текущую свечу, если необходимо
        UpBuffer[i] = high[i];
        DownBuffer[i] = low[i];
       }
     }
   }

//--- return value of prev_calculated for next call
  return(rates_total);

Пример 25. Тело функции OnCalculate

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

На рисунке 9 показана работа данного индикатора. Слева розовым цветом отмечены бары-предшественники внутренних баров, справа — внешние бары.

Работа индикатора InsideOutsideBar

Рисунок 9. Работа индикатора InsideOutsideBar. Слева — внутренние бары, справа — внешние


Заключение

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

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

Список предыдущих статей цикла:

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (2)
Juan Luis De Frutos Blanco
Juan Luis De Frutos Blanco | 31 авг. 2024 в 09:38

Buenos días Fedorov,

Debo agradecerte el esfuerzo en tus artículos: Me están resultando muy útiles. 

Saludos,

Oleh Fedorov
Oleh Fedorov | 1 сент. 2024 в 12:59
Juan Luis De Frutos Blanco #:

Buenos días Fedorov,

Debo agradecerte el esfuerzo en tus artículos: Me están resultando muy útiles. 

Saludos,

Gracias, señor.
Нейросети в трейдинге: Иерархический векторный Transformer (Окончание) Нейросети в трейдинге: Иерархический векторный Transformer (Окончание)
Продолжаем изучение метода Иерархического Векторного Transformer. И в данной статье мы завершим построение модели. А также проведем её обучение и тестирование на реальных исторических данных.
Поиск с запретами — Tabu Search (TS) Поиск с запретами — Tabu Search (TS)
В статье рассматривается алгоритм табу-поиска — один из первых и наиболее известных методов метаэвристики. Мы подробно разберем, как работает алгоритм, начиная с выбора начального решения и исследования соседних вариантов, с акцентом на использование табу-листа. Статья охватывает ключевые аспекты алгоритма и его особенности.
Универсальная формула оптимизации (GOF) при реализации режима Custom Max с ограничениями Универсальная формула оптимизации (GOF) при реализации режима Custom Max с ограничениями
В статье представлен способ реализации задач оптимизации с несколькими целями и ограничениями при выборе режима Custom Max в настройках терминала MetaTrader 5. Например, задача оптимизации может быть следующей: максимизировать фактор прибыли, чистую прибыль и фактор восстановления таким образом, чтобы просадка была менее 10%, количество последовательных убытков было менее 5, а количество сделок в неделю было более 5.
Разработка системы репликации (Часть 45): Проект Chart Trade (IV) Разработка системы репликации (Часть 45): Проект Chart Trade (IV)
Главное в этой статье — представление и объяснение класса C_ChartFloatingRAD. У нас есть индикатор Chart Trade, который работает довольно интересным образом. Как вы могли заметить, у нас на графике все еще достаточно небольшое количество объектов, и тем не менее, мы получили ожидаемое функционирование. Значения, присутствующие в индикаторе, можно редактировать. Вопрос в том, как это возможно? В этой статье все начнет проясняться.