Почти конструктор для создания советника

Vladimir Karputov | 16 сентября, 2021

Оглавление


Введение

С самого начала у меня была цель использовать Стандартную библиотеку. Помню свою первую задачу — это реализовать самый простейший функционал: подключить торговый класс CTrade и выполнить метод Buy или Sell. Стандартная библиотека была выбрана из-за короткого и лаконичного полученного кода. Короткий код ниже, выполненный в виде скрипта, открывает позицию BUY объёмом 1.0 лот:

//+------------------------------------------------------------------+
//|                                                     Open Buy.mq5 |
//|                         Copyright © 2018-2021, Vladimir Karputov |
//+------------------------------------------------------------------+
#property copyright "Copyright © 2018-2021, Vladimir Karputov"
#property version   "1.001"
//---
#include <Trade\Trade.mqh>
CTrade         m_trade;                      // trading object
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   m_trade.Buy(1.0); // open Buy position, volume 1.0 lot
  }

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

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

ПОМНИТЕ: MQL5 стиль подразумевает, что хендл индикатора создаётся ОДИН РАЗ и делается это, как правило, в OnInit

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

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

Каждый входной параметр тянул за собой создание блоков кода и новых функций.

Для ежедневного использования решено было собрать все самые популярные функции и полный набор входных параметров в советник 'Trading engine 3.mq5' по сути, это готовый советник, который заменяет много рутинной работы. Остаётся под каждый конкретный случай добавить или удалить функции или изменить взаимодействие между блоками кода.


1. Функционал советника после конструктора

Советник, созданный конструктором, имеет сразу очень много настроек, компонуя которые можно создавать уникальные стратегии. В версии 4.XXX применяются такие правила: 

Что такое 'points' всегда можно увидеть на графике символа, если перетащить инструмент Перекрестие:

points

Рис. 1. Points

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


2. Общий алгоритм конструктора

На глобальном программном уровне (в "шапке" советника) объявляется массив 'SPosition' ('SPosition' — имя массива), состоящий из структур 'STRUCT_POSITION'. При старте этот массив имеет нулевой размер, после отработки торгового сигнала массив также возвращается в нулевой размер.

Из OnTick вызывается функция 'SearchTradingSignals' — при наличии сигнала (сигнал получаем, если в рынке нет открытых позиций) эта функция формирует торговый приказ (создаёт для каждого торгового приказа одну структуру 'STRUCT_POSITION' в массиве). Также в OnTick проверяется наличие торгового приказа — проверяется размер массива 'SPosition:  если он больше нуля, значит есть торговый приказ и этот торговый приказ пересылается на выполнение в 'OpenBuy' или в 'OpenSell'. Контроль за исполнением торгового приказа осуществляется в OnTradeTransaction:

general algorithm (simple)

Рис. 2. Общий алгоритм (простой)

Всегда подразумевается, что советник работает по текущему символу — то есть по символу, на чей график установлен советник. Пример: советник размещён на графике 'USDPLN', значит, советник работает по символу 'USDPLN'.

2.1. Структура 'STRUCT_POSITION'

Данная структура это сердце советника, она выполняет сразу две роли: в структуре есть поля, в которые записывается торговый приказ (запись проводится в 'SearchTradingSignals') и в структуре есть поля для контроля за выполнением торгового приказа (контроль проводится в OnTradeTransaction). 

//+------------------------------------------------------------------+
//| Structure Positions                                              |
//+------------------------------------------------------------------+
struct STRUCT_POSITION
  {
   ENUM_POSITION_TYPE pos_type;              // position type
   double            volume;                 // position volume (if "0.0" -> the lot is "Money management")
   double            lot_coefficient;        // lot coefficient
   bool              waiting_transaction;    // waiting transaction, "true" -> it's forbidden to trade, we expect a transaction
   ulong             waiting_order_ticket;   // waiting order ticket, ticket of the expected order
   bool              transaction_confirmed;  // transaction confirmed, "true" -> transaction confirmed
   //--- Constructor
                     STRUCT_POSITION()
     {
      pos_type                   = WRONG_VALUE;
      volume                     = 0.0;
      lot_coefficient            = 0.0;
      waiting_transaction        = false;
      waiting_order_ticket       = 0;
      transaction_confirmed      = false;
     }
  };

Одни поля отвечают за торговый приказ, а другие поля за контроль над исполнением торгового приказа.  Cтруктура содержит конструктор  специальную функцию 'STRUCT_POSITION()', которая вызывается при создании объекта структуры и инициализирует элементы структуры.

Поля торгового приказа:

Поля контроля над исполнением торгового приказа

