English 中文 Español Deutsch 日本語 Português 한국어 Français Italiano Türkçe
Обработка торговых событий в эксперте при помощи функции OnTrade()

Обработка торговых событий в эксперте при помощи функции OnTrade()

MetaTrader 5Примеры | 1 июня 2010, 13:15
11 994 20
KlimMalgin
KlimMalgin

Введение

Любой трейдер, имеющий дело с написанием экспертов на MQL, рано или поздно сталкивался с необходимостью составления отчетов о работе своего эксперта, или же возникала необходимость реализовать отправку уведомлений о действиях эксперта по SMS или на e-mail. В любом случае нужно "отлавливать" определенные события, происходящие на рынке или действия производимые экспертом и уведомлять о них оператора.

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

В статье будет рассматриваться обработка следующих событий:

  • Позиции
    1. Открытие
    2. Добавление
    3. Модификация (изменение стоплосса и тейкпрофита)
    4. Переворот
    5. Закрытие всей позиции
    6. Закрытие части позиции
  • Отложенные ордера
    1. Установка
    2. Модификация

1. Как это происходит? 

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

В MQL есть предопределенные и пользовательские события. Нас интересуют предопределенные, а точнее торговое событие Trade, которое относится к предопределенным.

Событие Trade генерируется каждый раз при завершении торговой операции. Каждый раз после генерации события Trade происходит вызов функции OnTrade(). Именно в функции OnTrade() и будет проводиться работа с ордерами и позициями.

2. Шаблон эксперта

Итак, для начала создадим нового эксперта. Для этого в MetaEditor'e через меню Файл -> Создать вызовем мастера MQL5. В предложенном списке выберем Советник и нажмем Далее. В диалоге "Общие параметры советника" введите Имя эксперта и свои данные, если необходимо. Я назвал советника "TradeControl". Вы можете взять это же название или придумать свое, это не важно. Никаких параметров пока указывать не будем, т.к. они будут созданы по ходу написания эксперта.

Все! Шаблон эксперта создан, осталось только добавить в него функцию OnTrade().

В результате у вас должен получиться следующий код:

//+------------------------------------------------------------------+
//|                                                 TradeControl.mq5 |
//|                                             Copyright KlimMalgin |
//|                                                                  |
//+------------------------------------------------------------------+
#property copyright "KlimMalgin"
#property link      ""
#property version   "1.00"
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   
//---
   return(0);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   
  }
//+------------------------------------------------------------------+
//| OnTrade function                                                 |
//+------------------------------------------------------------------+
void OnTrade()
  {
//---

//---
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   
  }
//+------------------------------------------------------------------+

3. Работа с позициями

Работу с торговыми событиями начнем с самого простого - с открытия и закрытия позиций. Вначале разберемся, какие процессы происходят после нажатия кнопок "Sell" и "Buy".

Если в функции OnTrade() поместить вызов:

Alert("Поступило событие Trade");

То мы увидим, что после открытия по рынку функция OnTrade(), а вместе с ней и наш Alert выполнятся четыре раза:

Вызов функции OnTrade() 

Рисунок 1. Сигналы

Почему OnTrade() вызывается именно четыре раза и как на эти вызовы можно отреагировать? Чтобы разобраться в этом, обратимся к документации:

 OnTrade

Функция вызывается при наступлении Trade, которое возникает при изменении списка выставленных ордеров и открытых позицийистории ордеров и истории сделок.

Здесь следует оговориться:

В ходе написания статьи и общения с разработчиками выяснилось, что изменения в истории не приводят к вызову OnTrade()! То есть функция OnTrade() вызывается только при изменении списка выставленных ордеров и открытых позиций! При разработке обработчика торговых событий это грозит тем, что исполненные ордера и сделки могут появляться в истории с запаздыванием, и во время выполнения функции OnTrade() обработать их не удастся.  

А теперь вернемся к работе с событиями. Как мы выяснили - при открытии по рынку, событие Trade поступает 4 раза:

  1. Формирование ордера на открытие по рынку.
  2. Совершение сделки.
  3. Уход отработанного ордера в историю.
  4. Открытие позиции.
Чтобы проследить этот процесс в терминале, обратим внимание на список ордеров во вкладке "Торговля" MetaTrader'а:


Рисунок 2. Список ордеров во вкладке "Торговля"

