Знакомство с MQL5: написание простого советника и индикатора

Denis Zyatkevich | 16 марта, 2010


Введение

Язык программирования MetaQuotes Language 5 (MQL5), входящий в торговый терминал MetaTrader 5, обладает новыми возможностями и более высоким быстродействием, по сравнению с языком MetaQuotes Language 4 (MQL4). Эта статья поможет познакомиться с новым языком программирования. В ней приведен пример написания простого советника и индикатора, также описывается язык MQL5 в объеме, необходимом для понимания приведенных примеров.

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


Знакомство с MQL5

Торговая платформа MetaTrader 5 позволяет проводить анализ финансовых инструментов и вести торговлю, как вручную, так и в автоматическом режиме. MetaTrader 5 имеет некоторые отличия от своего предшественника MetaTrader 4, в частности, уточнены такие понятия, как позиция, сделка, ордер.

Язык программирования MQL5, встроенный в торговый терминал MetaTrader 5, позволяет писать программы, имеющие различное назначение:

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

Для написания программы (советника, индикатора или скрипта) необходимо запустить торговый терминал MetaTrader 5 и в меню Сервис выбрать пункт Редактор MetaQuotes Language или нажать клавишу F4.


Рис.1. Вызов редактора MetaEditor

В появившемся окне программы MetaEditor необходимо в меню Файл выбрать пункт Создать или нажать комбинацию клавиш Ctrl+N.


Рис.2. Создание новой программы

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


Рис.3. Мастер MQL5

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


Рис.4. Задание общих параметров создаваемой программы

После этого создается шаблон программы (советника, индикатора, скрипта), который можно редактировать и наполнять:


Рис.5. Шаблон новой программы

После написания текста программы, ее необходимо скомпилировать. Для этого в меню Файл нужно выбрать пункт Компилировать или нажать клавишу F7:

Рис.6. Компиляция

Если в тексте программы отсутствуют ошибки, будет создан файл с расширением .ex5. После этого новый советник, индикатор или скрипт можно прикреплять к графику в терминале MetaTrader 5 для исполнения.

Программа на языке MQL5 представляет собой набор операторов. Каждый оператор заканчивается символом "точка с запятой (;)".  Для удобства, в тексте программы можно указывать комментарии, они располагаются между комбинациями символов "/*" и "*/" или от "//" до конца строки. Язык MQL5 является событийно-ориентированным, это означает, что при наступлении определенных событий (запуск или удаление программы, приход новой котировки, и т.д.) запускается соответствующая функция (подпрограмма), написанная пользователем, которая выполняет определенные действия. В MQL5 существуют следующие предопределенные события:

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

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


Торговая система

Торговая система, используемая в данной статье в качестве примера, основана на том предположении, что утром открываются финансовые организации в Европе, а позже и в США, выходят экономические новости, что приводит к возникновению направленного движения валютной пары EUR/USD. Период графика не имеет значения, но рекомендуется использовать минутный график, так как на нем виден день (или часть дня) и удобно наблюдать за происходящим.

 

Рис.7. Торговая система

В 7 часов утра (по серверному времени) выставляются отложенные ордера Buy Stop и Sell Stop на расстоянии 1 пункт за диапазоном цен текущего дня (Buy Stop выставляется с учетом спрэда). Уровни StopLoss выставляются за противоположной границей диапазона. После срабатывания ордера, его StopLoss перемещается на простую скользящую среднюю только в том случае, если при этом он окажется в "прибыльной" зоне. 

Такое сопровождение позиции имеет преимущество по сравнению с классическим скользящим стопом (Trailing Stop) в том, что удается избежать досрочного закрытия позиции при резких скачках цены, сопровождающихся большими откатами, но, в то же время, это способствует закрытию позиции при завершении направленного и возникновении бокового движения. Простая скользящая средняя вычисляется по данным минутного графика и имеет период усреднения 240.

Уровень фиксации прибыли зависит от текущей волатильности рынка. Для ее определения используется индикатор Average True Range (ATR) с периодом 5, вычисленный по данным дневного графика (таким образом, он показывает средний дневной диапазон цен за последнюю неделю). Для определения уровня TakeProfit длинной позиции, отложим от минимального значения цены текущего дня вверх значение индикатора ATR. Аналогично, для коротких позиций отложим от максимального значения цены текущего дня вниз значение индикатора ATR.

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


Пишем индикатор

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

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

#property copyright   "2010, MetaQuotes Software Corp."
#property link        "http://www.mql5.com"
#property description "Это индикатор, вычисляющий уровни TakeProfit на основе"
#property description "средней волатильности рынка. При расчете индикатора"
#property description "используется значение индикатора Average True Range (ATR),"
#property description "вычисленного по дневным ценовым данным. Значение"
#property description "индикатора откладывается от минимального и"
#property description "максимального значения цены за день."
#property version     "1.00"

Свойства copyright и link указывают информацию об авторе и его web-страницу, свойство description позволяет задать краткое описание, а свойство version позволяет указать версию программы. При вызове индикатора эта информация будет видна в следующем виде:


Рис.8. Информация об индикаторе

Для индикаторов необходимо указать расположение: на графике цены или в отдельном окне. Это можно сделать, указав свойство indicator_chart_window или indicator_separate_window:

