Торговые события в MetaTrader 5

MetaQuotes | 24 января, 2011


Введение

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

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

Сделка приводит к изменению торговой позиции до данному символу - открытие, закрытие, наращивание, сокращение или переворот позиции. Поэтому, открытая позиция - это всегда результат проведения одной или нескольких сделок. Более подробную информацию можно получить из статьи Ордерa, позиции и сделки в MetaTrader 5.

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


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

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

В случае ручной торговли для совершения торговой операции можно нажатием F9 вызвать диалоговое окно заполнения торгового запроса. При автоматической торговли с помощью MQL5 запросы отправляются с помощью функции OrderSend(). Так как неправильные массовые запросы могут вызвать нежелательную загрузку торгового сервера, то каждый запрос перед его отправкой необходимо проверять на корректность с помощью функции OrderCheck(). Результат проверки запроса помещается в переменную, описываемую структурой MqlTradeCheckresult.

Важно: каждый запрос перед его отправкой торговому серверу предварительно проверяется на корректность в самом клиентском терминале. Заведомо неверные запросы (купить миллион лотов или купить по отрицательной цене) за пределы терминала не проходят. Это сделано для защиты торговых серверов от массовых неправильных запросов в случае ошибки в mql5-программе.

После отправки запроса он поступает на торговый сервер и проходит первичную проверку:

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

Отправка запросов торговому серверу из клиентского терминала

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

  1. запрос на изменение позиции (изменение Stop Loss и/или Take Profit);
  2. запрос на модификацию отложенного ордера (уровни цен и время истечения).

Терминал получает сообщение о том, что запрос принят и размещен в торговой подсистеме платформы MetaTrader 5. Принятый запрос сервер помещает в очередь запросов для дальнейшей обработки, результатом обработки может быть:

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


Отсылка торговых событий торговым сервером в терминал

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

Торговые события генерируются сервером в следующих случаях:

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

  1. появление сделки, которая заносится в торговую историю;
  2. перемещение отложенного ордера из разряда действующих в разряд исторических (ордер переносится в историю).

Другой пример множественных торговых событий - совершение на основании одного ордера нескольких сделок, если требуемый для сделки объем не удалось получить из одной встречной заявки. О  каждом торговом событии сервер создает и отправляет сообщение в клиентский терминал. Поэтому функция OnTrade() может быть вызвана несколько раз подряд для казалось бы одного события. Это простой пример процедуры обработки ордера в торговой подсистеме платформы MetaTrader 5.

Пример: в ожидании исполнения имеется отложенный ордер на покупку 10 лотов по EURUSD и в этот момент поступают встречные запросы на продажу - объемами 1, 4 и 5 лотов. Эти  три запроса в сумме дают объем в 10 лотов  и последовательно исполняются, если политика исполнения позволяет совершить покупку по частям.

В результате исполнения 4 запросов сервер проведет на основании имеющихся встречных заявок 3 сделки - объемами 1, 4 и 5 лотов соответственно. Сколько торговых событий будет сгенерировано в этом случае? Первая встречная заявка на продажу одного лота приведет к совершению сделки в 1 лот. Это первое торговое событие Trade (сделка объемом 1 лот). Но отложенный ордер на покупку 10 лотов также изменится - теперь он является ордером на покупку 9 лотов по EURUSD. Изменение объема в отложенном ордере - это второе торговое событие Trade (изменение объема отложенного ордера).

Генерация торговых событий

Для второй сделки на 4 лота будет сгенерировано еще два события Trade, сообщение о каждом из них будет отправлено терминалу, который инициировал первоначальный отложенный ордер на покупку EURUSD объемом 10 лотов.

Последняя сделка на 5 лотом приведет к отправке сообщений о трех торговых событиях:

  1. сделка на 5 лотов,
  2. изменение объема,
  3. перемещение ордера в историю.

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

Важно: каждое сообщение о торговом событии Trade может быть результатом одного или нескольких запросов. Каждый запрос может порождать несколько торговых событий. Нельзя полагаться на правило "Один запрос - Одно событие Trade", так как обработка запросов может происходить в несколько этапов и каждая операция может изменять состояние ордеров, позиций и торговой истории.


Обработка ордеров торговым сервером

Все ожидающие исполнения ордера в конечном счете попадут в историю - либо наступят условия для их исполнения либо ордер будет снят. Имеется несколько возможных вариантов прекращения действия ордера:

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


Общая схема обработки ордера торговым сервером

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