Когда вы хотите открыть позицию и открываетесь, например, вниз - в списке ордеров появляется ордер, имеющий статус started (рис. 2). При этом изменяется список выставленных ордеров и вызывается событие Trade. Вот первый раз, когда срабатывает функция OnTrade()Затем по сформированному ордеру совершается сделка. На этом этапе функция OnTrade() выполняется второй раз. Как только сделка будет исполнена, отработанный ордер и выполненная по нему сделка будут отправлены в историю, а функция OnTrade() будет вызвана в третий раз. На последнем этапе по исполненной сделке открывается позиция и происходит четвертый вызов OnTrade().

Для того чтобы "поймать" момент открытия позиции, нужно при каждом вызове OnTrade() анализировать список ордеров, историю ордеров и историю сделок. Это мы сейчас и будем делать!

Итак, вызвана функция OnTrade(), и нам нужно узнать, изменилось ли количество ордеров в списке вкладки "Торговля". Для этого необходимо сравнить количество ордеров в списке на момент предыдущего вызова OnTrade() и сейчас. Чтобы узнать, сколько ордеров в списке в данный момент, воспользуемся функцией OrdersTotal(), а чтобы знать, сколько ордеров было в списке на предыдущем вызове, нам придется сохранять значение OrdersTotal() в каждом вызове OnTrade(). Создадим для этого специальную переменную:

int OrdersPrev = 0;        // Хранит количество ордеров на момент предыдущего вызова OnTrade()

Переменной OrdersPrev в конце функции OnTrade() будем присваивать значение OrdersTotal().

Также нужно учесть ситуацию, когда вы запускаете советника, а в списке уже есть отложенные ордера. Чтобы эксперт не упустил их из виду, в функции OnInit() переменной OrdersPrev также нужно присвоить значение OrdersTotal(). Изменения, которые мы только что внесли в эксперт, будут выглядеть вот так: 

int OrdersPrev = 0;        // Хранит количество ордеров на момент предыдущего вызова OnTrade()


//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   OrdersPrev = OrdersTotal();
//---
   return(0);
  }
//+------------------------------------------------------------------+
//| OnTrade function                                                 |
//+------------------------------------------------------------------+
void OnTrade()
  {
//---

OrdersPrev = OrdersTotal();
//---
  }

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

if (OrdersPrev < OrdersTotal())
{
  // Ордер появился
}
else if(OrdersPrev > OrdersTotal())
{
  // Ордер исчез
}

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

Отдельной работы требует только изменение стоплосса и тейкпрофита. Я добавлю код для работы с позициями в функцию OnTrade() и давайте его рассмотрим:

void OnTrade()
  {
//---
Alert("Поступило событие Trade");

HistorySelect(start_date,TimeCurrent());

if (OrdersPrev < OrdersTotal())
{
   OrderGetTicket(OrdersTotal()-1);// Выбираем последний ордер для работы
   _GetLastError=GetLastError();
   Print("Error #",_GetLastError);ResetLastError();
   //--
   if (OrderGetInteger(ORDER_STATE) == ORDER_STATE_STARTED)
   {
      Alert(OrderGetTicket(OrdersTotal()-1),"Поступил ордер в обработку");
      LastOrderTicket = OrderGetTicket(OrdersTotal()-1);    // Сохраняем тикет ордера для дальнейшей работы
   }
   
}
else if(OrdersPrev > OrdersTotal())
{
   state = HistoryOrderGetInteger(LastOrderTicket,ORDER_STATE);

   // Если ордер не найден выдаем ошибку
   _GetLastError=GetLastError();
   if (_GetLastError != 0){Alert("Ошибка №",_GetLastError," Ордер не найден!");LastOrderTicket = 0;}
   Print("Error #",_GetLastError," state: ",state);ResetLastError();


   // Если ордер выполнен полностью
   if (state == ORDER_STATE_FILLED)
   {
      // Тогда анализируем последнюю сделку
      // --
      Alert(LastOrderTicket, "Ордер выполнен, переходим к сделке");
      switch(HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_ENTRY))
      {
         
         // Вход в рынок
         case DEAL_ENTRY_IN:
         Alert(HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_ORDER), " ордер породил сделку №",HistoryDealGetTicket(HistoryDealsTotal()-1));
         
            switch(HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_TYPE))
            {
               case 0:
               // если объемы позиции и сделки равны, значит позиция была только что открыта
                  if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) && (PositionGetDouble(POSITION_VOLUME) == HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_VOLUME)))
                  {
                     Alert("Открыта позиция buy на паре ", 
                           HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)); 
                  }
                  else
               // если объемы позиции и сделки различаются, значит позиция была наращена
                  if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) && (PositionGetDouble(POSITION_VOLUME) > HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_VOLUME)))
                  {
                     Alert("Наращена позиция buy на паре ", 
                           HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)); 
                  }
               break;
               
               case 1:
               // если объемы позиции и сделки равны, значит позиция была только что открыта
                  if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) && (PositionGetDouble(POSITION_VOLUME) == HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_VOLUME)))
                  {
                     Alert("Открыта позиция sell на паре ", 
                           HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)); 
                  }
                  else
               // если объемы позиции и сделки различаются, значит позиция была наращена
                  if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) && (PositionGetDouble(POSITION_VOLUME) > HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_VOLUME)))
                  {
                     Alert("Наращена позиция sell на паре ", 
                           HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)); 
                  }
                  
               break;
               
               default:
                  Alert("Необработанный код типа: ",
                        HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_TYPE));
               break;
            }
         break;
         
         // Выход из рынка
         case DEAL_ENTRY_OUT:
         Alert(HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_ORDER), " ордер породил сделку №",HistoryDealGetTicket(HistoryDealsTotal()-1));
         
            switch(HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_TYPE))
            {
               case 0:
               // если позиция, которую закрывали еще существует, значит закрыли только ее часть
                  if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) == true)
                  {
                     Alert("Закрыта часть позиции sell на паре ", 
                           HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL),
                           " с прибылью = ",
                           HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_PROFIT));
                  }
                  else
               // если позиция не найдена, значит она закрыта полностью
                  if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) == false)
                  {
                     Alert("Закрыта позиция sell на паре ",
                           HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL),
                           " с прибылью = ",
                           HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_PROFIT));
                  }
               break;
               
               case 1:
               // если позиция, которую закрывали еще существует, значит закрыли только ее часть
                  if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) == true)
                  {
                     Alert("Закрыта часть позиции buy на паре ", 
                           HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL),
                           " с прибылью = ",
                           HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_PROFIT));
                  }
                  else
               // если позиция не найдена, значит она закрыта полностью
                  if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) == false)
                  {
                     Alert("Закрыта позиция buy на паре ",
                           HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL),
                           " с прибылью = ",
                           HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_PROFIT));
                  }
                  
               break;
               
               default:
                  Alert("Необработанный код типа: ",
                        HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_TYPE));
               break;
            }
         break;
         
         // Разворот
         case DEAL_ENTRY_INOUT:
         Alert(HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_ORDER), " ордер породил сделку №",HistoryDealGetTicket(HistoryDealsTotal()-1));
         
            switch(HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_TYPE))
            {
               case 0:
                  Alert("Sell переворачивается на Buy на паре ",
                        HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL),
                        " прибыль в результате = ",
                        HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_PROFIT)); 
               break;
               
               case 1:
                  Alert("Buy переворачивается на Sell на паре ",
                        HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL),
                        " прибыль в результате = ",
                        HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_PROFIT)); 
               break;
               
               default:
                  Alert("Необработанный код типа: ",
                        HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_TYPE));
               break;
            }
         break;
         
         // Признак статусной записи
         case DEAL_ENTRY_STATE:
            Alert("Признак статусной записи. Неоработанный код направления: ", HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_TYPE));
         break;
      }
      // --
   }
}

OrdersPrev = OrdersTotal();

//---
  }

Так же проследите, чтобы в начале программы у вас были объявлены следующие переменные:

datetime start_date = 0;   // Дата с которой начинаем читать историю

int OrdersPrev = 0;        // Хранит количество ордеров на момент предыдущего вызова OnTrade()
int PositionsPrev = 0;     // Хранит количество позиций на момент предыдущего вызова OnTrade()
ulong LastOrderTicket = 0; // Переменная хранит тикет последнего поступившего в обработку ордера

int _GetLastError=0;       // Содержит код ошибки
long state=0;              // Для хранения статуса ордера

Вернемся к рассмотрению содержимого OnTrade().

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

В момент когда появляется новый ордер, мы в начале выбираем его и проверяем наличие ошибок:

OrderGetTicket(OrdersTotal()-1);// Выбираем последний ордер для работы
_GetLastError=GetLastError();
Print("Error #",_GetLastError);ResetLastError();

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

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

if (OrderGetInteger(ORDER_STATE) == ORDER_STATE_STARTED)
{
   Alert(OrderGetTicket(OrdersTotal()-1),"Поступил ордер в обработку");
   LastOrderTicket = OrderGetTicket(OrdersTotal()-1);    // Сохраняем тикет ордера для дальнейшей работы
}