#property indicator_chart_window

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

#property indicator_buffers 2
#property indicator_plots   2

Для каждой из линий индикатора укажем тип линии (свойство indicator_type), цвет (свойство indicator_color), стиль отрисовки (свойство indicator_style) и текстовую метку (свойство indicator_label):

#property indicator_type1   DRAW_LINE
#property indicator_color1  C'127,191,127'
#property indicator_style1  STYLE_SOLID
#property indicator_label1  "Buy TP"
#property indicator_type2   DRAW_LINE
#property indicator_color2  C'191,127,127'
#property indicator_style2  STYLE_SOLID
#property indicator_label2  "Sell TP"

Типы линии: DRAW_LINE - линия, DRAW_SECTION - отрезки, DRAW_HISTOGRAM - гистограмма, существуют и другие типы. Цвет указывается в виде яркости каждой из трех составляющих RGB, либо используется предопределенное имя цвета, например, Red, Green, Blue, White, Black... Стили отрисовки: STYLE_SOLID - сплошная линия, STYLE_DASH - прерывистая линия, STYLE_DOT - пунктирная линия, STYLE_DASHDOT - штрих-пунктирная линия, STYLE_DASHDOTDOT - штрих с двумя точками. Текстовая метка видна в окне Data Window, также она появляется во всплывающем окне при подведении курсора мыши к линии индикатора:

Рис.9. Описание линий индикатора

С помощью модификатора input укажем внешние переменные, значения которых пользователь сможет задавать при запуске индикатора, их тип и значения по умолчанию:

input int             ATRper       = 5;         //ATR Period
input ENUM_TIMEFRAMES ATRtimeframe = PERIOD_D1; //Indicator timeframe

Если указать в комментарии название параметра, оно будет видно при задании входных параметров вместо названия переменной:

 

Рис.10. Входные параметры индикатора

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

double bu[],bd[];
int hATR;

Массивы bu[] и bd[] будут использоваться для данных верхней и нижней линий индикатора. Используем динамические массивы (то есть без указания количества элементов), так как количество элементов заранее неизвестно (для них автоматически будет распределен необходимый размер). В переменной hATR будем хранить хэндл (указатель) встроенного технического индикатора ATR для обращения к нему.

Функция OnInit вызывается при запуске индикатора (наложении его на график).

void OnInit()
  {
   SetIndexBuffer(0,bu,INDICATOR_DATA);
   SetIndexBuffer(1,bd,INDICATOR_DATA);
   hATR=iATR(NULL,ATRtimeframe,ATRper);
  }

При помощи функции SetIndexBuffer укажем, что массивы bu[] и bd[] будут являться буферами индикатора и использоваться для хранения данных, которые отображаются в виде линий индикатора. Первый параметр задает номер индикаторного буфера, нумерация производится с нуля. Второй параметр указывает массив, который связан с индикаторным буфером. Третий параметр указывает тип данных, хранящихся в индикаторном массиве: INDICATOR_DATA - данные для отрисовки, INDICATOR_COLOR_INDEX - цвета отрисовки, INDICATOR_CALCULATIONS - вспомогательные буфера для промежуточных вычислений.

Полученный при вызове функции iATR хэндл индикатора ATR заносим в переменную hATR для обращения к индикатору в дальнейшем. Первый параметр функции iATR указывает символьное имя инструмента, NULL - для инструмента текущего графика. Второй параметр указывает период графика, данные которого используются при вычислении индикатора. Третий параметр указывает период усреднения индикатора ATR.

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

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[])

При вызове функции OnCalculate, в нее передаются следующие параметры: rates_total - общее количество баров на текущем графике, prev_calculated - количество баров, для которых уже рассчитаны значения индикатора, time[], open[], high[], low[], close[], tick_volume[], volume[], spread[] - массивы, содержащие для каждого бара значения времени открытия, цены открытия, максимальной и минимальной цены, цены закрытия, тиковый объем, торговый объем и спрэд соответственно. Чтобы сократить время расчета индикатора, не стоит пересчитывать значения, которые уже рассчитаны ранее и не изменились. После выполнения функции OnCalculate, в качестве параметра возвращается количество баров, для которых рассчитаны значения индикатора.