После поднятия флага в поле 'transaction_confirmed' торговый приказ удаляется из структуры. Таким образом, если в работе советника нет торгового приказа, структура имеет нулевой размер. Подробнее про поля структуры и про контроль в главе 'Ловим транзакцию упрощенный код We catch the transaction'.

Почему именно такой алгоритм

Казалось бы, достаточно проверить метод Buy на возврат 'true' или 'false' и в случае 'true' решить, что торговый приказ выполнен. И во многих случаях такой подход сработает, но встречаются моменты, когда возврат 'true' не дает гарантии результата, о чём, кстати, говорится в документации к методам Buy и Sell:

Примечание

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

А окончательным и точным подтверждением выполнения торговой операции может служить наличие записи в торговой истории. Поэтому и был выбран такой алгоритм: при успешном выполнении метода проверяем ResultDeal (тикет сделки), проверяем ResultRetcode (код результата выполнения запроса) и запоминаем ResultOrder (тикет ордера). А тикет ордера отлавливаем в OnTradeTransaction.


3. Добавляем стандартный индикатор — работа с файлом 'Indicators Code.mq5'

Для удобства работы готовые блоки кода (объявление переменных для хранения хендлов, входные параметры, создание хендлов) собраны в советнике 'Indicators Code.mq5'. Входные параметры индикаторов и переменные для хранения хендлов расположены в "шапке" советника, создание хендлов, как и полагается, прописано в OnInit. Нюанс: имена переменных для хранения хендлов формируются по следующему шаблону: 'handle_' + 'индикатор', например 'handle_iStdDev'. Вся работа с 'Indicators Code.mq5' сводится к операциям 'copy-paste'. 

ПОМНИТЕ: MQL5 стиль подразумевает, что хендл индикатора создаётся ОДИН РАЗ и делается это (как правило) в OnInit

3.1. Пример добавления индикатора 'iRVI' (Relative Vigor Index, RVI)

Создадим советник 'Add indicator.mq5'. В редакторе MetaEditor вызываем 'MQL Wizard', например, кликом по кнопке  New, и выбираем 'Expert Advisor (template)'

Expert Advisor (template)

Рис. 3. 'MQL Wizard' -> 'Expert Advisor (template)'

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

Expert Advisor (template)

Рис. 4. 'Expert Advisor (template)' -> 'Add parameter'

Такой приём автоматически добавляет в код строки для блока входных параметров:

//--- input parameters
input int      Input1=9;

'MQL Wizard' создал пустой советник, теперь добавим в него индикатор 'iRVI' (Relative Vigor Index, RVI). В 'Indicators Code.mq5' проводим поиск 'handle_iRVI' (поиск вызывается через 'ctrl' + 'F'). Поиск находит переменную, в которой хранится хендл:

iRVI

Рис. 5. handle RVI

Найденную строку копируем и вставляем в советник 'Add indicator' в шапку:

//--- input parameters
input int      Input1=9;
//---
int      handle_iRVI;                           // variable for storing the handle of the iRVI indicator
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()

Продолжаем поиск и находим блок создания хендла:

iRVI

Рис. 6. handle iRVI

Найденные строки копируем и вставляем в советник 'Add indicator' в OnInit:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- create handle of the indicator iRVI
   handle_iRVI=iRVI(m_symbol.Name(),Inp_RVI_period,Inp_RVI_ma_period);
//--- if the handle is not created
   if(handle_iRVI==INVALID_HANDLE)
     {
      //--- tell about the failure and output the error code
      PrintFormat("Failed to create handle of the iRVI indicator for the symbol %s/%s, error code %d",
                  m_symbol.Name(),
                  EnumToString(Inp_RVI_period),
                  GetLastError());
      //--- the indicator is stopped early
      m_init_error=true;
      return(INIT_SUCCEEDED);
     }
//---
   return(INIT_SUCCEEDED);
  }

Теперь добавим входные параметры индикатора. В 'Indicators Code.mq5' делаем клик средней кнопкой мыши, например на 'Inp_RVI_period', нас перебросит сразу в блок входных параметров:

iRVI

Рис. 7. handle iRVI

Копируем строки и вставляем во входные параметры:

#property version   "1.00"
//--- input parameters
input group             "RVI"
input ENUM_TIMEFRAMES      Inp_RVI_period                = PERIOD_D1;      // RVI: timeframe
input int                  Inp_RVI_ma_period             = 15;             // RVI: averaging period
//---
int      handle_iRVI;                           // variable for storing the handle of the iRVI indicator
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+