Именно поэтому в документации по функции OrderSend() сказано:

Возвращаемое значение

В случае успешной базовой проверки запроса функция OrderSend() возвращает  true - это не свидетельствует об успешном выполнении торговой операции. Для получения более подробного описания результата выполнения функции следует анализировать поля структуры MqlTradeResult.


Как обновляется торговля и история в терминале

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


Все сообщения из торгового сервера поступают в терминал независимо друг от друга


На рисунке показана ситуация,  когда торговый сервер сообщает mql5-программе тикет ордера, но само сообщение о торговом событии Trade (появление нового ордера) еще не поступило. Также еще не поступило сообщение об изменении в списке действующих ордеров.

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


Обработка торговых событий в MQL5

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

Алгоритм отслеживания торговых событий в эксперте таков:

  1. объявляем на глобальном уровне счетчики ордеров, позиций и сделок;
  2. определяемся с глубиной торговой истории, которую будем запрашивать в кэш mql5-программы. Чем больший объем истории загружаем в кэш, тем больше загружаются ресурсы терминала и компьютера;
  3. в функции OnInit инициализируем счетчики ордеров, позиций и сделок;
  4. определяем в каких функциях-обработчиках событий мы будем запрашивать торговую историю в кэш;
  5. Там же после загрузки торговой истории путем сравнения запомненного состояния и текущего состояния выясняем что именно произошло с торговым счетом.

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

В эксперте изменения счетчиков можно проверять в функциях OnTrade() и в OnTick()

Напишем по шагам пример программы.

1. Счетчики ордеров, позиций и сделок на глобальном уровне.

int          orders;            // количество действующих ордеров
int          positions;         // количество открытых позиций
int          deals;             // количество сделок в кэше торговой истории
int          history_orders;    // количество ордеров в кэше торговой истории
bool         started=false;     // признак инициализированности счетчиков


2. Глубину торговой истории для загрузки в кэш зададим input-переменной days (загружаем торговую историю за указанное этой переменной количество дней).

input    int days=7;            // глубина торговой истории в днях

//--- зададим на глобальном уровне границы торговой истории
datetime     start;             // дата начала торговой истории в кэше
datetime     end;               // дата конца торговой истории в кэше 


3. Инициализация счетчиков и границ торговой истории. Вынесем инициализацию счетчиков в отдельную функцию InitCounters() для лучшей читаемости кода:

int OnInit()
  {
//---
   end=TimeCurrent();
   start=end-days*PeriodSeconds(PERIOD_D1);
   PrintFormat("Границы загружаемой торговой истории: начало - %s, конец - %s",
               TimeToString(start),TimeToString(end));
   InitCounters();
//---
   return(0);
  }

Функция InitCounters() пытается загрузить в кэш торговую историю, и в случае успеха инициализирует все счетчики. Также при успешной загрузке истории значение глобальной переменной started устанавливается равным true, это означает, что счетчики инициализированы успешно.

//+------------------------------------------------------------------+
//|  инициализация счетчиков позиций, ордеров и сделок               |
//+------------------------------------------------------------------+
void InitCounters()
  {
   ResetLastError();
//--- загрузим историю 
   bool selected=HistorySelect(start,end);
   if(!selected)
     {
      PrintFormat("%s. Не удалось загрузить в кэш историю с %s по %s. Код ошибки: %d",
                  __FUNCTION__,TimeToString(start),TimeToString(end),GetLastError());
      return;
     }
//--- получим текущие значениия
   orders=OrdersTotal();
   positions=PositionsTotal();
   deals=HistoryDealsTotal();
   history_orders=HistoryOrdersTotal();
   started=true;
   Print("Счетчики ордеров,  позиций и сделок успешно инициализированы");
  }


4. В обработчиках OnTick() и OnTrade() производится проверка изменений на торговом счете. Сначала проверяется переменная started -  если ее значение равно true, вызывается функция SimpleTradeProcessor(), в противном случае вызывается функция инициализации счетчиков InitCounters().

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   if(started) SimpleTradeProcessor();
   else InitCounters();
  }
//+------------------------------------------------------------------+
//| вызывается при поступлении события Trade                         |
//+------------------------------------------------------------------+
void OnTrade()
  {
   if(started) SimpleTradeProcessor();
   else InitCounters();
  }

5. Функция SimpleTradeProcessor() проверяет изменение количество ордеров, сделок и позиций. После всех проверок вызывается функция CheckStartDateInTradeHistory(), которая при необходимости перемещает ближе к текущему моменту значение start.

