Событие OnTrade

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

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

void OnTrade(void)

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

В общем случае нет точного соотношения по количеству вызовов OnTrade и OnTradeTransaction. OnTrade вызывается после соответствующих вызовов OnTradeTransaction.

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

О возможности прикладного кэширования торгового окружения и истории мы поговорим в разделе о мультивалютных экспертах.

А сейчас для практического изучения OnTrade займемся экспертом, реализующим стратегию на двух отложенных ордерах OCO ("One Cancels Other"). Он будет выставлять пару стоп-ордеров на пробой диапазона и отслеживать срабатывание одного из них с тем, чтобы убрать другой. Для наглядности предусмотрим поддержку обоих типов торговых событий OnTrade и OnTradeTransaction, так что рабочая логика будет запускаться, по выбору пользователя, либо из одного обработчика, либо из другого.

Исходный код доступен в файле OCO2.mq5. Во входных параметрах: размер лота Volume (по умолчанию, 0, что означает минимальный), дистанция Distance2SLTP в пунктах до места установки каждого из ордеров, и она же определяет защитные уровни, срок истечения Expiration в секундах от времени установки, и переключатель событий ActivationBy (по умолчанию, OnTradeTransaction). Поскольку Distance2SLTP задает и отступ от текущей цены, и расстояние до стоплосса, стоплоссы двух ордеров совпадают и равны цене на момент установки.

enum EVENT_TYPE
{
   ON_TRANSACTION// OnTradeTransaction
   ON_TRADE        // OnTrade
};
   
input double Volume;            // Volume (0 - minimal lot)
input uint Distance2SLTP = 500// Distance Indent/SL/TP (points)
input ulong Magic = 1234567890;
input ulong Deviation = 10;
input ulong Expiration = 0;     // Expiration (seconds in future, 3600 - 1 hour, etc)
input EVENT_TYPE ActivationBy = ON_TRANSACTION;

Для упрощения инициализации структур запросов опишем свою собственную структуру MqlTradeRequestSyncOCO, производую от MqlTradeRequestSync.

struct MqlTradeRequestSyncOCOpublic MqlTradeRequestSync
{
   MqlTradeRequestSyncOCO()
   {
      symbol = _Symbol;
      magic = Magic;
      deviation = Deviation;
      if(Expiration > 0)
      {
         type_time = ORDER_TIME_SPECIFIED;
         expiration = (datetime)(TimeCurrent() + Expiration);
      }
   }
};

На глобальном уровне введем несколько объектов и переменных.

OrderFilter orders;        // объект для отбора ордеров
PositionFilter trades;     // объект для отбора позиций
bool FirstTick = false;    // для однократной обработки OnTick при старте
ulong ExecutionCount = 0;  // счетчик вызовов торговой стратегии RunStrategy()

Вся торговая логика за исключением момента старта будет запускаться торговыми событиями. В обработчика OnInit настраиваем объекты фильтров и ждем первого тика (ставим FirstTick в true).

int OnInit()
{
   FirstTick = true;
   
   orders.let(ORDER_MAGICMagic).let(ORDER_SYMBOL_Symbol)
      .let(ORDER_TYPE, (1 << ORDER_TYPE_BUY_STOP) | (1 << ORDER_TYPE_SELL_STOP),
      IS::OR_BITWISE);
   trades.let(POSITION_MAGICMagic).let(POSITION_SYMBOL_Symbol);
      
   return INIT_SUCCEEDED;
}

Нас интересуют только стоп-ордера (покупки/продажи) и позиции с конкретным магическим номером и текущим символом.

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

void OnTick()
{
   if(FirstTick)
   {
      RunStrategy();
      FirstTick = false;
   }
}

Например, когда включен режим OnTrade, работает данный фрагмент.

void OnTrade()
{
   static ulong count = 0;
   PrintFormat("OnTrade(%d)", ++count);
   if(ActivationBy == ON_TRADE)
   {
      RunStrategy();
   }
}

Обратите внимание, что количество вызовов самого обработчика OnTrade подсчитывается независимо от того, активируется ли стратегия здесь или нет. Аналогичным образом в обработчике OnTradeTransaction считается своё количество событий (даже если они происходят вхолостую). Так сделано, чтобы иметь возможность увидеть в журнале одновременно оба события и их счетчики.

Когда включен режим OnTradeTransaction, очевидно, RunStrategy запускается оттуда.

