Модификация отложенного ордера

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

Программная модификация ордеров выполняется операцией TRADE_ACTION_MODIFY: именно эту константу нужно записать в поле action структуры MqlTradeRequest перед отправкой на сервер функцией OrderSend или OrderSendAsync. Тикет модифицируемого ордера указывается в поле order. С учетом action и order, полный перечень обязательных полей для данной операции включает:

  • action
  • order
  • price
  • type_time (значение по умолчанию 0 соответствует ORDER_TIME_GTC)
  • expiration (значение по умолчанию 0, не важно при ORDER_TIME_GTC)
  • type_filling (значение по умолчанию 0 соответствует ORDER_FILLING_FOK)
  • stoplimit (только для ордеров типов ORDER_TYPE_BUY_STOP_LIMIT и ORDER_TYPE_SELL_STOP_LIMIT)

Опциональные поля:

  • sl
  • tp

Если защитные уровни уже были установлены у ордера, их следует указывать, чтобы сохранить. Нулевые значения предписывают удаление Stop Loss и/или Take Profit.

В структуре MqlTradeRequestSync (MqlTradeSync.mqh) реализация модификации ордера находится в методе modify.

struct MqlTradeRequestSyncpublic MqlTradeRequest
{
   ...
   bool modify(const ulong ticket,
      const double pconst double stop = 0const double take = 0,
      ENUM_ORDER_TYPE_TIME duration = ORDER_TIME_GTCdatetime until = 0,
      const double origin = 0)
   {
      if(!OrderSelect(ticket)) return false;
      
      action = TRADE_ACTION_MODIFY;
      order = ticket;
      
      // следующие поля нужны для проверок внутри подфункций
      type = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE);
      symbol = OrderGetString(ORDER_SYMBOL);
      volume = OrderGetDouble(ORDER_VOLUME_CURRENT);
      
      if(!setVolumePrices(volumepstoptakeorigin)) return false;
      if(!setExpiration(durationuntil)) return false;
      ZeroMemory(result);
      return OrderSend(thisresult);
   }

Фактическое исполнение запроса, как обычно, производится в методе completed, в выделенной ветке оператора if.

   bool completed()
   {
      ...
      else if(action == TRADE_ACTION_MODIFY)
      {
         result.order = order;
         result.bid = sl;
         result.ask = tp;
         result.price = price;
         result.volume = stoplimit;
         return result.modified(timeout);
      }
      ...
   }

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

struct MqlTradeResultSyncpublic MqlTradeResult
{
   ...
   bool modified(const ulong msc = 1000)
   {
      if(retcode != TRADE_RETCODE_DONE && retcode != TRADE_RETCODE_PLACED)
      {
         return false;
      }
   
      if(!wait(orderModifiedmsc))
      {
         Print("Order not found in environment: #" + (string)order);
         return false;
      }
      return true;
   }
   