Если скомпилировать, то получим ошибки: компилятор ругается на 'm_symbol' и на 'm_init_error'. И это правильно так как эти переменные есть в коде, который получаем после работы конструктора, а советник 'Add indicator' создан исключительно для демонстрации работы с файлом 'Indicators Code.mq5'.


4. Добавляем пользовательский индикатор

Добавим пользовательский индикатор MA on DeMarker. Во-первых, это пользовательский индикатор, во-вторых, этот индикатор использует group. По аналогии с предыдущим разделом создадим советник 'Add custom indicator'. После этого нужно из пользовательского индикатора скопировать входные параметры и вставить в советник:

#property version   "1.00"
//--- input parameters
input group             "DeMarker"
input int                  Inp_DeM_ma_period    = 14;             // DeM: averaging period
input double               Inp_DeM_LevelUP      = 0.7;            // DeM: Level UP
input double               Inp_DeM_LevelDOWN    = 0.3;            // DeM: Level DOWN
input group             "MA"
input int                  Inp_MA_ma_period     = 6;              // MA: averaging period
input ENUM_MA_METHOD       Inp_MA_ma_method     = MODE_EMA;       // MA: smoothing type
//---

Найдём в файле 'Indicators Code.mq5' переменную 'handle_iCustom' переменную для хранения хендла пользовательского индикатора  и вставим в советник:

//+------------------------------------------------------------------+
//|                                         Add custom indicator.mq5 |
//|                              Copyright © 2021, Vladimir Karputov |
//|                      https://www.mql5.com/en/users/barabashkakvn |
//+------------------------------------------------------------------+
#property copyright "Copyright © 2021, Vladimir Karputov"
#property link      "https://www.mql5.com/en/users/barabashkakvn"
#property version   "1.00"
//--- input parameters
input group             "DeMarker"
input int                  Inp_DeM_ma_period    = 14;             // DeM: averaging period
input double               Inp_DeM_LevelUP      = 0.7;            // DeM: Level UP
input double               Inp_DeM_LevelDOWN    = 0.3;            // DeM: Level DOWN
input group             "MA"
input int                  Inp_MA_ma_period     = 6;              // MA: averaging period
input ENUM_MA_METHOD       Inp_MA_ma_method     = MODE_EMA;       // MA: smoothing type
//---
int      handle_iCustom;                        // variable for storing the handle of the iCustom indicator
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()

Найдём в файле 'Indicators Code.mq5' в OnInit() блок создания хендла пользовательского индикатора и вставим в советник:  

int      handle_iCustom;                        // variable for storing the handle of the iCustom indicator
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- create handle of the indicator iCustom
   handle_iCustom=iCustom(m_symbol.Name(),Inp_DEMA_period,"Examples\\DEMA",
                          Inp_DEMA_ma_period,
                          Inp_DEMA_ma_shift,
                          Inp_DEMA_applied_price);
//--- if the handle is not created
   if(handle_iCustom==INVALID_HANDLE)
     {
      //--- tell about the failure and output the error code
      PrintFormat("Failed to create handle of the iCustom indicator for the symbol %s/%s, error code %d",
                  m_symbol.Name(),
                  EnumToString(Inp_DEMA_period),
                  GetLastError());
      //--- the indicator is stopped early
      m_init_error=true;
      return(INIT_SUCCEEDED);
     }
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |

Здесь придётся потрудиться. Нужно прописать таймфрейм ('InpWorkingPeriod'), путь к индикатору (предполагаем, что индикатор лежит в корне папки 'Indicators') и входные параметры:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- create handle of the indicator iCustom
   handle_iCustom=iCustom(m_symbol.Name(),InpWorkingPeriod,"MA on DeMarker",
                          "DeMarker",
                          Inp_DeM_ma_period,
                          Inp_DeM_LevelUP,
                          Inp_DeM_LevelDOWN,
                          "MA",
                          Inp_MA_ma_period,
                          Inp_MA_ma_method);
//--- if the handle is not created
   if(handle_iCustom==INVALID_HANDLE)
     {
      //--- tell about the failure and output the error code
      PrintFormat("Failed to create handle of the iCustom indicator for the symbol %s/%s, error code %d",
                  m_symbol.Name(),
                  EnumToString(InpWorkingPeriod),
                  GetLastError());
      //--- the indicator is stopped early
      m_init_error=true;
      return(INIT_SUCCEEDED);
     }
//---
   return(INIT_SUCCEEDED);
  }

5. Ловим транзакцию упрощенный код We catch the transaction

ВНИМАНИЕ: Этот советник - упрощенная версия. Многие функции имеет сокращенный код по сравнению с полноценным конструктором