Тело функции OnCalculate заключено в фигурные скобки. Указываем локальные переменные, используемые в этой функции и их тип.

  {
   int i,day_n,day_t;
   double atr[],h_day,l_day;

Переменная i используется как счетчик цикла, переменные day_n и day_t - для хранения номера дня и временного хранения номера дня при расчете максимального и минимального значения цены за день. Массив atr[] используем для хранения значений индикатора ATR, а переменные h_day и l_day - для хранения максимального и минимального значения цены за день.

Скопируем значения индикатора ATR в массив atr[] при помощи функции CopyBuffer. В качестве первого параметра этой функции используем хэндл индикатора ATR, полученный ранее, второй параметр указывает номер буфера индикатора, нумерация происходит с нуля, индикатор ATR имеет всего один буфер. Третий параметр указывает номер элемента, с которого происходит копирование, индексация ведется от настоящего к прошлому, нулевой элемент соответствует текущему, незавершенному (незакрытому) бару. Четвертый параметр указывает количество элементов, которые будут скопированы.

Скопируем два элемента, так как нас интересует только предпоследний элемент, соответствующий последнему завершенному (закрытому) бару. Последним параметром указывается массив, в который копируются данные.

   CopyBuffer(hATR,0,0,2,atr);

Направление индексации в массиве определяется флагом AS_SERIES. Если он установлен (равен true), то массив является таймсерией, индексация элементов массива производится от самых свежих данных к самым старым. Если он сброшен (равен false), более старые данные имеют меньший индекс, более свежие - больший.

   ArraySetAsSeries(atr,true);

При помощи функции ArraySetAsSeries устанавливаем флаг AS_SERIES для массива atr[] (первый параметр указывает на массив, для которого изменяется значение флага, второй параметр - новое значение флага). Теперь значение индикатора для формирующегося в данный момент бара имеет индекс 0, для предыдущего, закрытого бара: 1.

Оператор for позволяет создать цикл.

   for(i=prev_calculated;i<rates_total;i++)
     {
      day_t=time[i]/PeriodSeconds(ATRtimeframe);
      if(day_n<day_t)
        {
         day_n=day_t;
         h_day=high[i];
         l_day=low[i];
        }
        else
        {
         if(high[i]>h_day) h_day=high[i];
         if(low[i]<l_day) l_day=low[i];
        }
      bu[i]=l_day+atr[1];
      bd[i]=h_day-atr[1];
     }

На первом месте в операторе for находится оператор, который выполняется один раз перед началом цикла. В данном случае, это оператор присваивания i=prev_calculated. На втором месте находится выражение, в данном случае это i<rates_total. Тело цикла выполняется до тех пор, пока это выражение истинно. На третьем месте находится оператор, который выполняется каждый раз после выполнения тела цикла. В данном случае, это i++ (эта запись равносильна i=i+1 и означает увеличение i на единицу).

В данном цикле переменная i изменяется от значения prev_calculated до значения rates_total-1 с шагом 1. Массивы исторических данных time[], high[] и low[] по умолчанию не являются таймсериями, нулевой индекс соответствует самому старому бару истории, а последний - формирующемуся в настоящий момент. В теле цикла мы перебираем их от первого нерассчитанного (соответствует prev_calculated) до последнего (соответствует rates_total-1) и для каждого из них рассчитываем значение линий создаваемого индикатора.

Время в массиве time[] хранится в виде количества секунд, прошедших с 00:00:00 1 января 1970 года. Если мы разделим его на количество секунд в дне (или другом периоде) - целая часть будет показывать номер дня, начиная с 1 января 1970 года (или номер другого временного периода). Функция PeriodSeconds возвращает количество секунд во временном периоде, который задается в качестве единственного параметра. Переменная day_t содержит номер дня, к которому относится бар, имеющий индекс i, а переменная day_n содержит номер дня, для которого в данный момент вычисляется максимальное и минимальное значение.

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

Переменные h_day и l_day содержат максимальное и минимальное значение цены за анализируемый день. В данном случае проверяется условие: если анализируемый бар относится к новому дню - начинаем заново считать максимальное и минимальное значение цены, иначе - продолжаем. Вычисляем значение каждой из линий индикатора: для верхней линии откладываем от минимума дня вверх значение индикатора ATR, для нижней линии откладываем от максимума дня вниз значение индикатора ATR.

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

   return(rates_total);
  }

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

void OnDeinit(const int reason)
  {
   IndicatorRelease(hATR);
  }

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

      day_t=time[i]/PeriodSeconds(ATRtimeframe);

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

Когда индикатор написан и скомпилирован, его можно накладывать на график цены в торговом терминале MetaTrader 5 или использовать его в других индикаторах, советниках и скриптах. По ссылке в конце статьи можно загрузить исходный текст этого индикатора.

Пишем советник

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

#property copyright   "2010, MetaQuotes Software Corp."
#property version     "1.00"
#property description "Этот советник выставляет отложенные ордера в промежуток"
#property description "времени с часа StartHour до часа EndHour на расстоянии"
#property description "1 пункт за диапазоном цен текущего дня. StopLoss"
#property description "каждого ордера расположен за противоположной границей"
#property description "диапазона цен. После срабатывания ордера выставляется"
#property description "TakeProfit на уровень, который показывает индикатор"
#property description "'indicator_TP', StopLoss переносится на SMA в том случае,"
#property description "если он окажется в 'прибыльной' зоне."

Назначение этих директив препроцессора уже рассмотрено при написании индикатора. В советнике они работают аналогично.

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

input int    StartHour = 7;
input int    EndHour   = 19;
input int    MAper     = 240;
input double Lots      = 0.1;

Параметры StartHour и EndHour задают промежуток времени (с которого и по который час), в течение которого должны быть установлены отложенные ордера. Параметр MAper задает период усреднения простой скользящей средней, по которой перемещается StopLoss открытой позиции при ее сопровождении. Параметр Lots задает объем финансового инструмента, который используется при торговле.

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

int hMA,hCI;

Переменная hMA будет использоваться для хранения хэндла индикатора MA, а переменная hCI - для хранения хэндла пользовательского индикатора (которым будет являться только что написанный индикатор).

