Синхронные и асинхронные запросы

Прежде чем углубляться в детали, напомним, что каждая MQL-программа выполняется в собственном потоке, и потому параллельная асинхронная обработка транзакций (и прочих событий) возможна только за счет того, что этим занималась бы другая MQL-программа. При этом необходимо обеспечить передачу информации между программами. Мы уже знаем пару способов для этого: глобальные переменные терминала и файлы. В 7-ой Части книги мы познакомимся с другими возможностями, такими как графические ресурсы и базы данных.

Действительно, представим себе, что эксперт аналогичный TradeTransactions.mq5 выполняется параллельно с торговым экспертом и сохраняет полученные транзакции (не обязательно все поля, а лишь выборочные, влияющие на принятие решений) в глобальных переменных. Тогда торговый эксперт мог бы сразу после отправки очередного запроса проверять глобальные переменные и считывать из них результаты, не покидая текущей функции. Более того, в нем не нужен собственный обработчик OnTradeTransaction.

Однако запуск стороннего эксперта не очень просто организовать. Чисто технически это можно было сделать за счет создания объекта-графика и применения в нем шаблона с предопределенным экспертом-монитором транзакций. Но есть более легкий способ. Дело в том, что события OnTradeTransaction транслируются не только в эксперты, но и в индикаторы. А индикатор — наиболее просто запускаемый тип MQL-программы: достаточно вызвать iCustom.

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

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

Назовем данный индикатор TradeTransactionRelay.mq5. Опишем в нем единственный буфер. В принципе, его можно сделать невидимым, потому что в него будут записываться данные, не подлежащие визуализации, но мы оставили его видимым для подтверждения самой концепции.

#property indicator_chart_window
#property indicator_buffers 1
#property indicator_plots   1
   
double Buffer[];
   
void OnInit()
{
   SetIndexBuffer(0BufferINDICATOR_DATA);
}

Обработчик OnCalculate пустой.

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[])
{
   return rates_total;
}

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

#include <MQL5Book/ConverterT.mqh>
Converter<ulong,doublecnv;

А вот и функция OnTradeTransaction.

#define FIELD_NUM 6  // наиболее важные поля в MqlTradeResult
   
void OnTradeTransaction(const MqlTradeTransaction &transaction,
   const MqlTradeRequest &request,
   const MqlTradeResult &result)
{
   if(transaction.type == TRADE_TRANSACTION_REQUEST)
   {
      ArraySetAsSeries(Buffertrue);
      
      // сохраняем FIELD_NUM полей результата в последовательные ячейки буфера
      const int offset = (int)((result.request_id * FIELD_NUM)
         % (Bars(_Symbol_Period) / FIELD_NUM * FIELD_NUM));
      Buffer[offset + 1] = result.retcode;
      Buffer[offset + 2] = cnv[result.deal];
      Buffer[offset + 3] = cnv[result.order];
      Buffer[offset + 4] = result.volume;
      Buffer[offset + 5] = result.price;
      // это присваивание должно идти последним,
      // потому что оно является флагом готовности результатов
      Buffer[offset + 0] = result.request_id;
   }
}

Мы решили сохранять только 6 наиболее важных полей структуры MqlTradeResult. При желании можно расширить механизм на всю структуру целиком, но для переноса строкового поля comment потребуется массив символов, под который придется резервировать довольно много элементов.

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

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

Индикатор готов. Теперь приступим к реализации новой модификации тестового эксперта — OrderSendTransaction3.mq5 (ура, это его последняя версия). Опишем в нем переменную handle под дескриптор индикатора и создадим индикатор в OnInit.

int handle = 0;
   
int OnInit()
{
   ...
   const static string indicator = "MQL5Book/p6/TradeTransactionRelay";
   handle = iCustom(_SymbolPERIOD_D1indicator);
   if(handle == INVALID_HANDLE)
   {
      Alert("Can't start indicator "indicator);
      return INIT_FAILED;
   }
   return INIT_SUCCEEDED;
}