Если в рынке нет позиций, открытых данным советником, то записываем торговый приказ на открытие позиции BUY. Подтверждение, что позиция открыта, распечатываем из OnTradeTransaction и из OnTick. Поиск и запись торгового приказа в функции 'SearchTradingSignals':

//+------------------------------------------------------------------+
//| Search trading signals                                           |
//+------------------------------------------------------------------+
bool SearchTradingSignals(void)
  {
   if(IsPositionExists())
      return(true);
//---
   int size_need_position=ArraySize(SPosition);
   if(size_need_position>0)
      return(true);
   ArrayResize(SPosition,size_need_position+1);
   SPosition[size_need_position].pos_type=POSITION_TYPE_BUY;
   if(InpPrintLog)
      Print(__FILE__," ",__FUNCTION__,", OK: ","Signal BUY");
   return(true);
  }

Если в рынке нет позиций, открытых советником, и если размер массива 'SPosition' равен нулю, увеличиваем размер массива на единицу таким образом создаём один объект структуры 'STRUCT_POSITION', что в свою очередь вызывает конструктор 'STRUCT_POSITION()'. После вызова конструктора элементы структуры инициализированы (например, volume  объем позиции стоит в '0.0' значит, объём позиции будет браться из группы входных параметров 'Position size management (lot calculation)'). Остаётся в структуру записать только тип торгового приказа  в данном случае этот приказ можно прочитать как: "Открыть позицию BUY".

После записи торгового приказа массив 'SPosition' состоит из одной структуры и элементы этой структуры имеют такие значения:

Элемент Значение Примечание 
 pos_type  POSITION_TYPE_BUY  записан в 'SearchTradingSignals'
 volume  0.0  проинициализирован в конструкторе структуры
 lot_coefficient  0.0  проинициализирован в конструкторе структуры
 waiting_transaction  false  проинициализирован в конструкторе структуры
 waiting_order_ticket  0  проинициализирован в конструкторе структуры
 transaction_confirmed  false  проинициализирован в конструкторе структуры

5.1. На новом тике попадаем в OnTick

Общий принцип действия в OnTick:

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   int size_need_position=ArraySize(SPosition);
   if(size_need_position>0)
     {
      for(int i=size_need_position-1; i>=0; i--)
        {
         if(SPosition[i].waiting_transaction)
           {
            if(!SPosition[i].transaction_confirmed)
              {
               if(InpPrintLog)
                  Print(__FILE__," ",__FUNCTION__,", OK: ","transaction_confirmed: ",SPosition[i].transaction_confirmed);
               return;
              }
            else
               if(SPosition[i].transaction_confirmed)
                 {
                  ArrayRemove(SPosition,i,1);
                  return;
                 }
           }
         if(SPosition[i].pos_type==POSITION_TYPE_BUY)
           {
            SPosition[i].waiting_transaction=true;
            OpenPosition(i);
            return;
           }
         if(SPosition[i].pos_type==POSITION_TYPE_SELL)
           {
            SPosition[i].waiting_transaction=true;
            OpenPosition(i);
            return;
           }
        }
     }
//--- search for trading signals only at the time of the birth of new bar
   if(!RefreshRates())
      return;
//--- search for trading signals
   if(!SearchTradingSignals())
      return;
//---
  }

В начале OnTick проверяем размер массива 'SPosition' (это массив структур 'STRUCT_POSITION'). Если размер массива больше нуля  начинаем обход к нулю, при этом доступно два сценария: 

Продолжаем отслеживание, напомню блок кода:

         if(SPosition[i].pos_type==POSITION_TYPE_BUY)
           {
            SPosition[i].waiting_transaction=true;
            OpenPosition(i);
            return;
           }

Только что торговый приказ был записан в структуру (в функции 'SearchTradingSignals') и флаг 'waiting_transaction' стоит в 'false значит взводим флаг 'waiting_transaction' в 'true' и передаём в функцию 'OpenPosition' параметр 'i'  это порядковый номер структуры в массиве 'OpenPosition'. Так как тип торгового приказа у нас 'POSITION_TYPE_BUY', передаём порядковый номер структуры в функцию 'OpenBuy'.

5.2. Функция 'OpenBuy'

Задача функции пройти предварительные проверки, отослать торговый запрос на открытие позиции BUY и отследить сразу результат.

