Изучение свойств позиции в тестере MetaTrader 5

Изучение свойств позиции в тестере MetaTrader 5В этой статье будем модифицировать эксперта, которого сделали в предыдущей статье "Свойства позиции на пользовательской информационной панели". Рассмотрим ряд вопросов, таких как:

  •    - Отслеживание события новый бар на текущем символе.
  •    - Получение данных баров.
  •    - Подключение торгового класса стандартной библиотеки.
  •    - Создание функции определяющую торговые сигналы.
  •    - Создание функции осуществляющую торговые операции.
  •    - Рассмотрим функцию OnTrade(), которая определяет торговое событие.

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


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

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

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

Чтобы подключить класс нужно написать такую строчку кода:

//+------------------------------------------------------------------+
//| ПОДКЛЮЧАЕМ КЛАСС СТАНДАРТНОЙ БИБЛИОТЕКИ                          |
//+------------------------------------------------------------------+
#include <Trade/Trade.mqh>
//---

Расположить её можно в начале файла, чтобы было удобно найти, например, после директивы #define. Команда #include указывает, что нужно взять файл Trade.mqh из директории Metatrader 5\MQL5\Include\Trade\. Таким же образом можно подключить и любой другой файл с функциями. Это очень актуально, когда объём кода проекта сильно увеличивается и в нём сложно ориентироваться.

Теперь нужно создать экземпляр класса, чтобы получить доступ ко всем его функциям. Для этого после названия класса (CTrade) нужно написать имя экземпляра:

//+------------------------------------------------------------------+
//| ЗАГРУЗКА КЛАССА                                                  |
//+------------------------------------------------------------------+
CTrade trd; // Указатель (trd) на экземпляр класса (CTrade)
//---

В этом эксперте мы воспользуемся только одной торговой функцией из класса CTrade. Это функция PositionOpen() для открытия позиции. Её же можно использовать и для переворота уже открытой позиции. Как получить функцию из класса будет показано далее в статье при создании функции, в которой будут совершаться торговые операции.

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

// Массивы ценовых данных
double
prc_c[], // Close (цена закрытия бара)
prc_o[]; // Open (цена открытия бара)
//---

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

Ниже представлен код функции NewBar() с подробными комментариями:

//+------------------------------------------------------------------+
//| ПРОВЕРКА НОВОГО БАРА                                             |
//+------------------------------------------------------------------+
bool NewBar()
  {
// Переменная для времени открытия текущего бара
   static datetime NewBar=NULL;
//---
// Массив для получения времени открытия текущего бара
   static datetime lastbar_time[1]={0};
//---
// Получим время открытия текущего бара
// Если возникла ошибка при получении, сообщим об этом
   if(CopyTime(_Symbol,_Period,0,1,lastbar_time)==-1)
     { Print(__FUNCTION__,": Ошибка при копировании времени открытия бара: "+IS(GetLastError())+""); }
//---
// Если это первый вызов функции
   if(NewBar==NULL)
     {
      // Установим время
      NewBar=lastbar_time[0];
      Print(__FUNCTION__,": Инициализация ["+_Symbol+"][TF: "+TFtoS(_Period)+"]["+TSdms(lastbar_time[0])+"]");
      return(false); // Вернём false и выйдем 
     }
//---
// Если время отличается
   if(NewBar!=lastbar_time[0])
     {
      NewBar=lastbar_time[0]; // Установим время и выйдем 
      return(true); // Запомним время и вернем true
     }
//---
   return(false); // Дошли до этого места - значит бар не новый, вернем false
  }
//---

В коде выше видно, что функция NewBar() возвращает true, если бар новый и false, если нового бара ещё нет. Таким образом можно контролировать ситуацию при торговле/тестировании совершая торговые операции только по сформировавшимся барам.

В самом начале функции объявлены две локальные (объявлены внутри пользовательской функции) статические (static) переменная и массив типа datetime. Статические локальные переменные сохраняют свои значения даже после выхода из функции. При каждом следующем вызове функции такие локальные переменные содержат те значения, которые они получили при предыдущем вызове.