Для чтения результатов запросов из индикаторного буфера подготовим вспомогательную функцию AwaitAsync. Она принимает в качестве первого параметра ссылку на структуру MqlTradeRequestSync, куда будут в случае успеха записаны результаты, полученные из буфера индикатора с дескриптором handle. Идентификатор интересующего нас запроса уже должен быть во вложенной структуре, в поле result.request_id. Разумеется, здесь мы должны считывать данные по такому же принципу — шестерками баров.

#define FIELD_NUM   6  // наиболее важные поля в MqlTradeResult
#define TIMEOUT  1000  // 1 секунда
   
bool AwaitAsync(MqlTradeRequestSync &rconst int _handle)
{
   Converter<ulong,doublecnv;
   const int offset = (int)((r.result.request_id * FIELD_NUM)
      % (Bars(_Symbol_Period) / FIELD_NUM * FIELD_NUM));
   const uint start = GetTickCount();
   // ждем поступления результатов или таймаута
   while(!IsStopped() && GetTickCount() - start < TIMEOUT)
   {
      double array[];
      if((CopyBuffer(_handle0offsetFIELD_NUMarray)) == FIELD_NUM)
      {
         ArraySetAsSeries(arraytrue);
         // когда найден request_id, заполняем остальные поля результатами
         if((uint)MathRound(array[0]) == r.result.request_id)
         {
            r.result.retcode = (uint)MathRound(array[1]);
            r.result.deal = cnv[array[2]];
            r.result.order = cnv[array[3]];
            r.result.volume = array[4];
            r.result.price = array[5];
            PrintFormat("Got Req=%d at %d ms",
               r.result.request_idGetTickCount() - start);
            Print(TU::StringOf(r.result));
            return true;
         }
      }
   }
   Print("Timeout for: ");
   Print(TU::StringOf(r));
   return false;
}

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

void OnTimer()
{
   EventKillTimer();
   
   MqlTradeRequestSync::AsyncEnabled = true;
   
   MqlTradeRequestSync request;
   request.magic = Magic;
   request.deviation = Deviation;
   
   const double volume = Volume == 0 ?
      SymbolInfoDouble(_SymbolSYMBOL_VOLUME_MIN) : Volume;
   ...

Шаг 1.

   Print("Start trade");
   ResetLastError();
   if((bool)(Type == MARKET_BUY ? request.buy(volume) : request.sell(volume)))
   {
      Print("OK Open?");
   }
   
   if(!(AwaitAsync(requesthandle) && request.completed()))
   {
      Print("Failed Open");
      return;
   }
   ...

Шаг 2.

   Print("SL/TP modification");
   ...
   if(request.adjust(SLTP))
   {
      Print("OK Adjust?");
   }
   
   if(!(AwaitAsync(requesthandle) && request.completed()))
   {
      Print("Failed Adjust");
   }

Шаг 3.

   Print("Close down");
   if(request.close(request.result.position))
   {
      Print("OK Close?");
   }
   
   if(!(AwaitAsync(requesthandle) && request.completed()))
   {
      Print("Failed Close");
   }
   
   Print("Finish");
}

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

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

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

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

Start trade

OK Open?

Got Req=1 at 62 ms

DONE, D=1282677007, #=1300045365, V=0.01, @ 1.10564, Bid=1.10564, Ask=1.10564, Order placed, Req=1

Waiting for position for deal D=1282677007

SL/TP modification

OK Adjust?

Got Req=2 at 63 ms

DONE, Order placed, Req=2

Close down

OK Close?

Got Req=3 at 78 ms

DONE, D=1282677008, #=1300045366, V=0.01, @ 1.10564, Bid=1.10564, Ask=1.10564, Order placed, Req=3

Finish

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

Не обращайте внимание на разночтение статуса (DONE) и строкового описания ("Order placed"). Дело в том, что комментарий (а также поля ask/bid) остается в структуре с момента отправки функцией OrderSendAsync, а окончательный статус в поле retcode записывает наша функция AwaitAsync. Для нас важно, что в структуре с результатами являются актуальными номера тикетов (deal и order), цена исполнения (price) и объем (volume).