Первая проверка - проверка SYMBOL_VOLUME_LIMIT

 SYMBOL_VOLUME_LIMIT  Максимально допустимый для данного символа совокупный объем открытой позиции и отложенных ордеров  в одном направлении (покупка или продажа). Например, при ограничении в 5 лотов можно иметь открытую позицию на покупку объемом 5 лотов и выставить отложенный ордер Sell Limit объемом 5 лотов. Но при этом нельзя выставить отложенный ордер Buy Limit (поскольку совокупный объем в одном направлении превысит ограничение) или выставить Sell Limit объемом более 5 лотов.

Если проверку не прошли, то удаляем элемент структуры из массива 'SPosition'.

Вторая проверка - маржа

Получаем размер свободной маржи, которая останется после торговой операции (FreeMarginCheck), размер маржи, необходимой для торговой операции (MarginCheck). После этого защищаемся всегда страхуемся и оставляем после операции сумму как минимум равной FreeMarginCheck:

   if(free_margin_check>margin_check)

Если проверку не прошли распечатываем переменную 'free_margin_check' и удаляем элемент структуры из массива 'SPosition'.

Проверка номер три - bool результат операции Buy

Если метод Buy возвратил результат 'false', распечатываем ошибку и записываем в поле 'waiting_transaction' значение 'false' (самая распространённая причина - ошибка 10004, requote) таким образом на новом тике будет предпринята попытка снова открыть позицию BUY. Если же результат 'true' (ниже показан блок кода, когда метод Buy возвратил результат 'true')

         if(m_trade.ResultDeal()==0)
           {
            if(m_trade.ResultRetcode()==10009) // trade order went to the exchange
              {
               SPosition[index].waiting_transaction=true;
               SPosition[index].waiting_order_ticket=m_trade.ResultOrder();
              }
            else
              {
               SPosition[index].waiting_transaction=false;
               if(InpPrintLog)
                  Print(__FILE__," ",__FUNCTION__,", ERROR: ","#1 Buy -> false. Result Retcode: ",m_trade.ResultRetcode(),
                        ", description of result: ",m_trade.ResultRetcodeDescription());
              }
            if(InpPrintLog)
               PrintResultTrade(m_trade,m_symbol);
           }
         else
           {
            if(m_trade.ResultRetcode()==10009)
              {
               SPosition[index].waiting_transaction=true;
               SPosition[index].waiting_order_ticket=m_trade.ResultOrder();
              }
            else
              {
               SPosition[index].waiting_transaction=false;
               if(InpPrintLog)
                  Print(__FILE__," ",__FUNCTION__,", OK: ","#2 Buy -> true. Result Retcode: ",m_trade.ResultRetcode(),
                        ", description of result: ",m_trade.ResultRetcodeDescription());
              }
            if(InpPrintLog)
               PrintResultTrade(m_trade,m_symbol);
           }

то проверяем ResultDeal (тикет сделки).

Если тикет сделки равен нулю  проверяем ResultRetcode (код результата выполнения запроса). Получили код возврата '10009' (например, торговый приказ был отослан во внешнюю торговую систему, например на Биржу и поэтому тикет сделки равен нулю) значит в поле 'waiting_transaction' записываем 'true', а в поле 'waiting_order_ticket' записываем ResultOrder (тикет ордера), иначе (код возврата не равен '10009') в поле 'waiting_transaction' записываем 'false' и распечатываем сообщение об ошибке.

Если же тикет сделки не равен нулю (например, исполнение происходит на этом торговом сервере), то проводим аналогичные проверки кода возврата и аналогично записываем значения в поля 'waiting_transactionи 'waiting_order_ticket'.

5.3. OnTradeTransaction

В случае когда торговый приказ был отослан удачно, необходимо дождаться подтверждения того, что сделка прошла и она записана в торговую историю. В OnTradeTransaction работаем с переменной 'trans' (структура, имеющая тип MqlTradeTransaction). В структуре нас интересуют только два поля — 'deal' и 'type':

struct MqlTradeTransaction
  {
   ulong                         deal;             // Тикет сделки
   ulong                         order;            // Тикет ордера
   string                        symbol;           // Имя торгового инструмента
   ENUM_TRADE_TRANSACTION_TYPE   type;             // Тип торговой транзакции
   ENUM_ORDER_TYPE               order_type;       // Тип ордера
   ENUM_ORDER_STATE              order_state;      // Состояние ордера
   ENUM_DEAL_TYPE                deal_type;        // Тип сделки
   ENUM_ORDER_TYPE_TIME          time_type;        // Тип ордера по времени действия
   datetime                      time_expiration;  // Срок истечения ордера
   double                        price;            // Цена 
   double                        price_trigger;    // Цена срабатывания стоп-лимитного ордера
   double                        price_sl;         // Уровень Stop Loss
   double                        price_tp;         // Уровень Take Profit
   double                        volume;           // Объем в лотах
   ulong                         position;         // Тикет позиции
   ulong                         position_by;      // Тикет встречной позиции
  };