Функция OnInit выполняется при запуске советника. 

void OnInit()
  {
   hMA=iMA(NULL,0,MAper,0,MODE_SMA,PRICE_CLOSE);
   hCI=iCustom(NULL,0,"indicator_TP");
  }

В ней получаем хэндлы индикатора MA и пользовательского индикатора. Использование функции iMA и ее параметров аналогично рассмотренной выше функции iATR. В функции iCustom первый параметр указывает символьное имя инструмента, NULL - инструмент текущего графика. Второй параметр - период графика, данные которого используются для вычисления индикатора, 0 - период текущего графика. В третьем параметре указывается имя файла, содержащего индикатор (без расширения). Путь к файлу указывается относительно директории MQL5\Indicators.

Создаем функцию OnTick, которая запускается на исполнение при поступлении каждой новой котировки:

void OnTick()
  {

Тело функции заключено в фигурные скобки.

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

   MqlTradeRequest request;
   MqlTradeResult  result;
   MqlDateTime     dt;

Предопределенная структура MqlTradeRequest содержит параметры ордеров и позиций, которые передаются функции OrderSend при осуществлении торговой деятельности. Структура  MqlTradeResult предназначена для данных, возвращаемых функцией OrderSend, они позволяют узнать о результате выполнения торговой операции. Предопределенная структура MqlDateTime предназначена для хранения данных о дате и времени.

Укажем локальные переменные, которые будем использовать в функции OnTick и их тип:

   bool bord=false, sord=false;
   int i;
   ulong ticket;
   datetime t[];
   double h[], l[], ma[], atr_h[], atr_l[],
          lev_h, lev_l, StopLoss,
          StopLevel=_Point*SymbolInfoInteger(Symbol(),SYMBOL_TRADE_STOPS_LEVEL),
          Spread   =NormalizeDouble(SymbolInfoDouble(Symbol(),SYMBOL_ASK) - SymbolInfoDouble(Symbol(),SYMBOL_BID),_Digits);

Логические переменные bord и sord используются как флаги, показывающие наличие установленного отложенного ордера Buy Stop и Sell Stop. Если такой ордер присутствует - соответствующая переменная будет иметь значение true, иначе - false. Переменная i используется в качестве счетчика в операторах цикла и для хранения промежуточных данных. В переменной ticket будет храниться тикет отложенного ордера для доступа к нему.

Массивы t[], h[] и l[] используются для хранения времени, максимального и минимального значения цены каждого бара исторических данных. Массив ma[] используется для хранения значений индикатора MA, а массивы atr_h[] и atr_l[] - для хранения значений верхней и нижней линий пользовательского индикатора indicator_TP, который мы только что создали. 

Переменные lev_h и lev_l используются для хранения максимального и минимального значения цены текущего дня, а также цен открытия отложенных ордеров. Переменная StopLoss служит для временного хранения цены Stop Loss открытой позиции. 

Переменная StopLevel используется для хранения величины STOP_LEVEL - минимального расстояния от текущей цены до выставляемого ордера (в единицах цены). Эту величину мы получаем как произведение цены пункта (предопределенная переменная _Point) на величину STOP_LEVEL в пунктах. Величину STOP_LEVEL возвращает функция SymbolInfoInteger. Первым параметром этой функции является символьное имя инструмента, а вторым - идентификатор запрашиваемого свойства. Символьное имя инструмента можно получить, используя функцию Symbol (у нее нет параметров).

Переменная Spread используется для хранения величины спрэда (в единицах цены). Ее значение вычисляем как разность текущих цен Ask и Bid, которую приводим к нормализованному виду (округляем до необходимого количества десятичных знаков после запятой) при помощи функции NormalizeDouble. Первым параметром этой функции является величина, которую необходимо нормализовать, а вторым - необходимое количество десятичных знаков после запятой (которое получаем из предопределенной переменной _Digits).

Значения цен Ask и Bid можно получить при помощи функции SymbolInfoDouble, первым параметром которой является символьное имя инструмента, а вторым - идентификатор запрашиваемого свойства.

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

   request.symbol      =Symbol();
   request.volume      =Lots;
   request.tp          =0;
   request.deviation   =0;
   request.type_filling=ORDER_FILLING_FOK;
Элемент request.symbol содержит символьное имя инструмента, по которому совершаются торговые операции, элемент request.volume - величину контракта финансового инструмента, request.tp - значение цены TakeProfit (в некоторых случаях значение TakeProfit для ордера не будет указываться, поэтому в элемент структуры request.tp заносим ноль), request.deviation - допустимое отклонение цены при совершении торговой операции, request.type_filling - тип ордера по исполнению, может быть одним из:

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

   TimeCurrent(dt);

Для всех расчетов нам необходимы исторические ценовые данные только текущего дня. Количество баров, которое с небольшим запасом будет превышать необходимое количество, можно вычислить как i = (dt.hour + 1)*60, где dt.hour - элемент структуры, содержащий текущий час. Копируем значения времени, максимальной и минимальной цены исторических данных в массивы t[], h[] и l[] при помощи функций CopyTime, CopyHigh и CopyLow

   i=(dt.hour+1)*60;
   if(CopyTime(Symbol(),0,0,i,t)<i || CopyHigh(Symbol(),0,0,i,h)<i || CopyLow(Symbol(),0,0,i,l)<i)
     {
      Print("Не удалось скопировать таймсерию!");
      return;
     }

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

В операторе if происходит проверка количества скопированных элементов всех трех массивов, если хоть в одном из них скопировано меньшее количество, чем необходимо для расчетов, или произошла ошибка - заносится соответствующая запись в журнал советников и при помощи оператора return происходит завершение выполнения функции OnTick. Запись в журнал советника производится при помощи функции Print, в качестве его параметров могут выступать данные любых типов, которые разделяются запятой.

После того, как ценовые данные скопированы в массивы t[], h[] и l[], для них при помощи функции ArraySetAsSeries (уже описанной в этой статье ранее) установим флаг AS_SERIES, чтобы порядок элементов в массивах был как в таймсериях (от текущих цен к более старым):

   ArraySetAsSeries(t,true);
   ArraySetAsSeries(h,true);
   ArraySetAsSeries(l,true);

В переменных lev_h и lev_l вычисляем максимальное и минимальное значение цены текущего дня, перебирая значения минимальных и максимальных цен всех баров, относящихся к текущему дню:

   lev_h=h[0];
   lev_l=l[0];
   for(i=1;i<ArraySize(t) && MathFloor(t[i]/86400)==MathFloor(t[0]/86400);i++)
     {
      if(h[i]>lev_h) lev_h=h[i];
      if(l[i]<lev_l) lev_l=l[i];
     }

Для того, чтобы ограничить поиск барами текущего дня, тело цикла выполняется только при соблюдения условия MathFloor(t[i]/86400) == MathFloor(t[0]/86400). В левой части условия оказывается номер дня, к которому принадлежит исследуемый бар, в правой части - номер текущего дня (86400 - количество секунд в дне). Функция MathFloor округляет числовое значение до ближайшего снизу целого значения (то есть, для положительных чисел отбрасывает дробную часть). Единственным параметром в этой функции является округляемое выражение. В языке MQL5 (как и в MQL4) равенство в выражениях обозначается двумя символами "равно (==)"  (см. операции отношения).

После прибавления к переменной lev_h одного пункта (значение предопределенной переменной _Point равно стоимости одного пункта в единицах цены) и увеличением  ее на величину Spread (спрэд, выраженный в единицах цены), получаем цену открытия для отложенного ордера Buy Stop (запись lev_h+=Spread+_Point равносильна записи lev_h=lev_h+Spread+_Point). После вычитания из переменной lev_l одного пункта, получаем цену открытия для отложенного ордера Sell Stop (запись lev_l-=_Point равносильна записи lev_l=lev_l-_Point).

   lev_h+=Spread+_Point;
   lev_l-=_Point;

При помощи функции CopyBuffer копируем значения индикаторных буферов в массивы: индикатора MA - в массив ma[], верхней линии созданного нами пользовательского индикатора - в массив atr_h[], нижней линии - в массив atr_l[]. Функция CopyBuffer была описана в этой статье при написании индикатора.

   if(CopyBuffer(hMA,0,0,2,ma)<2 || CopyBuffer(hCI,0,0,1,atr_h)<1 || CopyBuffer(hCI,1,0,1,atr_l)<1)
     {
      Print("Не удалось скопировать буфер индикатора!");
      return;
     }

Нам понадобится значение индикатора MA, соответствующее предпоследнему бару (последнему завершенному), и значение описанного выше индикатора, соответствующее последнему бару, поэтому копируем два элемента в массив ma[] и по одному элементу в массивы atr_h[] и atr_l[]. Если при копировании хотя бы одного из трех массивов не буден скопировано указанное количество элементов, или произойдет ошибка - функцией Print создастся соответствующая запись в журнале советников и выполнение функции OnTick будет завершено при помощи оператора return.

Для массива ma[] устанавливается флаг AS_SERIES, таким образом устанавливаем направление индексации как у таймсерии:

   ArraySetAsSeries(ma,true);

Массивы atr_h и atr_l содержат по одному элементу, поэтому направление индексации для них не имеет значения. Так как значение atr_l[0] будет использоваться для определения уровня TakeProfit короткой позиции, короткие позиции закрываются по цене Ask, а ценовые графики и индикаторы от них построены по цене Bid, к atr_l[0] прибавляем спрэд:

   atr_l[0]+=Spread;

Функция PositionsTotal возвращает общее количество открытых позиций (параметры у этой функции отсутствуют). Нумерация индексов позиций производится с нуля. Создадим цикл, в котором будут перебраны все открытые позиции:

// в этом цикле поочередно перебираем все открытые позиции
   for(i=0;i<PositionsTotal();i++)
     {
      // выбираем позиции только по "нашему" инструменту
      if(Symbol()==PositionGetSymbol(i))
        {
         // будут изменяться значения StopLoss и TakeProfit
         request.action=TRADE_ACTION_SLTP;
         // обслуживаем длинные позиции
         if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY)
           {
            // определяем StopLoss
            if(ma[1]>PositionGetDouble(POSITION_PRICE_OPEN)) StopLoss=ma[1]; else StopLoss=lev_l;
            // если StopLoss не указан или ниже, чем нужно
            if((PositionGetDouble(POSITION_SL)==0 || NormalizeDouble(StopLoss-PositionGetDouble(POSITION_SL),_Digits)>0
               // если TakeProfit не указан или выше, чем нужно
               || PositionGetDouble(POSITION_TP)==0 || NormalizeDouble(PositionGetDouble(POSITION_TP)-atr_h[0],_Digits)>0)
               // новый StopLoss не близко к текущей цене?
               && NormalizeDouble(SymbolInfoDouble(Symbol(),SYMBOL_BID)-StopLoss-StopLevel,_Digits)>0
               // новый TakeProfit не близко к текущей цене?
               && NormalizeDouble(atr_h[0]-SymbolInfoDouble(Symbol(),SYMBOL_BID)-StopLevel,_Digits)>0)
              {
               // заносим новое значение StopLoss в структуру
               request.sl=NormalizeDouble(StopLoss,_Digits);
               // заносим новое значение TakeProfit в структуру
               request.tp=NormalizeDouble(atr_h[0],_Digits);
               // отправляем запрос на торговый сервер
               OrderSend(request,result);
              }
           }
         // обслуживаем короткие позиции
         if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL)
           {
            // определяем значение StopLoss
            if(ma[1]+Spread<PositionGetDouble(POSITION_PRICE_OPEN)) StopLoss=ma[1]+Spread; else StopLoss=lev_h;
            // если StopLoss не указан или выше, чем нужно
            if((PositionGetDouble(POSITION_SL)==0 || NormalizeDouble(PositionGetDouble(POSITION_SL)-StopLoss,_Digits)>0
               // если TakeProfit не указан или ниже, чем нужно
               || PositionGetDouble(POSITION_TP)==0 || NormalizeDouble(atr_l[0]-PositionGetDouble(POSITION_TP),_Digits)>0)
               // новый StopLoss не близко к текущей цене?
               && NormalizeDouble(StopLoss-SymbolInfoDouble(Symbol(),SYMBOL_ASK)-StopLevel,_Digits)>0
               // новый TakeProfit не близко к текущей цене?
               && NormalizeDouble(SymbolInfoDouble(Symbol(),SYMBOL_ASK)-atr_l[0]-StopLevel,_Digits)>0)
              {
               // заносим новое значение StopLoss в структуру
               request.sl=NormalizeDouble(StopLoss,_Digits);
               // заносим новое значение TakeProfit в структуру
               request.tp=NormalizeDouble(atr_l[0],_Digits);
               // отправляем запрос на торговый сервер
               OrderSend(request,result);
              }
           }
         // если есть открытая позиция - дальше здесь делать нечего...
         return;
        }
     }

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