   static bool orderModified(MqlTradeResultSync &ref)
   {
      if(!(OrderSelect(ref.order) || HistoryOrderSelect(ref.order)))
      {
         Print("OrderSelect failed: #=" + (string)ref.order);
         return false;
      }
      return TU::Equal(ref.bidOrderGetDouble(ORDER_SL))
         && TU::Equal(ref.askOrderGetDouble(ORDER_TP))
         && TU::Equal(ref.priceOrderGetDouble(ORDER_PRICE_OPEN))
         && TU::Equal(ref.volumeOrderGetDouble(ORDER_PRICE_STOPLIMIT));
   }

Здесь мы видим, как свойства ордера считываются при помощи функции OrderGetDouble и сравниваются с установленными значениями. Все это происходит по уже привычной схеме: в цикле внутри функции wait, в пределах некоего таймаута msc (1000 миллисекунд по умолчанию).

В качестве примера разберем эксперт PendingOrderModify.mq5, наследующий от PendingOrderSend.mq5 некоторые фрагменты кода: в частности, набор входных параметров и функцию PlaceOrder для создания нового ордера. Она используется при первом запуске, если ордера для заданного сочетания символа и Magic-числа еще нет, тем самым гарантируя, что эксперту есть что модифицировать.

Для поиска подходящего ордера потребовалась новая функция GetMyOrder. Она во многом похожа на функцию GetMyPosition, которая применялась в примере сопровождения позиции (TrailingStop.mq5) для поиска подходящей позиции. Назначение используемых внутри GetMyOrder встроенных функций MQL5 API должно быть в общих чертах ясно по их названиям, а техническое описание будет представлено в отдельных разделах.

ulong GetMyOrder(const string nameconst ulong magic)
{
   for(int i = 0i < OrdersTotal(); ++i)
   {
      ulong t = OrderGetTicket(i);
      if(OrderGetInteger(ORDER_MAGIC) == magic
         && OrderGetString(ORDER_SYMBOL) == name)
      {
         return t;
      }
   }
   
   return 0;
}

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

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

Основная логика эксперта "зашита" в обработчике OnTick.

void OnTick()
{
   static datetime lastDay = 0;
   static const uint DAYLONG = 60 * 60 * 24// количество секунд в сутках
   // отбрасываем "дробную" часть, т.е. время
   if(TimeTradeServer() / DAYLONG * DAYLONG == lastDayreturn;
   ...

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

Далее делается расчет ценового диапазона за предыдущий день.

   const string symbol = StringLen(Symbol) == 0 ? _Symbol : Symbol;
   const double range = iHigh(symbolPERIOD_D11) - iLow(symbolPERIOD_D11);
   Print("Autodetected daily range: ", (float)range);
   ...

В зависимости от того, найдется ли ордер или нет в функции GetMyOrder, мы либо создадим новый с помощью PlaceOrder, либо отредактируем имеющийся с помощью ModifyOrder.

   uint retcode = 0;
   ulong ticket = GetMyOrder(symbolMagic);
   if(!ticket)
   {
      retcode = PlaceOrder((ENUM_ORDER_TYPE)TypesymbolVolume,
         rangeExpirationUntilMagic);
   }
   else
   {
      retcode = ModifyOrder(ticketrangeExpirationUntil);
   }
   ...

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

  • Обновить переменную lastDay, если запрос успешный (ордер был обновлен и до начала следующего дня эксперт в "спячке");
  • Оставить пока в lastDay "старый" день, чтобы повторить попытку на следующих тиках, если есть временные проблемы (например, торговая сессия еще не началась);
  • Остановить эксперт, если обнаружены серьезные проблемы (например, выбранный тип ордера или направление торговли не разрешены на символе).

   ...
   if(/* какой-то анализ retcode */)
   {
      lastDay = TimeTradeServer() / DAYLONG * DAYLONG;
   }
}

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

Исходный код функции PlaceOrder практически не изменился по сравнению с предыдущим примером, а ModifyOrder приведена ниже.

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

uint ModifyOrder(const ulong ticketconst double range,
   ENUM_ORDER_TYPE_TIME expirationdatetime until)
{
   // значения по умолчанию
   const string symbol = OrderGetString(ORDER_SYMBOL);
   const double point = SymbolInfoDouble(symbolSYMBOL_POINT);
   ...

Ценовые уровни рассчитываются в зависимости от типа ордера и переданного диапазона range.

   const ENUM_ORDER_TYPE type = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE);
   const double price = TU::GetCurrentPrice(typesymbol) + range * Coefficients[type];
   
   // origin заполняется только для ордеров *_STOP_LIMIT
   const bool stopLimit =
      type == ORDER_TYPE_BUY_STOP_LIMIT ||
      type == ORDER_TYPE_SELL_STOP_LIMIT;
   const double origin = stopLimit ? TU::GetCurrentPrice(typesymbol) : 0
   
   TU::TradeDirection dir(type);
   const int sltp = (int)(range / 2 / point);
   const double stop = sltp == 0 ? 0 :
      dir.negative(stopLimit ? origin : pricesltp * point);
   const double take = sltp == 0 ? 0 :
      dir.positive(stopLimit ? origin : pricesltp * point);
   ...

После вычисления всех значений создаем объект структуры MqlTradeRequestSync и выполняем запрос.

   MqlTradeRequestSync request(symbol);
   
   ResetLastError();
   // передаем данные для полей, отсылаем приказ и ждем результата
   if(request.modify(ticketpricestoptakeexpirationuntilorigin)
      && request.completed())
   {
      Print("OK order modified: #="ticket);
   }
   
   Print(TU::StringOf(request));
   Print(TU::StringOf(request.result));
   return request.result.retcode;
}

Для анализа retcode, который мы должны выполнить в вызывающем блоке внутри OnTick, был разработан новый механизм, дополнивший файл TradeRetcode.mqh. Все коды возврата сервера поделены на несколько групп "важности", описываемых элементами перечисления TRADE_RETCODE_SEVERITY.

enum TRADE_RETCODE_SEVERITY
{
   SEVERITY_UNDEFINED,   // что-то нестандартное - просто выведем в журнал
   SEVERITY_NORMAL,      // нормальное функционирование
   SEVERITY_RETRY,       // попробовать снова (вероятно, несколько раз), обновить окружение/цены
   SEVERITY_TRY_LATER,   // следует подождать и попробовать еще
   SEVERITY_REJECT,      // запрос отклонен, вероятно(!) можно попробовать еще раз
                         // 
   SEVERITY_INVALID,     // требуется исправить запрос
   SEVERITY_LIMITS,      // требуется проверить ограничения и исправить запрос
   SEVERITY_PERMISSIONS// требуется уведомить пользователя и изменить настройки программы/терминала
   SEVERITY_ERROR,       // остановка, выводим информацию в журнал и пользователю
};

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

Деление всех кодов по группам выполняет функция TradeCodeSeverity (приводится с сокращениями).

TRADE_RETCODE_SEVERITY TradeCodeSeverity(const uint retcode)
{
   static const TRADE_RETCODE_SEVERITY severities[] =
   {
      ...
      SEVERITY_RETRY,       // REQUOTE (10004)
      SEVERITY_UNDEFINED,     
      SEVERITY_REJECT,      // REJECT (10006)
      SEVERITY_NORMAL,      // CANCEL (10007)
      SEVERITY_NORMAL,      // PLACED (10008)
      SEVERITY_NORMAL,      // DONE (10009)
      SEVERITY_NORMAL,      // DONE_PARTIAL (10010)
      SEVERITY_ERROR,       // ERROR (10011)
      SEVERITY_RETRY,       // TIMEOUT (10012)
      SEVERITY_INVALID,     // INVALID (10013)
      SEVERITY_INVALID,     // INVALID_VOLUME (10014)
      SEVERITY_INVALID,     // INVALID_PRICE (10015)
      SEVERITY_INVALID,     // INVALID_STOPS (10016)
      SEVERITY_PERMISSIONS// TRADE_DISABLED (10017)
      SEVERITY_TRY_LATER,   // MARKET_CLOSED (10018)
      SEVERITY_LIMITS,      // NO_MONEY (10019)
      ...
   };
   
   if(retcode == 0return SEVERITY_NORMAL;
   if(retcode < 10000 || retcode > HEDGE_PROHIBITEDreturn SEVERITY_UNDEFINED;
   return severities[retcode - 10000];
}

Благодаря этому функционалу обработчик OnTick может быть дополнен "умной" обработкой ошибок. В статической переменной RetryFrequency хранится периодичность, с которой программа будет пытаться повторить запрос в случае некритических ошибок. Последний раз, когда делалась такая попытка, запоминается в переменной RetryRecordTime.

void OnTick()
{
   ...
   const static int DEFAULT_RETRY_TIMEOUT = 1// секунды
   static int RetryFrequency = DEFAULT_RETRY_TIMEOUT;
   static datetime RetryRecordTime = 0;
   if(TimeTradeServer() - RetryRecordTime < RetryFrequencyreturn;
   ...

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

   const TRADE_RETCODE_SEVERITY severity = TradeCodeSeverity(retcode);
   if(severity >= SEVERITY_INVALID)
   {
      Alert("Can't place/modify pending order, EA is stopped");
      RetryFrequency = INT_MAX;
   }
   else if(severity >= SEVERITY_RETRY)
   {
      RetryFrequency += (int)sqrt(RetryFrequency + 1);
      RetryRecordTime = TimeTradeServer();
      PrintFormat("Problems detected, waiting for better conditions "
         "(timeout enlarged to %d seconds)",
         RetryFrequency);
   }
   else
   {
      if(RetryFrequency > DEFAULT_RETRY_TIMEOUT)
      {
         RetryFrequency = DEFAULT_RETRY_TIMEOUT;
         PrintFormat("Timeout restored to %d second"RetryFrequency);
      }
      lastDay = TimeTradeServer() / DAYLONG * DAYLONG;
   }

В случае повторных проблем, которые отнесены к классу разрешимых, таймаут RetryFrequency постепенно увеличивается с каждой следующей ошибкой, но сбрасывается в 1 секунду при успешной обработке запроса.

Следует отметить, что методы прикладной структуры MqlTradeRequestSync проверяют большое количество сочетаний параметров на корректность и при обнаружении проблем прерывают процесс, не доводя до вызова SendRequest. Такое поведение заложено по умолчанию, но его можно отключить, определив пустой макрос RETURN(X) перед директивой #include с MqlTradeSync.mqh.

#define RETURN(X)
#include <MQL5Book/MqlTradeSync.mqh>

При таком макроопределении проверки станут только выводить предупреждения в журнал, но продолжат выполнение методов вплоть до вызова SendRequest.

В любом случае, после вызова того или иного метода структуры MqlTradeResultSync в поле retcode будет помещен код ошибки: либо сервером, либо проверяющими алгоритмами структуры MqlTradeRequestSync (здесь пришелся кстати тот факт, что экземпляр MqlTradeResultSync включен внутрь MqlTradeRequestSync). Запись кодов ошибок и использование макроса RETURN в методах MqlTradeRequestSync были для краткости опущены во фрагментах методов, приведенных в книге. Желающие могут ознакомиться с полным исходным кодом в файле MqlTradeSync.mqh.

Запустим в тестере, в визуальном режиме, эксперт PendingOrderModify.mq5 на XAUUSD,H1 (в режиме всех тиков или реальных тиков). С настройками по умолчанию эксперт будет ставить ордера типа ORDER_TYPE_BUY_STOP минимальным лотом. Убедимся по журналу и торговой истории, что программа выставляет отложенные ордера и модифицирует их в начале каждого дня.

2022.01.03 01:05:00   Autodetected daily range: 14.37

2022.01.03 01:05:00   buy stop 0.01 XAUUSD at 1845.73 sl: 1838.55 tp: 1852.91 (1830.63 / 1831.36)

2022.01.03 01:05:00   OK order placed: #=2

2022.01.03 01:05:00   TRADE_ACTION_PENDING, XAUUSD, ORDER_TYPE_BUY_STOP, V=0.01, ORDER_FILLING_FOK, »

  » @ 1845.73, SL=1838.55, TP=1852.91, ORDER_TIME_GTC, M=1234567890

2022.01.03 01:05:00   DONE, #=2, V=0.01, Bid=1830.63, Ask=1831.36, Request executed

2022.01.04 01:05:00   Autodetected daily range: 33.5

2022.01.04 01:05:00   order modified [#2 buy stop 0.01 XAUUSD at 1836.56]

2022.01.04 01:05:00   OK order modified: #=2

2022.01.04 01:05:00   TRADE_ACTION_MODIFY, XAUUSD, ORDER_TYPE_BUY_STOP, V=0.01, ORDER_FILLING_FOK, » 

  » @ 1836.56, SL=1819.81, TP=1853.31, ORDER_TIME_GTC, #=2

2022.01.04 01:05:00   DONE, #=2, @ 1836.56, Bid=1819.81, Ask=1853.31, Request executed, Req=1

2022.01.05 01:05:00   Autodetected daily range: 18.23

2022.01.05 01:05:00   order modified [#2 buy stop 0.01 XAUUSD at 1832.56]

2022.01.05 01:05:00   OK order modified: #=2

2022.01.05 01:05:00   TRADE_ACTION_MODIFY, XAUUSD, ORDER_TYPE_BUY_STOP, V=0.01, ORDER_FILLING_FOK, »

  » @ 1832.56, SL=1823.45, TP=1841.67, ORDER_TIME_GTC, #=2

2022.01.05 01:05:00   DONE, #=2, @ 1832.56, Bid=1823.45, Ask=1841.67, Request executed, Req=2

...

2022.01.11 01:05:00   Autodetected daily range: 11.96

2022.01.11 01:05:00   order modified [#2 buy stop 0.01 XAUUSD at 1812.91]

2022.01.11 01:05:00   OK order modified: #=2

2022.01.11 01:05:00   TRADE_ACTION_MODIFY, XAUUSD, ORDER_TYPE_BUY_STOP, V=0.01, ORDER_FILLING_FOK, »

  » @ 1812.91, SL=1806.93, TP=1818.89, ORDER_TIME_GTC, #=2

2022.01.11 01:05:00   DONE, #=2, @ 1812.91, Bid=1806.93, Ask=1818.89, Request executed, Req=6

2022.01.11 18:10:58   order [#2 buy stop 0.01 XAUUSD at 1812.91] triggered

2022.01.11 18:10:58   deal #2 buy 0.01 XAUUSD at 1812.91 done (based on order #2)

2022.01.11 18:10:58   deal performed [#2 buy 0.01 XAUUSD at 1812.91]

2022.01.11 18:10:58   order performed buy 0.01 at 1812.91 [#2 buy stop 0.01 XAUUSD at 1812.91]

2022.01.11 20:28:59   take profit triggered #2 buy 0.01 XAUUSD 1812.91 sl: 1806.93 tp: 1818.89 »

  » [#3 sell 0.01 XAUUSD at 1818.89]

2022.01.11 20:28:59   deal #3 sell 0.01 XAUUSD at 1818.91 done (based on order #3)

2022.01.11 20:28:59   deal performed [#3 sell 0.01 XAUUSD at 1818.91]

2022.01.11 20:28:59   order performed sell 0.01 at 1818.91 [#3 sell 0.01 XAUUSD at 1818.89]

2022.01.12 01:05:00   Autodetected daily range: 23.28

2022.01.12 01:05:00   buy stop 0.01 XAUUSD at 1843.77 sl: 1832.14 tp: 1855.40 (1820.14 / 1820.49)

2022.01.12 01:05:00   OK order placed: #=4

2022.01.12 01:05:00   TRADE_ACTION_PENDING, XAUUSD, ORDER_TYPE_BUY_STOP, V=0.01, ORDER_FILLING_FOK, »

  » @ 1843.77, SL=1832.14, TP=1855.40, ORDER_TIME_GTC, M=1234567890

2022.01.12 01:05:00   DONE, #=4, V=0.01, Bid=1820.14, Ask=1820.49, Request executed, Req=7

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

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

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

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

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

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

Autodetected daily range: 34.48
TRADE_ACTION_PENDING, XAUUSD, ORDER_TYPE_BUY_STOP, V=0.01, ORDER_FILLING_FOK, »
  » @ 1975.73, SL=1958.49, TP=1992.97, ORDER_TIME_GTC, M=1234567890
CLIENT_DISABLES_AT, AutoTrading disabled by client
Alert: Can't place/modify pending order, EA is stopped

Данная ошибка относится к критическим, и эксперт останавливает работу.

Для демонстрации одной из более легких ошибок можно было бы использовать обработчик OnTimer, а не OnTick. Тогда запуск этого же эксперта на символах, где торговые сессии занимают лишь часть суток, мог бы периодически генерировать последовательность некритических ошибок о закрытом рынке ("Market closed"). В этом случае эксперт будет продолжать попытки начать торговлю, постоянно увеличивая время ожидания.

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

Настройка неторговых периодов в тестере

Настройка неторговых периодов в тестере

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

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