Как только в OnTradeTransaction словили транзакцию TRADE_TRANSACTION_DEAL_ADD (добавление сделки в историю), осуществляем проверку: пытаемся выбрать сделку в истории через HistoryDealSelect  если выбрать сделку не удалось, печатаем ошибку, если же сделка существует в торговой истории, то начинаем в цикле обход массива 'SPosition'. В цикле смотрим только те структуры, у которых поле 'waiting_transaction' стоит в 'true' и поле 'waiting_order_ticket' равно тикету ордера выбранной нами сделки. Если обнаружено совпадение, записываем в поле 'transaction_confirmed' значение 'true' это означает, что торговый приказ выполнен и подтвержден.

//+------------------------------------------------------------------+
//| TradeTransaction function                                        |
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction &trans,
                        const MqlTradeRequest &request,
                        const MqlTradeResult &result)
  {
//--- get transaction type as enumeration value
   ENUM_TRADE_TRANSACTION_TYPE type=trans.type;
//--- if transaction is result of addition of the transaction in history
   if(type==TRADE_TRANSACTION_DEAL_ADD)
     {
      ResetLastError();
      if(HistoryDealSelect(trans.deal))
         m_deal.Ticket(trans.deal);
      else
        {
         Print(__FILE__," ",__FUNCTION__,", ERROR: ","HistoryDealSelect(",trans.deal,") error: ",GetLastError());
         return;
        }
      if(m_deal.Symbol()==m_symbol.Name() && m_deal.Magic()==InpMagic)
        {
         if(m_deal.DealType()==DEAL_TYPE_BUY || m_deal.DealType()==DEAL_TYPE_SELL)
           {
            int size_need_position=ArraySize(SPosition);
            if(size_need_position>0)
              {
               for(int i=0; i<size_need_position; i++)
                 {
                  if(SPosition[i].waiting_transaction)
                     if(SPosition[i].waiting_order_ticket==m_deal.Order())
                       {
                        Print(__FUNCTION__," Transaction confirmed");
                        SPosition[i].transaction_confirmed=true;
                        break;
                       }
                 }
              }
           }
        }
     }
  }

На новом тике попадаем в OnTick и там структура, у которой стоит 'true' в поле 'transaction_confirmed', будет удалена из массива 'SPosition'. Таким образом, был выдан торговый приказ и этот торговый приказ был отслежен до момента, когда он появится в торговой истории.


6. Создаём советник (сигналы на открытие позиций) при помощи конструктора

Перед созданием любого советника нужно продумать саму торговую стратегию. Рассмотрим простую стратегию по индикатору iDEMA (Double Exponential Moving Average, DEMA) эта стратегия по умолчанию прописана в конструкторе. Сигнал на открытие позиции ищем только в момент рождения нового бара, а сам торговый сигнал это индикатор, который последовательно повышается или понижается:

DEMA Strategy

Рис. 8. DEMA Strategy

Помните: любую стратегию можно сильно видоизменить, если настраивать параметры. Например, можно оставить Тейк профит и Стоп лосс, но выключить Трейлинг. Или наоборот: выключить Тейк профит и Стоп лосс, а Трейлинг оставить. Или ограничить, в какую сторону торговать — разрешить только BUY или только SELL. А можно включить 'Time control' и ограничить торговлю в ночные часы или наоборот настроить торговлю только в ночные часы. Также сильно можно изменить торговую систему, настраивая параметры из группы 'Additional features'.

В общем, костяк торговой стратегии строится в функции 'SearchTradingSignals', а все остальные параметры это "прощупывание" рынка в поисках оптимальных подходов.

Итак, создаём новый файл заготовку советника (выполните шаги, обозначенные на рисунках 3 и 4). На шаге 4 необходимо дать уникальное имя советнику пусть это будет 'iDEMA Full EA.mq5'. Получили вот такую заготовку:

//+------------------------------------------------------------------+
//|                                                iDEMA Full EA.mq5 |
//|                              Copyright © 2021, Vladimir Karputov |
//|                      https://www.mql5.com/en/users/barabashkakvn |
//+------------------------------------------------------------------+
#property copyright "Copyright © 2021, Vladimir Karputov"
#property link      "https://www.mql5.com/en/users/barabashkakvn"
#property version   "1.00"
//--- input parameters
input int      Input1=9;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   
  }
//+------------------------------------------------------------------+