Так как, возможно, будет необходимо менять параметры StopLoss и TakeProfit открытых позиций, занесем в элемент структуры request.action значение TRADE_ACTION_SLTP. Далее происходит разделение позиций на длинные и короткие. Для длинных позиций определяется уровень StopLoss.

Если значение индикатора MA выше цены открытия позиции, то значение StopLoss принимается равным значению индикатора MA, в противном случае - StopLoss соответствует значению lev_l. При помощи функции PositionGetDouble получаем текущее значение StopLoss для открытой позиции (единственный параметр этой функции - идентификатор свойства позиции). Если значение StopLoss для открытой позиции не указано (равно нулю) или текущий уровень StopLoss выше, чем должен быть - будем модифицировать значения StopLoss и TakeProfit для этой позиции.

Аналогичным образом, при помощи функции PositionGetDouble получаем текущее значение TakeProfit для открытой позиции. Если уровень TakeProfit не указан (равен нулю) или TakeProfit выше, чем должен быть (выше, чем верхняя линия нашего индикатора) - будем модифицировать значения StopLoss и TakeProfit для этой позиции.

Для определения возможности установить StopLoss и TakeProfit на требуемых уровнях, проверяем, не окажутся ли они слишком близко от текущей цены. Новое значение StopLoss должно быть ниже текущей цены Bid более, чем на величину STOP_LEVEL. Новое значение TakeProfit должно быть выше текущей цены Bid более, чем на величину STOP_LEVEL. В операциях отношения используется нормализованная разность, так как сравниваемые величины могут отличаться в последних десятичных знаках из-за погрешности, вызванной преобразованием из внутреннего представления чисел типа double в виде двоичного числа с плавающей запятой в десятичное число с плавающей запятой.

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