Если его статус "started", т.е. он проверен на корректность, но еще не принят, значит в ближайшее время ожидается его исполнение, а мы просто выдаем Alert() о поступлении ордера в обработку и сохраняем его тикет для работы на следующем вызове OnTrade(). Вместо Alert() можно использовать любой другой вид оповещений.

В коде выше, конструкция

OrderGetTicket(OrdersTotal()-1)

вернет тикет последнего ордера из всего списка ордеров.

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

Это было первое условие, оно обычно срабатывает при первом вызове OnTrade(). Далее следует второе условие, которое выполняется при втором вызове OnTrade(), когда ордер исполнен, ушел в историю и должна открыться позиция.

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

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

В случае, если произошла ошибка, обработка прекращается, так как нет данных для работы и ни одно из последующих условий не выполнится. А если вызов HistoryOrderGetInteger() прошел успешно и ордер имеет статус "Ордер выполнен полностью":

// Если ордер выполнен полностью
if (state == ORDER_STATE_FILLED)

То выдаем очередное оповещение:

// Тогда анализируем последнюю сделку
// --
  Alert(LastOrderTicket, "Ордер выполнен, переходим к сделке");

И переходим к обработке сделки, которую породил этот ордер. Первым делом узнаем направление сделки (Свойство DEAL_ENTRY). Под направлением подразумевается не buy или sell, а Вход в рынок, Выход из рынка, Разворот или Признак статусной записи. Таким образом, по свойству DEAL_ENTRY нам удастся узнать был ли установлен ордер на открытие позиции, на закрытие позиции или на разворот. 

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

switch(HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_ENTRY))
{
  ...
}

Принцип действия тот же что и в работе с ордерами:

HistoryDealsTotal() вернет общее количество сделок. Для получения номера последней сделки от значения HistoryDealsTotal() отнимем единицу. Полученный номер сделки передается в функцию HistoryDealGetTicket(), которая в свою очередь передает тикет выбранной сделки в функцию HistoryDealGetInteger(). А уже HistoryDealGetInteger() по указанным тикету и типу свойства вернет значение направления сделки.

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

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

// Вход в рынок
case DEAL_ENTRY_IN:

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

После оповещения следует еще один блок switch, в котором анализируется тип сделки:

   switch(HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_TYPE))
   {
      case 0:
      // если объемы позиции и сделки равны, значит позиция была только что открыта
         if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) && (PositionGetDouble(POSITION_VOLUME) == HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_VOLUME)))
         {
            Alert("Открыта позиция buy на паре ", 
                  HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)); 
         }
         else
      // если объемы позиции и сделки различаются, значит позиция была наращена
         if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) && (PositionGetDouble(POSITION_VOLUME) > HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_VOLUME)))
         {
            Alert("Наращена позиция buy на паре ", 
                  HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)); 
         }
      break;
      
      case 1:
      // если объемы позиции и сделки равны, значит позиция была только что открыта
         if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) && (PositionGetDouble(POSITION_VOLUME) == HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_VOLUME)))
         {
            Alert("Открыта позиция sell на паре ", 
                  HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)); 
         }
         else
      // если объемы позиции и сделки различаются, значит позиция была наращена
         if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) && (PositionGetDouble(POSITION_VOLUME) > HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_VOLUME)))
         {
            Alert("Наращена позиция sell на паре ", 
                  HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)); 
         }
         
      break;
      
      default:
         Alert("Необработанный код типа: ",
               HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_TYPE));
      break;
   }

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

Как вы наверняка заметили, в коде обрабатывается не только открытие buy и sell позиций, но и их наращивание. Чтобы определить, когда позиция была наращена, а когда открыта - нужно сравнить объемы исполненной сделки и позиции, которая стала результатом этой сделки. В том случае, если объем позиции равен объему исполненной сделки - позиция была открыта, а если объемы позиции и сделки различаются - позиция была наращена. Это касается как позиций buy (в блоке case '0'), так и позиций sell (в блоке case '1'). Последним блоком идет default, в котором обрабатываются все ситуации отличные от buy и sell. Причем вся обработка заключается в оповещении о коде типа, возвращенном функцией HistoryDealGetInteger().

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

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