Теперь копируем весь код из файла 'Trading engine 3.mq5' и вставляем вместо строк. Нужно отредактировать "шапку советника". После операции вставки получилась такая "шапка":

//+------------------------------------------------------------------+
//|                                                iDEMA Full EA.mq5 |
//+------------------------------------------------------------------+
//|                                             Trading engine 3.mq5 |
//|                              Copyright © 2021, Vladimir Karputov |
//|                      https://www.mql5.com/en/users/barabashkakvn |
//+------------------------------------------------------------------+
#property copyright "Copyright © 2021, Vladimir Karputov"
#property link      "https://www.mql5.com/en/users/barabashkakvn"
#property version   "4.003"
#property description "barabashkakvn Trading engine 4.003"
#property description "Take Profit, Stop Loss and Trailing - in Points (1.00055-1.00045=10 points)"
/*
   barabashkakvn Trading engine 4.003
*/
#include <Trade\PositionInfo.mqh>

Приводим "шапку" в такой вид:

//+------------------------------------------------------------------+
//|                                                iDEMA Full EA.mq5 |
//|                              Copyright © 2021, Vladimir Karputov |
//|                      https://www.mql5.com/en/users/barabashkakvn |
//+------------------------------------------------------------------+
#property copyright "Copyright © 2021, Vladimir Karputov"
#property link      "https://www.mql5.com/en/users/barabashkakvn"
#property version   "1.001"
#property description "iDEMA EA"
#property description "Take Profit, Stop Loss and Trailing - in Points (1.00055-1.00045=10 points)"
/*
   barabashkakvn Trading engine 4.003
*/
#include <Trade\PositionInfo.mqh>

Если скомпилировать не будет ни одной ошибки. Полученный советник даже сможет торговать.

6.1. Функция 'SearchTradingSignals'

Это самая главная функция, которая отвечает за проверку наличия торговых приказов. Рассмотрим эту функцию по блокам.

Не более одной позиции на баре:

   if(iTime(m_symbol.Name(),InpWorkingPeriod,0)==m_last_deal_in) // on one bar - only one deal
      return(true);

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

   if(!TimeControlHourMinute())
      return(true);

Получение данных с индикатора. Данные с индикатора получаем в массив 'dema', которому при помощи ArraySetAsSeries устанавливается обратный порядок индексации (элемент массива [0] будет соответствовать самому правому бару на графике). Данные получаем через пользовательскую функцию 'iGetArray':

   double dema[];
   ArraySetAsSeries(dema,true);
   int start_pos=0,count=6;
   if(!iGetArray(handle_iCustom,0,start_pos,count,dema))
     {
      return(false);
     }
   int size_need_position=ArraySize(SPosition);
   if(size_need_position>0)
      return(true);

Сигнал на открытие позиции BUY. Если нужно (переменная 'InpReverse' хранит значение входного параметра 'Positions: Reverse'), то торговый сигнал будет перевернут. Если есть ограничение в какую сторону торговать (переменная 'InpTradeMode' хранит значение входного параметра 'Trade mode:'), то это ограничение будет учтено:

//--- BUY Signal
   if(dema[m_bar_current]>dema[m_bar_current+1] && dema[m_bar_current+1]>dema[m_bar_current+3])
     {
      if(!InpReverse)
        {
         if(InpTradeMode!=sell)
           {
            ArrayResize(SPosition,size_need_position+1);
            SPosition[size_need_position].pos_type=POSITION_TYPE_BUY;
            if(InpPrintLog)
               Print(__FILE__," ",__FUNCTION__,", OK: ","Signal BUY");
            return(true);
           }
        }
      else
        {
         if(InpTradeMode!=buy)
           {
            ArrayResize(SPosition,size_need_position+1);
            SPosition[size_need_position].pos_type=POSITION_TYPE_SELL;
            if(InpPrintLog)
               Print(__FILE__," ",__FUNCTION__,", OK: ","Signal SELL");
            return(true);
           }
        }
     }

Блок кода для сигнала SELL аналогичен. 


7. Создаём советник (сигналы на размещение отложенного ордера) при помощи конструктора

Имя советника будет 'iDEMA Full EA Pending.mq5' для этого откройте советник 'iDEMA Full EA.mq5' и сохраните его под новым именем.

Сначала всегда разрабатывается торговая стратегия и только потом, под эту стратегию, реализуется код. Немного изменим стратегию, которая использовалась в главе 6. Создаём советник (сигналы на открытие позиций) при помощи конструктора  вместо сигнала на открытие позиции BUY будет сигнал на размещение отложенного ордера Buy stop, а вместо сигнала на открытие позиции SELL отложенный ордер Sell stop. Для отложенных ордеров будут использоваться такие параметры:

Функция 'SearchTradingSignals' примет такой вид:

//+------------------------------------------------------------------+
//| Search trading signals                                           |
//+------------------------------------------------------------------+
bool SearchTradingSignals(void)
  {
   if(iTime(m_symbol.Name(),InpWorkingPeriod,0)==m_last_deal_in) // on one bar - only one deal
      return(true);
   if(!TimeControlHourMinute())
      return(true);
   double dema[];
   ArraySetAsSeries(dema,true);
   int start_pos=0,count=6;
   if(!iGetArray(handle_iCustom,0,start_pos,count,dema))
     {
      return(false);
     }
   int size_need_pending=ArraySize(SPending);
   if(size_need_pending>0)
      return(true);
//---
   if(InpPendingOnlyOne)
      if(IsPendingOrdersExists())
         return(true);
   if(InpPendingClosePrevious)
      m_need_delete_all=true;
//--- BUY Signal
   if(dema[m_bar_current]>dema[m_bar_current+1] && dema[m_bar_current+1]>dema[m_bar_current+3])
     {
      if(!InpReverse)
        {
         if(InpTradeMode!=sell)
           {
            ArrayResize(SPending,size_need_pending+1);
            SPending[size_need_pending].pending_type=ORDER_TYPE_BUY_STOP;
            if(InpPrintLog)
               Print(__FILE__," ",__FUNCTION__,", OK: ","Signal BUY STOP");
            return(true);
           }
        }
      else
        {
         if(InpTradeMode!=buy)
           {
            ArrayResize(SPending,size_need_pending+1);
            SPending[size_need_pending].pending_type=ORDER_TYPE_SELL_STOP;
            if(InpPrintLog)
               Print(__FILE__," ",__FUNCTION__,", OK: ","Signal SELL STOP");
            return(true);
           }
        }
     }
//--- SELL Signal
   if(dema[m_bar_current]<dema[m_bar_current+1] && dema[m_bar_current+1]<dema[m_bar_current+3])
     {
      if(!InpReverse)
        {
         if(InpTradeMode!=buy)
           {
            ArrayResize(SPending,size_need_pending+1);
            SPending[size_need_pending].pending_type=ORDER_TYPE_SELL_STOP;
            if(InpPrintLog)
               Print(__FILE__," ",__FUNCTION__,", OK: ","Signal SELL STOP");
            return(true);
           }
        }
      else
        {
         if(InpTradeMode!=sell)
           {
            ArrayResize(SPending,size_need_pending+1);
            SPending[size_need_pending].pending_type=ORDER_TYPE_BUY_STOP;
            if(InpPrintLog)
               Print(__FILE__," ",__FUNCTION__,", OK: ","Signal BUY STOP");
            return(true);
           }
        }
     }
//---
   /*if(InpPendingOnlyOne)
      if(IsPendingOrdersExists())
         return(true);
   if(InpPendingClosePrevious)
      m_need_delete_all=true;
   int size_need_pending=ArraySize(SPending);
   ArrayResize(SPending,size_need_pending+1);
   if(!InpPendingReverse)
      SPending[size_need_pending].pending_type=ORDER_TYPE_BUY_STOP;
   else
      SPending[size_need_pending].pending_type=ORDER_TYPE_SELL_STOP;
   SPending[size_need_pending].indent=m_pending_indent;
   if(InpPendingExpiration>0)
      SPending[size_need_pending].expiration=(long)(InpPendingExpiration*60);
   if(InpPrintLog)
      Print(__FILE__," ",__FUNCTION__,", OK: ","Signal BUY STOP");*/
//---
   return(true);
  }

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

Мы получили торговый сигнал, но он сработал (отложенный ордер был размещён) только тогда, когда спред стал ниже заданного:

iDEMA Full EA Pending

Рис. 9. iDEMA Full EA Pending


Файлы, прикрепленные к статье:

Название Тип файла Описание
Indicators Code Советник Содержит переменные для хранения хендлов, входные параметры индикаторов, блоки создания индикаторов
Add indicator.mq5 Советник Пример работы с файлом 'Add indicator.mq5' - добавляем стандартный индикатор
Add custom indicator.mq5 Советник
Пример добавления пользовательского индикатора
Trading engine 4.mq5 Советник Конструктор
iDEMA Full EA.mq5
Советник
Советник, созданный при помощи конструктора, - сигналы на открытие позиций
iDEMA Full EA Pending.mq5
Советник
Советник, созданный при помощи конструктора, - сигналы на размещение отложенных ордеров

Заключение

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