Для коротких позиций изменение значений StopLoss и TakeProfit происходит аналогично. Так как короткие позиции, в отличии от длинных, закрываются по ценам Ask, то в операциях отношения будут использоваться цены Ask. Если есть открытая позиция по инструменту текущего графика - при помощи оператора return завершаем выполнение функции OnTick.

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

// в этом цикле поочередно перебираем все установленные отложенные ордера
   for(i=0;i<OrdersTotal();i++)
     {
      // выбираем каждый из ордеров, получаем его тикет
      ticket=OrderGetTicket(i);
      // выбираем ордера только по "нашему" инструменту
      if(OrderGetString(ORDER_SYMBOL)==Symbol())
        {
         // обслуживаем ордера Buy Stop
         if(OrderGetInteger(ORDER_TYPE)==ORDER_TYPE_BUY_STOP)
           {
            // если время рабочее и есть перспектива движения
            if(dt.hour>=StartHour && dt.hour<EndHour && lev_h<atr_h[0])
              {
               // если цена открытия ниже, чем нужно
               if((NormalizeDouble(lev_h-OrderGetDouble(ORDER_PRICE_OPEN),_Digits)>0
                  // если StopLoss не указан или выше, чем нужно
                  || OrderGetDouble(ORDER_SL)==0 || NormalizeDouble(OrderGetDouble(ORDER_SL)-lev_l,_Digits)!=0)
                  // цена открытия не близка к текущей цене?
                  && NormalizeDouble(lev_h-SymbolInfoDouble(Symbol(),SYMBOL_ASK)-StopLevel,_Digits)>0)
                 {
                  // будут изменяться параметры отложенного ордера
                  request.action=TRADE_ACTION_MODIFY;
                  // заносим тикет модифицируемого ордера в структуру
                  request.order=ticket;
                  // заносим новое значение цены открытия в структуру
                  request.price=NormalizeDouble(lev_h,_Digits);
                  // заносим новое значение StopLoss в структуру
                  request.sl=NormalizeDouble(lev_l,_Digits);
                  // отправляем запрос на торговый сервер
                  OrderSend(request,result);
                  // выходим из функции OnTick()
                  return;
                 }
              }
            // если время не рабочее или средний диапазон цен сегодня пройден
            else
              {
               // будет удаляться отложенный ордер
               request.action=TRADE_ACTION_REMOVE;
               // заносим тикет ордера в структуру
               request.order=ticket;
               // отправляем запрос на торговый сервер
               OrderSend(request,result);
               // выходим из функции OnTick()
               return;
              }
            // устанавливаем флаг, индицирующий то, что присутствует ордер Buy Stop
            bord=true;
           }
         // обслуживаем ордера Sell Stop
         if(OrderGetInteger(ORDER_TYPE)==ORDER_TYPE_SELL_STOP)
           {
            // если время рабочее и есть перспектива движения
            if(dt.hour>=StartHour && dt.hour<EndHour && lev_l>atr_l[0])
              {
               // если цена открытия выше, чем нужно
               if((NormalizeDouble(OrderGetDouble(ORDER_PRICE_OPEN)-lev_l,_Digits)>0
                  // если StopLoss не указан или ниже, чем нужно
                  || OrderGetDouble(ORDER_SL)==0 || NormalizeDouble(lev_h-OrderGetDouble(ORDER_SL),_Digits)>0)
                  // цена открытия не близка к текущей цене?
                  && NormalizeDouble(SymbolInfoDouble(Symbol(),SYMBOL_BID)-lev_l-StopLevel,_Digits)>0)
                 {
                  // будут изменяться параметры отложенного ордера
                  request.action=TRADE_ACTION_MODIFY;
                  // заносим тикет модифицируемого ордера в структуру
                  request.order=ticket;
                  // заносим новое значение цены открытия в структуру
                  request.price=NormalizeDouble(lev_l,_Digits);
                  // заносим новое значение StopLoss в структуру
                  request.sl=NormalizeDouble(lev_h,_Digits);
                  // отправляем запрос на торговый сервер
                  OrderSend(request,result);
                  // выходим из функции OnTick()
                  return;
                 }
              }
            // если время не рабочее или средний диапазон цен сегодня пройден
            else
              {
               // будет удаляться отложенный ордер
               request.action=TRADE_ACTION_REMOVE;
               // заносим тикет ордера в структуру
               request.order=ticket;
               // отправляем запрос на торговый сервер
               OrderSend(request,result);
               // выходим из функции OnTick()
               return;
              }
            // устанавливаем флаг, индицирующий то, что присутствует ордер Sell Stop
            sord=true;
           }
        }
     }