Также обратите внимание на функцию CopyTime(). С её помощью мы получаем в массив lastbar_time время последнего бара. Обязательно ознакомьтесь с синтаксисом этой функции в Справке языка MQL5.

Можно ещё заметить функцию TFtoS(), которая до этого ни разу не упоминалась в этой серии статей. Она конвертирует значения таймфреймов в понятную для пользователя строку. Вот её код:

//+------------------------------------------------------------------+
//| ВОЗВРАЩАЕТ_СТРОКУ_TIMEFRAME                                      |
//+------------------------------------------------------------------+
string TFtoS(int TF)
  {
   string str="";
   int tf_check=NULL;
//---
// Если переданное значение некорректно, ...
   if(TF<=0) { tf_check=_Period; } // ...конвертируем текущий период
   else { tf_check=TF; } // Иначе то, которое передано
//---
   switch(tf_check)
     {
      case PERIOD_M1  : str="m1";  break;
      case PERIOD_M2  : str="m2";  break;
      case PERIOD_M3  : str="m3";  break;
      case PERIOD_M4  : str="m4";  break;
      case PERIOD_M5  : str="m5";  break;
      case PERIOD_M6  : str="m6";  break;
      case PERIOD_M10 : str="m10"; break;
      case PERIOD_M12 : str="m12"; break;
      case PERIOD_M15 : str="m15"; break;
      case PERIOD_M20 : str="m20"; break;
      case PERIOD_M30 : str="m30"; break;
      case PERIOD_H1  : str="H1";  break;
      case PERIOD_H2  : str="H2";  break;
      case PERIOD_H3  : str="H3";  break;
      case PERIOD_H4  : str="H4";  break;
      case PERIOD_H6  : str="H6";  break;
      case PERIOD_H8  : str="H8";  break;
      case PERIOD_H12 : str="H12"; break;
      case PERIOD_D1  : str="D1";  break;
      case PERIOD_W1  : str="W1";  break;
      case PERIOD_MN1 : str="MN1"; break;
     }
//---
   return(str);
  }
//---

Как использовать функцию NewBar() будет показано далее в статье, когда подготовим для работы все остальные необходимые функции. А теперь рассмотрим функцию GetDataBars(), которая принимает значения запрошенного количества баров.

//+------------------------------------------------------------------+
//| ПОЛУЧАЕТ ЗНАЧЕНИЯ БАРОВ                                          |
//+------------------------------------------------------------------+
void GetDataBars()
  {
   int amount=2; // Количество баров для получения их данных в массив
//---
// Перевернём таймсерию ... 3 2 1 0
   ArraySetAsSeries(prc_c,true);
   ArraySetAsSeries(prc_o,true);
//---
// ЗАПОЛНЕНИЕ ЦЕНОВЫХ МАССИВОВ ДАННЫМИ
//---
// Получим цену закрытия бара
// Если полученных значений меньше, чем запрошено
// вывести сообщение об этом
   if(CopyClose(_Symbol,_Period,0,amount,prc_c)<amount)
     {
      Print("Не удалось скопировать значения ("
            +_Symbol+"; "+TFtoS(_Period)+") в массив цен Close! "
            "Ошибка ("+IS(GetLastError())+"): "+ErrorDesc(GetLastError())+"");
     }
//---
// Получим цену открытия бара
// Если полученных значений меньше, чем запрошено
// вывести сообщение об этом
   if(CopyOpen(_Symbol,_Period,0,amount,prc_o)<amount)
     {
      Print("Не удалось скопировать значения ("
            +_Symbol+"; "+TFtoS(_Period)+") в массив цен Open! "
            "Ошибка ("+IS(GetLastError())+"): "+ErrorDesc(GetLastError())+"");
     }
  }
//---

Рассмотрим подробнее код выше. В начале, в переменной amount указываем, данные скольких баров нужно получить. Далее, с помощью функции ArraySetAsSeries() устанавливаем индексацию массивов так, чтобы значения последнего (текущего) бара было в нулевом индексе массива. То есть, если мы хотим в расчётах использовать значение последнего бара, то это будет выглядеть так, например, для цены открытия: prc_o[0]. А предпоследнего так: prc_o[1].