Используя наработки примера OrderSendTransaction3.mq5, создадим новую версию сеточного эксперта PendingOrderGrid3.mq5 (напомним, что предыдущая описана в разделе Функции для чтения свойств позиций). Её особенностью будет возможность устанавливать полную сетку ордеров в синхронном или асинхронном режиме, по выбору пользователя. Также мы засечем времена установки полной сетки для сравнения.

Режимом управляет входная переменная EnableAsyncSetup, а под дескриптор индикатора выделена, как обычно, переменная handle.

input bool EnableAsyncSetup = false;
   
int handle;

При инициализации, в случае асинхронного режима, создаем экземпляр индикатора TradeTransactionRelay.

int OnInit()
{
   ...
   if(EnableAsyncSetup)
   {
      const uint start = GetTickCount();
      const static string indicator = "MQL5Book/p6/TradeTransactionRelay";
      handle = iCustom(_SymbolPERIOD_D1indicator);
      if(handle == INVALID_HANDLE)
      {
         Alert("Can't start indicator "indicator);
         return INIT_FAILED;
      }
      PrintFormat("Started in %d ms"GetTickCount() - start);
   }
   ...
}

В целях упрощения кодирования мы заменили в функции SetupGrid двумерный массив request на одномерный.

uint SetupGrid()
{
   ...                                  // было:
   MqlTradeRequestSyncLog request[];    // MqlTradeRequestSyncLog request[][2];
   ArrayResize(requestGridSize * 2);  // ArrayResize(request, GridSize);
   ...
}

Далее в цикле по массиву вместо обращений типа request[i][1] используется адресация request[i * 2 + 1].

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

Для каждого запроса, согласно его request_id рассчитывается свое смещений в индикаторном буфере: все смещения помещаются в массив offset. По мере получения подтверждений запросов, соответствующие элементы массива помечаются отработанными путем записи туда значения -1. Количество выполненных запросов подсчитывается в переменной done. Когда оно сравняется с размером массива, вся сетка готова.