Функцией OrderGetTicket выбираем ордер для дальнейшей работы с ним, а также, заносим тикет ордера в переменную ticket. У этой функции в качестве параметра указывается номер ордера в списке ордеров. Функцией OrderGetString получаем символьное имя инструмента, параметром этой функции является идентификатор свойства ордера. Сравниваем символьное имя инструмента с именем инструмента текущего графика, таким образом выбирая ордера только по инструменту, на котором работает советник. Определяем тип ордера при помощи функции OrderGetInteger, параметром этой функции является идентификатор свойства ордера. Ордера Buy Stop и Sell Stop будем обслуживать раздельно.

Если текущий час попадает в диапазон от StartHour до EndHour и цена открытия ордера Buy Stop не превышает верхнюю линию индикатора, корректируем цену открытия ордера и уровень StopLoss (если в этом есть необходимость), иначе удаляем ордер.

Определяем, нужно ли корректировать цену открытия или StopLoss. Если цена открытия ордера Buy Stop ниже, чем должна быть, или если StopLoss не указан или выше, чем должен быть, заносим в элемент структуры request.action значение TRADE_ACTION_MODIFY, означающее, что будут меняться параметры отложенного ордера, в элемент структуры request.ticket - тикет отдера, полученный ранее, и отправляем запрос на торговый сервер функцией OrderSend. В операции отношения определяется только случай, когда цена открытия ордера ниже, чем должна быть, потому, что величина спреда может изменяться, но ордер не будем перемещать при каждом изменении спрэда, а установим в самом верхнем положении, соответствующем максимальной величине спрэда. 