Получение цен закрытия и открытия производится так же, как это было в функции NewBar(), когда получали время последнего бара. Только в этом случае используются функции CopyClose() и CopyOpen(). Аналогичные функции есть и для максимальной CopyHigh() и минимальной CopyLow() цен баров.

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

//+------------------------------------------------------------------+
//| ОПРЕДЕЛЯЕТ ТОРГОВЫЕ СИГНАЛЫ                                      |
//+------------------------------------------------------------------+
int GetTradingSignal()
  {
// Сигнал на продажу (1) :
   if(prc_c[1]<prc_o[1]) { return(1); }
//---
// Сигнал на покупку (0) :
   if(prc_c[1]>prc_o[1]) { return(0); }
//---
// Отсутствие сигнала (3)
   return(3);
  }
//---

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

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

//+------------------------------------------------------------------+
//| ТОРГОВЫЙ БЛОК                                                    |
//+------------------------------------------------------------------+
void TradingBlock()
  {
   int signal=-1; // Переменная для приёма сигнала
   string comment="hello :)"; // Комментарий для позиции
   double
   blot=0.1, // Начальный объём позиции
   lot=0.0,  // Для расчёта объёма в случае переворота позиции
   ask=0.0,  // Цена ask
   bid=0.0;  // Цены bid
//---
   signal=GetTradingSignal(); // Получим сигнал
//---
// Узнаем, есть ли позиция
   isPos=PositionSelect(_Symbol);
//---
// Если сигнал на покупку
   if(signal==0)
     {
      // Получим цену Ask
      ask=ND(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits);
      //---
      // Если позиции нет
      if(!isPos)
        {
         // Откроем позицию
         // Если позиция не открылась, вывести сообщение об этом
         if(!trd.PositionOpen(_Symbol,ORDER_TYPE_BUY,blot,ask,0,0,comment))
           { Print("Ошибка при открытии позиции BUY: ",GetLastError()," - ",ErrorDesc(GetLastError())); }
        }
      else // Если позиция есть
        {
         // Получим тип позиции
         pos_type=(ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
         //---
         // Если позиция SELL
         if(pos_type==POSITION_TYPE_SELL)
           {
            // Получим объём позиции
            pos_volume=PositionGetDouble(POSITION_VOLUME);
            //---
            // Скорректируем объём
            lot=ND(pos_volume+blot,2);
            //---
            // Откроем позицию
            // Если позиция не открылась, вывести сообщение об этом
            if(!trd.PositionOpen(_Symbol,ORDER_TYPE_BUY,lot,ask,0,0,comment))
              { Print("Ошибка при открытии позиции SELL: ",GetLastError()," - ",ErrorDesc(GetLastError())); }
           }
        }
      //---
      return;
     }
//---
// Если сигнал на продажу
   if(signal==1)
     {
      // Получим цену Bid
      bid=ND(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits);
      //---
      // Если позиции нет
      if(!isPos)
        {
         // Откроем позицию
         // Если позиция не открылась, вывести сообщение об этом
         if(!trd.PositionOpen(_Symbol,ORDER_TYPE_SELL,blot,bid,0,0,comment))
           { Print("Ошибка при открытии позиции SELL: ",GetLastError()," - ",ErrorDesc(GetLastError())); }
        }
      else // Если позиция есть
        {
         // Получим тип позиции
         pos_type=(ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
         //---
         // Если позиция BUY
         if(pos_type==POSITION_TYPE_BUY)
           {
            // Получим объём позиции
            pos_volume=PositionGetDouble(POSITION_VOLUME);
            //---
            // Скорректируем объём
            lot=ND(pos_volume+blot,2);
            //---
            // Откроем позицию
            // Если позиция не открылась, вывести сообщение об этом
            if(!trd.PositionOpen(_Symbol,ORDER_TYPE_SELL,lot,bid,0,0,comment))
              { Print("Ошибка при открытии позиции SELL: ",GetLastError()," - ",ErrorDesc(GetLastError())); }
           }
        }
      //---
      return;
     }
  }
//---

Думаю, что до момента открытия позиции не должно возникнуть вопросов. В коде выше видно (выделенные строки), что после указателя (trd) установлена точка, после которой идёт функция PositionOpen(). Именно так из класса вызывается та или иная функция. В момент, когда Вы вводите точку, выходит список со всеми функциями, которые содержит в себе класс (см. рисунок ниже). Остаётся только выбрать из списка нужную функцию:

Вызов функции из класса

В функции TradingBlock() два основных блока, для покупки и для продажи. Сразу после определения направления сигнала получаем цену ask для покупки и bid для продажи. Для сокращения кода функция нормализации приведена в такой вид:

//+------------------------------------------------------------------+
//| НОРМАЛИЗАЦИЯ                                                     |
//+------------------------------------------------------------------+
double ND(double value,int digits)
  {
   return(NormalizeDouble(value,digits));
  }
//---

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

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

//+------------------------------------------------------------------+
//| ИНИЦИАЛИЗАЦИЯ                                                    |
//+------------------------------------------------------------------+
int OnInit()
  {
   NewBar(); // Инициализируем новый бар
//---
// Получить свойства позиции и
// обновить значения на панели
   GetPropPosition();
//---
   return(0);
  }
//+------------------------------------------------------------------+
//| ДЕИНИЦИАЛИЗАЦИЯ                                                  |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
// Вывести в журнал причину деинициализации
   Print(GetTextReason(reason));
//---
// При удалении с графика
   if(reason==REASON_REMOVE)
     {
      // Удалить все объекты с графика,
      // которые относятся к информационной панели
      DeleteInfoPanel();
     }
  }
//+------------------------------------------------------------------+
//| СОБЫТИЕ ТИК ТЕКУЩЕГО ИНСТРУМЕНТА                                 |
//+------------------------------------------------------------------+
void OnTick()
  {
// Если бар не новый, выходим
   if(!NewBar()) { return; }
   else // Если есть новый бар
     {
      GetDataBars(); // Получим данные баров
      TradingBlock(); // Проверим условия и торгуем
     }
//---
// Получить свойства позиции и
// обновить значения на панели
   GetPropPosition();
  }
//---

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

//+------------------------------------------------------------------+
//| ТОРГОВОЕ СОБЫТИЕ                                                 |
//+------------------------------------------------------------------+
void OnTrade()
  {
// Получить свойства позиции и
// обновить значения на панели
   GetPropPosition();
  }
//---

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

Настройте тестер, включите визуализацию и нажмите кнопку Старт. Эксперт начнёт проводить сделки в тестере и Вы будете видеть примерно такую картинку:

Режим визуализации в тестере MetaTrader 5

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

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

Кроме того, что сделано выше, я добавил ещё один массив для названий свойств позиции на информационной панели:

//---
// Массив названий свойств позиции
string txt_prop[SZIP]=
  {
   "Symbol :",
   "Magic Number :",
   "Comment :",
   "Swap :",
   "Commission :",
   "Price Open :",
   "Current Price :",
   "Profit :",
   "Volume :",
   "Stop Loss :",
   "Take Profit :",
   "Time :",
   "Identifier :",
   "Type :"
  };
//---

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

//---
// Список названий свойств позиции и их значений
   for(int i=0; i<SZIP; i++)
     {
      // Название свойства
      CreateLabel(0,0,nm_prop[i],txt_prop[i],anchor,corner,bsc_fnt,font_sz,clr_fnt,xF,arrY[i],2);
      //---
      // Значение свойства
      CreateLabel(0,0,vl_prop[i],GetValInfoPanel(i),anchor,corner,bsc_fnt,font_sz,clr_fnt,xS,arrY[i],2);
     }
//---

Ещё в начале функции SetInfoPanel() можно увидеть вот такую строку:

//---
   if(MQL5InfoInteger(MQL5_VISUAL_MODE)) { yF=2; fln=16; }
//---

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

Вот теперь точно всё. Ниже можно скачать исходник эксперта expGPPtester.mq5. Если возникли вопросы, спрашивайте.

Успехов!




Скачать эксперт expGPPtester.mq5



Комментариев нет :

Отправить комментарий