void GetPosition(_position &Array[])
  {
   int _GetLastError=0,_PositionsTotal=PositionsTotal();

   int temp_value=(int)MathMax(_PositionsTotal,1);
   ArrayResize(Array, temp_value);

   _ExpertPositionsTotal=0;
   for(int z=_PositionsTotal-1; z>=0; z--)
     {
      if(!PositionSelect(PositionGetSymbol(z)))
        {
         _GetLastError=GetLastError();
         Print("OrderSelect() - Error #",_GetLastError);
         continue;
        }
      else
        {
            // если позиция найдена, то помещаем информацию о ней в массив
            Array[z].type = PositionGetInteger(POSITION_TYPE);
            Array[z].time = PositionGetInteger(POSITION_TIME);
            Array[z].magic = PositionGetInteger(POSITION_MAGIC);
            Array[z].volume = PositionGetDouble(POSITION_VOLUME);
            Array[z].priceopen = PositionGetDouble(POSITION_PRICE_OPEN);
            Array[z].sl = PositionGetDouble(POSITION_SL);
            Array[z].tp = PositionGetDouble(POSITION_TP);
            Array[z].pricecurrent = PositionGetDouble(POSITION_PRICE_CURRENT);
            Array[z].comission = PositionGetDouble(POSITION_COMMISSION);
            Array[z].swap = PositionGetDouble(POSITION_SWAP);
            Array[z].profit = PositionGetDouble(POSITION_PROFIT);
            Array[z].symbol = PositionGetString(POSITION_SYMBOL);
            Array[z].comment = PositionGetString(POSITION_COMMENT);
        _ExpertPositionsTotal++;
        }
     }

   temp_value=(int)MathMax(_ExpertPositionsTotal,1);
   ArrayResize(Array,temp_value);
  }

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

/*
 *
 * Структура для хранения информации о позициях
 *
 */
struct _position
{

long     type,          // Тип позиции
         magic;         // Magic number для позиции
datetime time;          // Время открытия позиции

double   volume,        // Объем позиции
         priceopen,     // Цена позиции
         sl,            // Уровень Stop Loss для открытой позиции
         tp,            // Уровень Take Profit для открытой позиции
         pricecurrent,  // Текущая цена по символу
         comission,     // Коммисия
         swap,          // Накопленный своп
         profit;        // Текущая прибыль

string   symbol,        // Символ, по которому открыта позиция
         comment;       // Комментарий к позиции
};

int _ExpertPositionsTotal = 0;

_position PositionList[],     // Массив для хранения информации о позициях
          PrevPositionList[];

Прототип функции GetPosition() очень давно был найден в статьях на сайте www.mql4.com, но сейчас мне не удалось ее найти и я не могу указать источник. Не буду в подробностях рассматривать работу этой функции, скажу только, что в качестве параметра по ссылке ей передается массив типа _position (структура с полями, соответствующими полям позиции), в который заносится вся информация об открытых на текущий момент позициях и значениях всех их параметров.

Для того, чтобы было удобно отслеживать изменения параметров позиций, созданы два массива типа _position. Это PositionList[] (текущее состояние позиций) и PrevPositionList[] (предыдущее состояние позиций) .

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

GetPosition(PrevPositionList);

Также в начало Ontrade() нужно добавить вызов:

GetPosition(PositionList);

Теперь в массивах PositionList[] и PrevPositionList[] у нас в распоряжении будет информация о позициях на текущем и предыдущем вызове OnTrade() соответственно.

Теперь рассмотрим код собственно для отслеживания изменений sl и tp:

if ((PositionsPrev == PositionsTotal()) && (OrdersPrev == OrdersTotal()))
{
   string _alerts = "";
   bool modify = false;
     
   for (int i=0;i<_ExpertPositionsTotal;i++)
   {
      if (PrevPositionList[i].sl != PositionList[i].sl)
      {
         _alerts += "На паре "+PositionList[i].symbol+" изменен stoploss с "+ PrevPositionList[i].sl +" на "+ PositionList[i].sl +"\n";
         modify = true;
      }
      if (PrevPositionList[i].tp != PositionList[i].tp)
      {
         _alerts += "На паре "+PositionList[i].symbol+" изменен takeprofit с "+ PrevPositionList[i].tp +" на "+ PositionList[i].tp +"\n";
         modify = true;
      }
      
   }
   if (modify == true)
   {
      Alert(_alerts);
      modify = false;
   }
}

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

Все начинается с условия:

if ((PositionsPrev == PositionsTotal()) && (OrdersPrev == OrdersTotal()))

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

В начале функции объявляются две переменных:

  • _alerts - сохраняет в себе все сообщения об изменениях.
  • modify - позволяет отображать сообщения об изменениях только в том случае если они действительно были.