//+------------------------------------------------------------------+
//| простейший пример обработки изменений в торговле и истории       |
//+------------------------------------------------------------------+
void SimpleTradeProcessor()
  {
   end=TimeCurrent();
   ResetLastError();
//--- загрузим историю 
   bool selected=HistorySelect(start,end);
   if(!selected)
     {
      PrintFormat("%s. Не удалось загрузить в кэш историю с %s по %s. Код ошибки: %d",
                  __FUNCTION__,TimeToString(start),TimeToString(end),GetLastError());
      return;
     }

//--- получим текущие значение
   int curr_orders=OrdersTotal();
   int curr_positions=PositionsTotal();
   int curr_deals=HistoryDealsTotal();
   int curr_history_orders=HistoryOrdersTotal();

//--- проверим изменения в количестве действующих ордеров
   if(curr_orders!=orders)
     {
      //--- количество действующих ордеров изменилось
      PrintFormat("Изменилось количество ордеров. Было %d, стало %d",
                  orders,curr_orders);
     /*
       еще какие-то действия в связи с изменением ордеров
     */
      //--- обновим значение
      orders=curr_orders;
     }

//--- изменения в количестве открытых позиций
   if(curr_positions!=positions)
     {
      //--- количество открытых позиций изменилось
      PrintFormat("Изменилось количество позиций. Было %d, стало %d",
                  positions,curr_positions);
      /*
       еще какие-то действия в связи с изменением в позициях
      */
      //--- обновим значение
      positions=curr_positions;
     }

//--- изменения в количестве сделок в кэше торговой истории
   if(curr_deals!=deals)
     {
      //--- количество сделок в кэше торговой истории изменилось
      PrintFormat("Изменилось количество сделок. Было %d, стало %d",
                  deals,curr_deals);
      /*
       еще какие-то действия в связи с изменением количества сделок
      */
      //--- обновим значение
      deals=curr_deals;
     }

//--- изменения в количестве исторических ордеров в кэше торговой истории
   if(curr_history_orders!=history_orders)
     {
      //--- количество исторических ордеров в кэше торговой истории изменилось
      PrintFormat("Изменилось количество ордеров в истории. Было %d, стало %d",
                  history_orders,curr_history_orders);
     /*
       еще какие-то действия в связи с изменением количества ордеров в кэше торговой истории
      */
     //--- обновим значение
     history_orders=curr_history_orders;
     }
//--- проверка на необходимость изменения границ торговой истории для запроса в кэш
   CheckStartDateInTradeHistory();
  }

Функция CheckStartDateInTradeHistory() вычисляет начальную дату запроса торговой истории на текущий момент (curr_start) и сравнивает ее с переменной start. Если разница между ними составляет больше, чем один день, то значение start корректируется и обновляются счетчики исторических ордеров и сделок.

//+------------------------------------------------------------------+
//|  изменения начальной даты для запроса торговой истории           |
//+------------------------------------------------------------------+
void CheckStartDateInTradeHistory()
  {
//--- начальный интервал, если бы мы начали работу прямо сейчас
   datetime curr_start=TimeCurrent()-days*PeriodSeconds(PERIOD_D1);
//--- убедимся, что граница начала торговой истории ушла не больше, 
//--- чем на 1 день от задуманной даты
   if(curr_start-start>PeriodSeconds(PERIOD_D1))
     {
      //--- придется подкорректировать дату начала загружаемой в кэш истории 
      start=curr_start;
      PrintFormat("Новая граница начала загружаемой торговой истории: начало => %s",
                  TimeToString(start));

      //--- теперь заново загрузим торговоую историю для поправленного интервала
      HistorySelect(start,end);

      //--- подкорректируем счетчики сделок и ордеров в истории для следующего сравнения
      history_orders=HistoryOrdersTotal();
      deals=HistoryDealsTotal();
     }
  }

Полный код эксперта DemoTradeEventProcessing.mq5 приложен к статье.


Заключение

Все операции в торгово-аналитической платформе MetaTrader 5 производятся асинхронно и отсылка сообщений обо всех изменениях на торговом счете производятся независимо друг от друга. Поэтому не нужно пытаться отслеживать одиночное событие по правилу "Один запрос - Одно торговое событие". Если требуется точно определить что именно изменилось по приходу события Trade, то нужно на каждом вызове обработчика OnTrade анализировать все сделки, позиции и ордера и сравнивать с тем состоянием, что было до его появления.