void OnTradeTransaction(const MqlTradeTransaction &transaction,
   const MqlTradeRequest &request,
   const MqlTradeResult &result)
{
   static ulong count = 0;
   PrintFormat("OnTradeTransaction(%d)", ++count);
   Print(TU::StringOf(transaction));
   
   if(ActivationBy != ON_TRANSACTIONreturn;
   
   if(transaction.type == TRADE_TRANSACTION_ORDER_DELETE)
   {
      // почему не здесь? ответ см. в тексте
      /* // это не сработает онлайн: m.isReady() == false, т.к. ордер временно потерян
      OrderMonitor m(transaction.order);
      if(m.isReady() && m.get(ORDER_MAGIC) == Magic && m.get(ORDER_SYMBOL) == _Symbol)
      {
         RunStrategy();
      }
      */
   }
   else if(transaction.type == TRADE_TRANSACTION_HISTORY_ADD)
   {
      OrderMonitor m(transaction.order);
      if(m.isReady() && m.get(ORDER_MAGIC) == Magic && m.get(ORDER_SYMBOL) == _Symbol)
      {
         // свойство ORDER_STATE неважно - в любом случае нужно удалить оставшийся
         // if(transaction.order_state == ORDER_STATE_FILLED
         // || transaction.order_state == ORDER_STATE_CANCELED ...)
         RunStrategy();
      }
   }
}

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

В обоих обработчиках торговых событий мы подсчитываем и выводим в журнал их количество вызовов. Для случая OnTrade оно должно будет совпасть со счетчиком ExecutionCount, который мы скоро увидим внутри RunStrategy. А вот для OnTradeTransaction её счетчик и значение ExecutionCount будут существенно отличаться, потому что стратегия здесь вызывается сильно избирательно — по одному типу событий. Из этого можно сделать вывод, что OnTradeTransaction позволяет более эффективно расходовать ресурсы за счет вызова алгоритма, только когда это уместно.

Счетчик ExecutionCount выводится в журнал при выгрузке эксперта.

void OnDeinit(const int r)
{
   Print("ExecutionCount = "ExecutionCount);
}

Теперь, наконец представим функцию RunStrategy. Обещанный счетчик инкрементируется в самом начале.

void RunStrategy()
{
   ExecutionCount++;
   ...

Далее описаны два массива для приема тикетов ордеров и их статусов из объекта-фильтра orders.

   ulong tickets[];
   ulong states[];

Для начала запросим ордера, подпадающие под наши условия. Если их 2 — всё хорошо, и делать ничего не надо.

   orders.select(ORDER_STATEticketsstates);
   const int n = ArraySize(tickets);
   if(n == 2return// OK - штатное состояние
   ...

Если остался один ордер, значит другой сработал и оставшийся нужно удалить.

   if(n > 0)          // 1 или 2+ ордера это ошибка, нужно все удалить
   {
      // удаляем все подходящие ордера, кроме частично залитых
      MqlTradeRequestSyncOCO r;
      for(int i = 0i < n; ++i)
      {
         if(states[i] != ORDER_STATE_PARTIAL)
         {
            r.remove(tickets[i]) && r.completed();
         }
      }
   }
   ...

В противном случае ордеров нет. Следовательно, нужно проверить, нет ли открытой позиции: для этого используем другой объект-фильтр trades, но результаты складываем в тот же приемный массив tickets. При отсутствии позиции выставляем новую пару ордеров.

   else // n == 0
   {
      // если нет открытых позиций, выставляем 2 ордера
      if(!trades.select(tickets))
      {
         MqlTradeRequestSyncOCO r;
         SymbolMonitor sm(_Symbol);
         
         const double point = sm.get(SYMBOL_POINT);
         const double lot = Volume == 0 ? sm.get(SYMBOL_VOLUME_MIN) : Volume;
         const double buy = sm.get(SYMBOL_BID) + point * Distance2SLTP;
         const double sell = sm.get(SYMBOL_BID) - point * Distance2SLTP;
         
         r.buyStop(lotbuybuy - Distance2SLTP * point,
            buy + Distance2SLTP * point) && r.completed();
         r.sellStop(lotsellsell + Distance2SLTP * point,
            sell - Distance2SLTP * point) && r.completed();
      }
   }
}

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

Эксперт с парой отложенных стоп-ордеров по стратегии OCO в тестере

Эксперт с парой отложенных стоп-ордеров по стратегии OCO в тестере

На стадии установки пары ордеров увидим в журнале следующие записи.

buy stop 0.01 EURUSD at 1.11151 sl: 1.10651 tp: 1.11651 (1.10646 / 1.10683)

sell stop 0.01 EURUSD at 1.10151 sl: 1.10651 tp: 1.09651 (1.10646 / 1.10683)

OnTradeTransaction(1)

TRADE_TRANSACTION_ORDER_ADD, #=2(ORDER_TYPE_BUY_STOP/ORDER_STATE_PLACED), ORDER_TIME_GTC, EURUSD, »

   » @ 1.11151, SL=1.10651, TP=1.11651, V=0.01

OnTrade(1)

OnTradeTransaction(2)

TRADE_TRANSACTION_REQUEST

OnTradeTransaction(3)

TRADE_TRANSACTION_ORDER_ADD, #=3(ORDER_TYPE_SELL_STOP/ORDER_STATE_PLACED), ORDER_TIME_GTC, EURUSD, »

   » @ 1.10151, SL=1.10651, TP=1.09651, V=0.01

OnTrade(2)

OnTradeTransaction(4)

TRADE_TRANSACTION_REQUEST

Как только один из ордеров срабатывает, происходит вот что:

order [#3 sell stop 0.01 EURUSD at 1.10151] triggered

deal #2 sell 0.01 EURUSD at 1.10150 done (based on order #3)

deal performed [#2 sell 0.01 EURUSD at 1.10150]

order performed sell 0.01 at 1.10150 [#3 sell stop 0.01 EURUSD at 1.10151]

OnTradeTransaction(5)

TRADE_TRANSACTION_DEAL_ADD, D=2(DEAL_TYPE_SELL), #=3(ORDER_TYPE_BUY/ORDER_STATE_STARTED), »

   » EURUSD, @ 1.10150, SL=1.10651, TP=1.09651, V=0.01, P=3

OnTrade(3)

OnTradeTransaction(6)

TRADE_TRANSACTION_ORDER_DELETE, #=3(ORDER_TYPE_SELL_STOP/ORDER_STATE_FILLED), ORDER_TIME_GTC, »

   » EURUSD, @ 1.10151, SL=1.10651, TP=1.09651, V=0.01, P=3

OnTrade(4)

OnTradeTransaction(7)

TRADE_TRANSACTION_HISTORY_ADD, #=3(ORDER_TYPE_SELL_STOP/ORDER_STATE_FILLED), ORDER_TIME_GTC, »

   » EURUSD, @ 1.10151, SL=1.10651, TP=1.09651, P=3

order canceled [#2 buy stop 0.01 EURUSD at 1.11151]

OnTrade(5)

OnTradeTransaction(8)

TRADE_TRANSACTION_ORDER_DELETE, #=2(ORDER_TYPE_BUY_STOP/ORDER_STATE_CANCELED), ORDER_TIME_GTC, »

   » EURUSD, @ 1.11151, SL=1.10651, TP=1.11651, V=0.01

OnTrade(6)

OnTradeTransaction(9)

TRADE_TRANSACTION_HISTORY_ADD, #=2(ORDER_TYPE_BUY_STOP/ORDER_STATE_CANCELED), ORDER_TIME_GTC, »

   » EURUSD, @ 1.11151, SL=1.10651, TP=1.11651, V=0.01

OnTrade(7)

OnTradeTransaction(10)

TRADE_TRANSACTION_REQUEST

Ордер #3 удалился сам, а ордер #2 удален (отменен) нашим экспертом.

Если запустить эксперт, изменив в настройках только режим работы через событие OnTrade, мы должны получить полностью аналогичные финансовые результаты (при прочих равных условиях, то есть, например, если не включены случайные задержки в генерации тиков). Единственное, что будет отличаться: количество вызовов функции RunStrategy. Например, за 4 месяца 2022 года на EURUSD,H1 при 88 сделках получим такие приблизительные показатели ExecutionCount (важно соотношение, а не абсолютные величины, связанные с тиками вашего брокера):

  • OnTradeTransaction — 132;
  • OnTrade — 438;

Это практическое доказательство возможности строить более избирательные алгоритмы на базе OnTradeTransaction по сравнению с OnTrade.

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

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

Ниже представлен один из вариантов настроек (Distance2SLTP=250, Expiration=5000), найденных на промежутке в 16 месяцев, с начала 2021 года по паре EURUSD.

Результаты тестового прогона эксперта OCO2

Результаты тестового прогона эксперта OCO2