Далее в цикле проверяем соответствие значений тейпрофитов и стоплоссов на предыдущем и на текущем вызове OnTrade() для каждой позиции. Информация обо всех несоответствиях будет сохранена в переменной _alerts и в дальнейшем отображена для пользователя функцией Alert(). Кстати, по аналогии будет осуществляться и обработка модификации отложенных ордеров.

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

4. Работа с ордерами

Начнем с события установки отложенных ордеров.

При появлении нового отложенного ордера событие Trade генерируется всего один раз, но этого достаточно чтобы его обработать! Помещаем код работы с отложенными ордерами в тело оператора:

if (OrdersPrev < OrdersTotal())

И получаем следующее:

if (OrdersPrev < OrdersTotal())
{
   OrderGetTicket(OrdersTotal()-1);// Выбираем последний ордер для работы
   _GetLastError=GetLastError();
   Print("Error #",_GetLastError);ResetLastError();
   //--
   if (OrderGetInteger(ORDER_STATE) == ORDER_STATE_STARTED)
   {
      Alert(OrderGetTicket(OrdersTotal()-1),"Поступил ордер в обработку");
      LastOrderTicket = OrderGetTicket(OrdersTotal()-1);    // Сохраняем тикет ордера для дальнейшей работы
   }
   
   state = OrderGetInteger(ORDER_STATE);
   if (state == ORDER_STATE_PLACED)
   {
      switch(OrderGetInteger(ORDER_TYPE))
      {
         case 2:
            Alert("Отложенный ордер buy limit №", OrderGetTicket(OrdersTotal()-1)," принят!");
         break;
         
         case 3:
            Alert("Отложенный ордер sell limit №", OrderGetTicket(OrdersTotal()-1)," принят!");
         break;
         
         case 4:
            Alert("Отложенный ордер Buy Stop №", OrderGetTicket(OrdersTotal()-1)," принят!");
         break;
         
         case 5:
            Alert("Отложенный ордер Sell Stop №", OrderGetTicket(OrdersTotal()-1)," принят!");
         break;
         
         case 6:
            Alert("Отложенный ордер Buy Stop Limit №", OrderGetTicket(OrdersTotal()-1)," принят!");
         break;
                 
         case 7:
            Alert("Отложенный ордер Sell Stop Limit  №", OrderGetTicket(OrdersTotal()-1)," принят!");
         break;         
      }
   }
}