bool AwaitAsync(MqlTradeRequestSyncLog &r[], const int _handle)
{
   Converter<ulong,doublecnv;
   int offset[];
   const int n = ArraySize(r);
   int done = 0;
   ArrayResize(offsetn);
   
   for(int i = 0i < n; ++i)
   {
      offset[i] = (int)((r[i].result.request_id * FIELD_NUM)
         % (Bars(_Symbol_Period) / FIELD_NUM * FIELD_NUM));
   }
   
   const uint start = GetTickCount();
   while(!IsStopped() && done < n && GetTickCount() - start < TIMEOUT)
   for(int i = 0i < n; ++i)
   {
      if(offset[i] == -1continue// пропускаем пустые элементы
      double array[];
      if((CopyBuffer(_handle0offset[i], FIELD_NUMarray)) == FIELD_NUM)
      {
         ArraySetAsSeries(arraytrue);
         if((uint)MathRound(array[0]) == r[i].result.request_id)
         {
            r[i].result.retcode = (uint)MathRound(array[1]);
            r[i].result.deal = cnv[array[2]];
            r[i].result.order = cnv[array[3]];
            r[i].result.volume = array[4];
            r[i].result.price = array[5];
            PrintFormat("Got Req=%d at %d ms"r[i].result.request_id,
               GetTickCount() - start);
            Print(TU::StringOf(r[i].result));
            offset[i] = -1// помечаем обработанным
            done++;
         }
      }
   }
   return done == n;
}

Возвращаясь к функции SetupGrid, покажем, как вызов AwaitAsync производится после цикла отправки запросов.

uint SetupGrid()
{
   ...
   const uint start = GetTickCount();
   for(int i = 0i < (int)GridSize / 2; ++i)
   {
      // вызовы buyLimit/sellStopLimit/sellLimit/buyStopLimit
   }
   
   if(EnableAsyncSetup)
   {
      if(!AwaitAsync(requesthandle))
      {
         Print("Timeout");
         return TRADE_RETCODE_ERROR;
      }
   }
   
   PrintFormat("Done %d requests in %d ms (%d ms/request)",
      GridSize * 2GetTickCount() - start,
      (GetTickCount() - start) / (GridSize * 2));
   ...
}

Если при установке сетки случится таймаут (не все запросы получат подтверждение за отведенное время), мы вернем код TRADE_RETCODE_ERROR, и эксперт попытается "откатить" то, что успел создать.

Важно отметить, что асинхронный режим предполагается только для установки полной сетки, когда требуется отправить пакет запросов. В остальных случаях будет по-прежнему использоваться синхронный режим. Поэтому мы должны установить флаг MqlTradeRequestSync::AsyncEnabled в true перед циклом отправки и вернуть в false после. Однако здесь есть одна тонкость. Внутри цикла могут случиться ошибки, из-за которых он досрочно прерывается с возвратом последнего кода с сервера. Таким образом, если мы разместим сброс асинхронного режима после цикла, нет гарантии, что он будет сброшен.

Чтобы решить эту проблему в файле MqlTradeSync.mqh добавлен маленький класс AsyncSwitcher, управляющий включением и отключением асинхронного режима из своих конструктора и деструктора. Это в духе концепции по управлению ресурсами RAII, рассмотренной в разделе Управление дескрипторами файлов.

class AsyncSwitcher
{
public:
   AsyncSwitcher(const bool enabled = true)
   {
      MqlTradeRequestSync::AsyncEnabled = enabled;
   }
   ~AsyncSwitcher()
   {
      MqlTradeRequestSync::AsyncEnabled = false;
   }
};

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

uint SetupGrid()
{
   ...
   AsyncSwitcher sync(EnableAsyncSetup);
   ...
   for(int i = 0i < (int)GridSize / 2; ++i)
   {
      ...
   }
   ...
}

Эксперт готов. Попробуем запустить его два раза: в синхронном и асинхронном режимах для сетки достаточно большого размера (10 уровней, шаг сетки 200).

Для сетки в 10 уровней мы получим 20 запросов, поэтому ниже показаны фрагменты журнала с сокращениями. Сначала был использован синхронный режим. Поясним, что надпись о готовности запросов выводится раньше сообщений о самих запросах, потому что последние генерируются деструкторами структур при выходе из функции. Скорость обработки: 51мс на запрос.

Start setup at 1.10379

Done 20 requests in 1030 ms (51 ms/request)

TRADE_ACTION_PENDING, EURUSD, ORDER_TYPE_BUY_LIMIT, V=0.01, ORDER_FILLING_FOK, @ 1.10200, »

   » ORDER_TIME_GTC, M=1234567890, G[1.10400]

DONE, #=1300978336, V=0.01, Request executed, Req=1

TRADE_ACTION_PENDING, EURUSD, ORDER_TYPE_SELL_STOP_LIMIT, V=0.01, ORDER_FILLING_FOK, @ 1.10200, »

   » X=1.10400, ORDER_TIME_GTC, M=1234567890, G[1.10400]

DONE, #=1300978337, V=0.01, Request executed, Req=2

TRADE_ACTION_PENDING, EURUSD, ORDER_TYPE_BUY_LIMIT, V=0.01, ORDER_FILLING_FOK, @ 1.10000, »

   » ORDER_TIME_GTC, M=1234567890, G[1.10400]

DONE, #=1300978343, V=0.01, Request executed, Req=5

TRADE_ACTION_PENDING, EURUSD, ORDER_TYPE_SELL_STOP_LIMIT, V=0.01, ORDER_FILLING_FOK, @ 1.10000, » 

   » X=1.10200, ORDER_TIME_GTC, M=1234567890, G[1.10400]

DONE, #=1300978344, V=0.01, Request executed, Req=6

TRADE_ACTION_PENDING, EURUSD, ORDER_TYPE_BUY_LIMIT, V=0.01, ORDER_FILLING_FOK, @ 1.09800, »

   » ORDER_TIME_GTC, M=1234567890, G[1.10400]

DONE, #=1300978348, V=0.01, Request executed, Req=9

TRADE_ACTION_PENDING, EURUSD, ORDER_TYPE_SELL_STOP_LIMIT, V=0.01, ORDER_FILLING_FOK, @ 1.09800, »

   » X=1.10000, ORDER_TIME_GTC, M=1234567890, G[1.10400]

DONE, #=1300978350, V=0.01, Request executed, Req=10

...

TRADE_ACTION_PENDING, EURUSD, ORDER_TYPE_SELL_LIMIT, V=0.01, ORDER_FILLING_FOK, @ 1.10600, »

   » ORDER_TIME_GTC, M=1234567890, G[1.10400]

DONE, #=1300978339, V=0.01, Request executed, Req=3

TRADE_ACTION_PENDING, EURUSD, ORDER_TYPE_BUY_STOP_LIMIT, V=0.01, ORDER_FILLING_FOK, @ 1.10600, »

   » X=1.10400, ORDER_TIME_GTC, M=1234567890, G[1.10400]

DONE, #=1300978340, V=0.01, Request executed, Req=4

TRADE_ACTION_PENDING, EURUSD, ORDER_TYPE_SELL_LIMIT, V=0.01, ORDER_FILLING_FOK, @ 1.10800, »

   » ORDER_TIME_GTC, M=1234567890, G[1.10400]

DONE, #=1300978345, V=0.01, Request executed, Req=7

TRADE_ACTION_PENDING, EURUSD, ORDER_TYPE_BUY_STOP_LIMIT, V=0.01, ORDER_FILLING_FOK, @ 1.10800, »

   » X=1.10600, ORDER_TIME_GTC, M=1234567890, G[1.10400]

DONE, #=1300978347, V=0.01, Request executed, Req=8

...

TRADE_ACTION_PENDING, EURUSD, ORDER_TYPE_SELL_LIMIT, V=0.01, ORDER_FILLING_FOK, @ 1.11400, »

   » ORDER_TIME_GTC, M=1234567890, G[1.10400]

DONE, #=1300978365, V=0.01, Request executed, Req=19

TRADE_ACTION_PENDING, EURUSD, ORDER_TYPE_BUY_STOP_LIMIT, V=0.01, ORDER_FILLING_FOK, @ 1.11400, »

   » X=1.11200, ORDER_TIME_GTC, M=1234567890, G[1.10400]

DONE, #=1300978366, V=0.01, Request executed, Req=20

Середина сетки пришлась на цену 1.10400. Система нумерует запросы в порядке поступления, а их нумерация в массиве соответствует порядку, в котором мы выставляем ордера: от центрального базового уровня постепенно расходимся в стороны. Поэтому не удивляйтесь, что после пары 1 и 2 (для уровня 1.10200) идет 5 и 6 (1.10000) — просто мы отослали 3 и 4 (1.10600) раньше.

В асинхронном режиме перед деструкторами "вклиниваются" сообщения о готовности конкретных запросов, получаемые в AwaitAsync в реальном времени, причем не обязательно в том порядке, в котором запросы были отправлены (например, 49-й и 50-й запросы "обогнали" 47-й и 48-й).

Started in 16 ms
Start setup at 1.10356
Got Req=41 at 109 ms
DONE, #=1300979180, V=0.01, Order placed, Req=41
Got Req=42 at 109 ms
DONE, #=1300979181, V=0.01, Order placed, Req=42
Got Req=43 at 125 ms
DONE, #=1300979182, V=0.01, Order placed, Req=43
Got Req=44 at 140 ms
DONE, #=1300979183, V=0.01, Order placed, Req=44
Got Req=45 at 156 ms
DONE, #=1300979184, V=0.01, Order placed, Req=45
Got Req=46 at 172 ms
DONE, #=1300979185, V=0.01, Order placed, Req=46
Got Req=49 at 172 ms
DONE, #=1300979188, V=0.01, Order placed, Req=49
Got Req=50 at 172 ms
DONE, #=1300979189, V=0.01, Order placed, Req=50
Got Req=47 at 172 ms
DONE, #=1300979186, V=0.01, Order placed, Req=47
Got Req=48 at 172 ms
DONE, #=1300979187, V=0.01, Order placed, Req=48
Got Req=51 at 172 ms
DONE, #=1300979190, V=0.01, Order placed, Req=51
Got Req=52 at 203 ms
DONE, #=1300979191, V=0.01, Order placed, Req=52
Got Req=55 at 203 ms
DONE, #=1300979194, V=0.01, Order placed, Req=55
Got Req=56 at 203 ms
DONE, #=1300979195, V=0.01, Order placed, Req=56
Got Req=53 at 203 ms
DONE, #=1300979192, V=0.01, Order placed, Req=53
Got Req=54 at 203 ms
DONE, #=1300979193, V=0.01, Order placed, Req=54
Got Req=57 at 218 ms
DONE, #=1300979196, V=0.01, Order placed, Req=57
Got Req=58 at 218 ms
DONE, #=1300979198, V=0.01, Order placed, Req=58
Got Req=59 at 218 ms
DONE, #=1300979199, V=0.01, Order placed, Req=59
Got Req=60 at 218 ms
DONE, #=1300979200, V=0.01, Order placed, Req=60
Done 20 requests in 234 ms (11 ms/request)
...

Из-за того, что все запросы выполнялись параллельно, общая длительность отправки (234мс) лишь незначительно больше чем время одного запроса (здесь в районе 100мс, но у вас будет свой тайминг). В результате мы получили скорость 11мс на запрос — в 5 раз быстрее, чем при синхронном способе. Поскольку запросы отправлялись практически одновременно, мы не можем узнать время выполнения каждого, и миллисекунды сообщают приход результата конкретного запроса с момента общего начала групповой отправки.

Дальнейший вывод в лог содержит, как и в предыдущем случае, все поля запросов и результатов, печатаемые из деструкторов структур. Напомним, что строка "Order placed" осталась неизменной после OrderSendAsync, так как наш вспомогательный индикатор TradeTransactionRelay.mq5 не публикует структуру MqlTradeResult из сообщения TRADE_TRANSACTION_REQUEST полностью.

...

TRADE_ACTION_PENDING, EURUSD, ORDER_TYPE_BUY_LIMIT, V=0.01, ORDER_FILLING_FOK, @ 1.10200, »

   » ORDER_TIME_GTC, M=1234567890, G[1.10400]

DONE, #=1300979180, V=0.01, Order placed, Req=41

TRADE_ACTION_PENDING, EURUSD, ORDER_TYPE_SELL_STOP_LIMIT, V=0.01, ORDER_FILLING_FOK, @ 1.10200, »

   » X=1.10400, ORDER_TIME_GTC, M=1234567890, G[1.10400]

DONE, #=1300979181, V=0.01, Order placed, Req=42

TRADE_ACTION_PENDING, EURUSD, ORDER_TYPE_BUY_LIMIT, V=0.01, ORDER_FILLING_FOK, @ 1.10000, »

   » ORDER_TIME_GTC, M=1234567890, G[1.10400]

DONE, #=1300979184, V=0.01, Order placed, Req=45

TRADE_ACTION_PENDING, EURUSD, ORDER_TYPE_SELL_STOP_LIMIT, V=0.01, ORDER_FILLING_FOK, @ 1.10000, »

   » X=1.10200, ORDER_TIME_GTC, M=1234567890, G[1.10400]

DONE, #=1300979185, V=0.01, Order placed, Req=46

TRADE_ACTION_PENDING, EURUSD, ORDER_TYPE_BUY_LIMIT, V=0.01, ORDER_FILLING_FOK, @ 1.09800, »

   » ORDER_TIME_GTC, M=1234567890, G[1.10400]

DONE, #=1300979188, V=0.01, Order placed, Req=49

TRADE_ACTION_PENDING, EURUSD, ORDER_TYPE_SELL_STOP_LIMIT, V=0.01, ORDER_FILLING_FOK, @ 1.09800, »

   » X=1.10000, ORDER_TIME_GTC, M=1234567890, G[1.10400]

DONE, #=1300979189, V=0.01, Order placed, Req=50

...

TRADE_ACTION_PENDING, EURUSD, ORDER_TYPE_SELL_LIMIT, V=0.01, ORDER_FILLING_FOK, @ 1.10600, »

   » ORDER_TIME_GTC, M=1234567890, G[1.10400]

DONE, #=1300979182, V=0.01, Order placed, Req=43

TRADE_ACTION_PENDING, EURUSD, ORDER_TYPE_BUY_STOP_LIMIT, V=0.01, ORDER_FILLING_FOK, @ 1.10600, »

   » X=1.10400, ORDER_TIME_GTC, M=1234567890, G[1.10400]

DONE, #=1300979183, V=0.01, Order placed, Req=44

TRADE_ACTION_PENDING, EURUSD, ORDER_TYPE_SELL_LIMIT, V=0.01, ORDER_FILLING_FOK, @ 1.10800, »

   » ORDER_TIME_GTC, M=1234567890, G[1.10400]

DONE, #=1300979186, V=0.01, Order placed, Req=47

TRADE_ACTION_PENDING, EURUSD, ORDER_TYPE_BUY_STOP_LIMIT, V=0.01, ORDER_FILLING_FOK, @ 1.10800, »

   » X=1.10600, ORDER_TIME_GTC, M=1234567890, G[1.10400]

DONE, #=1300979187, V=0.01, Order placed, Req=48

...

TRADE_ACTION_PENDING, EURUSD, ORDER_TYPE_SELL_LIMIT, V=0.01, ORDER_FILLING_FOK, @ 1.11400, »

   » ORDER_TIME_GTC, M=1234567890, G[1.10400]

DONE, #=1300979199, V=0.01, Order placed, Req=59

TRADE_ACTION_PENDING, EURUSD, ORDER_TYPE_BUY_STOP_LIMIT, V=0.01, ORDER_FILLING_FOK, @ 1.11400, »

   » X=1.11200, ORDER_TIME_GTC, M=1234567890, G[1.10400]

DONE, #=1300979200, V=0.01, Order placed, Req=60

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

В функции SetupGrid потребуется массив структур размером GridSize, а не удвоенный, и количество запросов также уменьшится в 2 раза: для них используются только методы buyLimit и sellLimit.

Функция CheckGrid проверяет целостность сетки по другому принципу. Ранее ошибкой считалось отсутствие парного стоп-лимитного ордера на уровне, где есть лимитный. Такое могло произойти при срабатывании на сервере стоп-лимитного ордера с соседнего уровня. Однако данная схема не способна восстановить сетку, если на одном баре случится сильное двустороннее движение цены (спайк): оно выбьет не только исходные лимитные ордера, но и новые — сгенерированные при этом из стоп-лимитных. Теперь же алгоритм честно проверяет вакантные уровни по обе стороны от текущей цены и создает там лимитные ордера с помощью RepairGridLevel. Эта вспомогательная функция ранее расставляла стоп-лимитные ордера.

Наконец, в PendingOrderGrid4.mq5 появился обработчик OnTradeTransaction. Срабатывание отложенного ордера приведет к заключению сделки (и изменению конфигурации сетки, которую нужно поправить), поэтому мы контролируем сделки по заданному символу и магику. При обнаружении сделки функция CheckGrid вызывается моментально, в дополнение к тому, что она по-прежнему выполняется в начале каждого бара.

void OnTradeTransaction(const MqlTradeTransaction &transaction,
   const MqlTradeRequest &,
   const MqlTradeResult &)
{
   if(transaction.type == TRADE_TRANSACTION_DEAL_ADD)
   {
      if(transaction.symbol == _Symbol)
      {
         DealMonitor dm(transaction.deal); // выделяем сделку
         if(dm.get(DEAL_MAGIC) == Magic)
         {
            CheckGrid();
         }
      }
   }
}

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

Однако помимо события OnTradeTransaction MQL5 предоставляет и другое, более простое событие OnTrade.