Также, в операции отношения определяется только случай, когда значение уровня StopLoss отложенного ордера выше, чем должно быть, потому, что диапазон движения цены внутри дня может только расширяться и может потребоваться перемещение вниз уровня StopLoss ордера Buy Stop при покорении ценой новых минимумов. После отправки запроса на торговый сервер завершаем выполнение функции OnTick при помощи оператора return. Если присутствует ордер Buy Stop (и операции по нему не производились) - устанавливаем в переменной bord значение true.

Ордера Sell Stop обслуживаются аналогично ордерам Buy Stop.

Теперь займемся установкой отложенных ордеров Buy Stop и Sell Stop, если какой-то из них еще отсутствует. Заносим в элемент структуры request.action значение TRADE_ACTION_PENDING (означающее, что будет установка отложенного ордера).

   request.action=TRADE_ACTION_PENDING;

Если значение текущего часа попадает в диапазон времени, когда выставляются ордера, - выставляем их.

   if(dt.hour>=StartHour && dt.hour<EndHour)
     {
      if(bord==false && lev_h<atr_h[0])
        {
         request.price=NormalizeDouble(lev_h,_Digits);
         request.sl=NormalizeDouble(lev_l,_Digits);
         request.type=ORDER_TYPE_BUY_STOP;
         OrderSend(request,result);
        }
      if(sord==false && lev_l>atr_l[0])
        {
         request.price=NormalizeDouble(lev_l,_Digits);
         request.sl=NormalizeDouble(lev_h,_Digits);
         request.type=ORDER_TYPE_SELL_STOP;
         OrderSend(request,result);
        }
     }
  }

При выставлении ордеров Buy Stop или Sell Stop проверяем, что такого ордера еще нет, анализируя переменную bord или sord, а также, проверяем, не выходит ли цена открытия будущего ордера за линии индикатора. Заносим в элемент структуры request.price цену открытия ордера, предварительно нормализовав ее; заносим в request.sl нормализованное значение StopLoss, а в request.type - тип устанавливаемого ордера: ORDER_BUY_STOP или ORDER_SELL_STOP и отправляем запрос на торговый сервер. На этом написание тела функции OnTick завершается и в конце ставим закрывающую фигурную скобку.

В функции OnDeinit освобождаем ресурсы, занимаемые индикаторами. Для этого используем функцию IndicatorRelease (она были рассмотрена выше, при написании индикатора).

void OnDeinit(const int reason)
  {
   IndicatorRelease(hCI);
   IndicatorRelease(hMA);
  }

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


Запуск и отладка

После того, как советник и индикатор готовы, рассмотрим, как их запускать на исполнение и как проводить отладку при помощи встроенного в MetaEditor отладчика.

Для запуска советника, его необходимо найти в окне Навигатор в ветви Советники, щелкнуть на его имени правой клавишей мыши и, в появившемся меню, выбрать пункт Присоединить к графику.


Рис.11. Запуск советника

Появится окно с входными параметрами советника, при необходимости их можно изменить. После нажатия кнопки OK в правом верхнем углу появится значок с именем советника - советник работает. Для отображения или скрытия окна Навигатор нужно в меню Вид выбрать пункт Навигатор или нажать комбинацию клавиш Ctrl+N. Второй способ запуска советника - в меню Вставка выбрать пункт Эксперты, а в появившемся раскрывающемся меню - имя советника.

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


Рис.12. Параметры терминала - включение автоматической торговли

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

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

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

Аналогично советникам и индикаторам запускаются и скрипты.

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

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

Запустить программу на отладку можно, выбрав в редакторе MetaEditor в меню Отладка пункт Начать или нажав клавишу F5. Программа будет скомпилирована и запущена в отладочном режиме в отдельном окне графика, период которого и символьное  имя финансового инструмента можно указать в настройках редактора MetaEditor, выбрав в меню Сервис пункт Настройки, на вкладке Отладка.


Рис.13. Параметры редактора - отладка

В тексте программы можно устанавливать или убирать точки останова (breakpoints), это можно сделать нажатием клавиши F9, двойным щелчком левой клавиши мыши на сером поле слева от строки программы или выбрав в меню Отладка пункт Переключить точку останова. При отладке выполнение программы будет остановлено перед оператором, на строке которого находится точка останова. В окне Инструменты редактора MetaEditor появится вкладка Отладка.

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

Рис.14. Отладка программы

Пошагово выполнять программу можно нажимая клавиши F11F10 или комбинацию клавиш Shift+F11.

Зеленая стрелка слева от текста программы указывает на строку, которая будет выполняться.


Заключение

В данной статье приведен пример написания простого советника и индикатора, даны основы языка MQL5.

Торговая система, приведенная здесь, выбрана в качестве примера, за ее использование автор ответственности не несет.