Здесь, код работы с отложенными ордерами начинается с конструкции:

   state = OrderGetInteger(ORDER_STATE);
   if (state == ORDER_STATE_PLACED)
   {

Вначале проверяется статус ордера. Ордер должен иметь статус ORDER_STATE_PLACED, т.е. должен быть принят. И если это условие выполняется, то далее следует оператор switch, который в зависимости от типа ордера выдает соответствующее сообщение.

Далее рассмотрим работу с появлением событий при модификации ордеров. Работа с модификацией ордеров будет аналогична работе с модификацией позиций. Также создается структура для хранения свойств ордеров: 

/*
 *
 * Структура для хранения информации об ордерах
 *
 */
struct _orders
{

datetime time_setup,       // Время постановки ордера
         time_expiration,  // Время истечения ордера
         time_done;        // Время исполнения или снятия ордера
         
long     type,             // Тип ордера
         state,            // Статус ордера
         type_filling,     // Тип исполнения по остатку
         type_time,        // Время жизни ордера
         ticket;           // Тикет ордера
         
long     magic,            // Идентификатор эксперта выставившего ордер 
                           // (предназначен для того, чтобы каждый эксперт 
                           // выставлял свой собственный уникальный номер)
                           
         position_id;      // Идентификатор позиции, который ставится на ордере 
                           // при его исполнении. Каждый исполненный ордер порождает 
                           // сделку, которая открывает новую или изменяет уже существующую 
                           // позицию. Идентификатор этой позиции и устанавливается 
                           // исполненному ордеру в этот момент.
                           
double volume_initial,     // Первоначальный объем при постановке ордера
       volume_current,     // Невыполненный объем
       price_open,         // Цена, указанная в ордере
       sl,                 // Уровень Stop Loss
       tp,                 // Уровень Take Profit
       price_current,      // Текущая цена по символу ордера
       price_stoplimit;    // Цена постановки Limit ордера при срабатывании StopLimit ордера
       
string symbol,             // Символ, по которому выставлен ордер
       comment;            // Комментарий
                           
};

int _ExpertOrdersTotal = 0;

_orders OrderList[],       // Массивы для хранения информации об ордерах
        PrevOrderList[];

Каждое поле структуры соответствует одному из свойств ордера. После объявления структуры объявляется переменная типа int и два массива типа _orders. Переменная _ExpertOrdersTotal будет хранить общее количество ордеров, а массивы OrderList[] и PrevOrderList[] будут хранить информацию об ордерах на текущем и на предыдущем вызове OnTrade() соответственно.

Сама функция имеет следующий вид:

void GetOrders(_orders &OrdersList[])
  {
   
   int _GetLastError=0,_OrdersTotal=OrdersTotal();

   int temp_value=(int)MathMax(_OrdersTotal,1);
   ArrayResize(OrdersList,temp_value);

   _ExpertOrdersTotal=0;
   for(int z=_OrdersTotal-1; z>=0; z--)
     {
      if(!OrderGetTicket(z))
        {
         _GetLastError=GetLastError();
         Print("GetOrders() - Error #",_GetLastError);
         continue;
        }
      else
        {
        OrdersList[z].ticket = OrderGetTicket(z);
        OrdersList[z].time_setup = OrderGetInteger(ORDER_TIME_SETUP);
        OrdersList[z].time_expiration = OrderGetInteger(ORDER_TIME_EXPIRATION);
        OrdersList[z].time_done = OrderGetInteger(ORDER_TIME_DONE);
        OrdersList[z].type = OrderGetInteger(ORDER_TYPE);
        
        OrdersList[z].state = OrderGetInteger(ORDER_STATE);
        OrdersList[z].type_filling = OrderGetInteger(ORDER_TYPE_FILLING);
        OrdersList[z].type_time = OrderGetInteger(ORDER_TYPE_TIME);
        OrdersList[z].magic = OrderGetInteger(ORDER_MAGIC);
        OrdersList[z].position_id = OrderGetInteger(ORDER_POSITION_ID);
        
        OrdersList[z].volume_initial = OrderGetDouble(ORDER_VOLUME_INITIAL);
        OrdersList[z].volume_current = OrderGetDouble(ORDER_VOLUME_CURRENT);
        OrdersList[z].price_open = OrderGetDouble(ORDER_PRICE_OPEN);
        OrdersList[z].sl = OrderGetDouble(ORDER_SL);
        OrdersList[z].tp = OrderGetDouble(ORDER_TP);
        OrdersList[z].price_current = OrderGetDouble(ORDER_PRICE_CURRENT);
        OrdersList[z].price_stoplimit = OrderGetDouble(ORDER_PRICE_STOPLIMIT);
        
        OrdersList[z].symbol = OrderGetString(ORDER_SYMBOL);
        OrdersList[z].comment = OrderGetString(ORDER_COMMENT);
        
        _ExpertOrdersTotal++;
        }
     }

   temp_value=(int)MathMax(_ExpertOrdersTotal,1);
   ArrayResize(OrdersList,temp_value);

  }

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

GetOrders(PrevOrderList);

Размещается в OnInit() и в конце OnTrade().

GetOrders(OrderList);

Размещается в начале OnTrade().

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

   for (int i = 0;i<_ExpertOrdersTotal;i++)
   {
      if (PrevOrderList[i].sl != OrderList[i].sl)
      {
         _alerts += "У ордера "+OrderList[i].ticket+" изменен stoploss с "+ PrevOrderList[i].sl +" на "+ OrderList[i].sl +"\n";
         modify = true;
      }
      if (PrevOrderList[i].tp != OrderList[i].tp)
      {
         _alerts += "У ордера "+OrderList[i].ticket+" изменен takeprofit с "+ PrevOrderList[i].tp +" на "+ OrderList[i].tp +"\n";
         modify = true;
      }
   }

Цикл делает обход всех ордеров и сравнивает значения их стоплоссов и тейкпрофитов на текущем и предыдущем вызовах OnTrade(). Если есть различия, то они сохраняются в переменной _alerts, и после выполнения цикла будут отображены функцией Alert().

Этот код помещается в теле оператора:

if ((PositionsPrev == PositionsTotal()) && (OrdersPrev == OrdersTotal()))
{

Сразу после цикла, работающего с позициями.

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

Заключение

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

Прикрепленные файлы |
tradecontrol.mq5 (19.97 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (20)
Tango_X
Tango_X | 21 окт. 2018 в 07:29
Странно...., а почему не показано как отслеживать закрытие позиции по стопу или тейку? 
Vladimir Karputov
Vladimir Karputov | 21 окт. 2018 в 07:32
Tango_X:
Странно...., а почему не показано как отслеживать закрытие позиции по стопу или тейку? 

Стаья давно писалась. С тех пор появилась новая возможность возможность 

Начиная с билда 1625 появилось замечательное перечисление ENUM_DEAL_REASON:

ENUM_DEAL_REASONОписание причины
......
DEAL_REASON_SLОперация совершена в результате срабатывания Stop Loss
DEAL_REASON_TPОперация совершена в результате срабатывания Take Profit
......

которое можно отследить в OnTradeTransaction.


Пример работы: Stop Loss Take Profit

Tango_X
Tango_X | 21 окт. 2018 в 07:41
Vladimir Karputov:

Стаья давно писалась. С тех пор появилась новая возможность возможность 

Начиная с билда 1625 появилось замечательное перечисление ENUM_DEAL_REASON:

ENUM_DEAL_REASONОписание причины
......
DEAL_REASON_SLОперация совершена в результате срабатывания Stop Loss
DEAL_REASON_TPОперация совершена в результате срабатывания Take Profit
......

которое можно отследить в OnTradeTransaction.


Пример работы: Stop Loss Take Profit

супер! спасибо!!!

Tango_X
Tango_X | 21 окт. 2018 в 12:17
Vladimir Karputov:

Стаья давно писалась. С тех пор появилась новая возможность возможность 

Начиная с билда 1625 появилось замечательное перечисление ENUM_DEAL_REASON:

ENUM_DEAL_REASONОписание причины
......
DEAL_REASON_SLОперация совершена в результате срабатывания Stop Loss
DEAL_REASON_TPОперация совершена в результате срабатывания Take Profit
......

которое можно отследить в OnTradeTransaction.


Пример работы: Stop Loss Take Profit

Еще один вопрос по ходу.

Поле "Комментарии" в позиции я используя для хранения периода открытия этой позиции, и при срабатывания Stop Loss/Take Profit терминал пишет в это поле st/tp. Как запретить терминалу и брокеру менять комментарий? Или может знаете другой способ хранения периода для каждой позиции? 

Serhii Tymchenko
Serhii Tymchenko | 2 сент. 2022 в 15:24
в mql5 не могу такую штуку допилить. Как распознавать только новые ордера независимо от комментов и мэжик №, у меня при закрытии buy сигнализирует что поступил sell (так и наоборот).
Создание и публикация отчетов о результатах торговли, отправка SMS-сообщений Создание и публикация отчетов о результатах торговли, отправка SMS-сообщений
Трейдер не всегда имеет возможность и желание находиться часами перед торговым терминалом. Особенно, если торговая система в той или иной степени формализована и позволяет автоматически идентифицировать некоторые состояния рынка. В данной статье описано, как с помощью советника, индикатора или скрипта сформировать отчет о результатах торговли в виде html-файла и загрузить его по протоколу FTP на WWW-сервер, рассмотрен вопрос отправки уведомлений о торговых событиях на мобильный телефон в виде SMS-сообщений.
Новые возможности с MetaTrader 5 Новые возможности с MetaTrader 5
MetaTrader 4 завоевал популярность у трейдеров по всему миру, и казалось бы, нельзя желать большего. Высокая производительность и стабильность, широкие возможности по написанию индикаторов, экспертов и торгово-информационных систем, возможность выбора любого из нескольких сотен брокеров - вот те основные преимущества, которые выделяют этот терминал на фоне всех остальных. Но время не стоит на месте, и вот мы уже стоим перед выбором - MetaTrader 4 или MetaTrader 5. В этой статье мы опишем основные отличия терминала 5-го поколения от нынешнего фаворита.
Пошаговое руководство по написанию MQL5-советников для начинающих Пошаговое руководство по написанию MQL5-советников для начинающих
Написание советников на MQL5 проще чем кажется, вы легко можете этому научиться. В этом руководстве вы познакомитесь с основными моментами, необходимыми для написания простого советника на основе конкретной торговой стратегии. Рассмотрена структура советника, использование встроенных технических индикаторов и торговых функций, вопросы отладки и тестирования советника на исторических данных.
Создание информационных табло с использованием классов из Стандартной библиотеки и Google Chart API Создание информационных табло с использованием классов из Стандартной библиотеки и Google Chart API
Мощный язык программирования MQL5 нацелен в первую очередь на создание автоматических торговых систем и сложных инструментов технического анализа. Но помимо прочего он позволяет создавать интересные информационные системы для отслеживания рыночной ситуации и обеспечения обратной связи с трейдером. В статье сделан обзор компонентов Стандартной библиотеки и примеры их использования на практике для этих целей. Также показан пример использования Google Charts API для создания графиков.