Установка отложенного ордера

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

  • action
  • symbol
  • volume
  • price
  • type (значение по умолчанию 0 соответствует ORDER_TYPE_BUY)
  • type_filling (значение по умолчанию 0 соответствует ORDER_FILLING_FOK)
  • type_time (значение по умолчанию 0 соответствует ORDER_TIME_GTC)
  • expiration (значение по умолчанию 0, не важно при ORDER_TIME_GTC)

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

Поле stoplimit является обязательным только для ордеров типов ORDER_TYPE_BUY_STOP_LIMIT и ORDER_TYPE_SELL_STOP_LIMIT.

Опциональными полями являются:

  • sl
  • tp
  • magic
  • comment

Нулевые значения в sl и tp обозначают отсутствие защитных уровней.

Дополним наши структуры в файле MqlTradeSync.mqh методами для проверки значений и заполнения полей. Принцип формирования всех типов ордеров одинаков, поэтому рассмотрим пару частных случаев установки лимитных ордеров на покупку и продажу. Остальные типы буду отличаться только значением поля type. Публичные методы с полным набором обязательных полей, а также защитных уровней, называются согласно типам: buyLimit и sellLimit.

   ulong buyLimit(const string nameconst double lotconst double p,
      const double stop = 0const double take = 0,
      ENUM_ORDER_TYPE_TIME duration = ORDER_TIME_GTCdatetime until = 0)
   {
      type = ORDER_TYPE_BUY_LIMIT;
      return _pending(namelotpstoptakedurationuntil);
   }
   
   ulong sellLimit(const string nameconst double lotconst double p,
      const double stop = 0const double take = 0,
      ENUM_ORDER_TYPE_TIME duration = ORDER_TIME_GTCdatetime until = 0)
   {
      type = ORDER_TYPE_SELL_LIMIT;
      return _pending(namelotpstoptakedurationuntil);
   }

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

MqlTradeRequestSync request// по умолчанию использует текущий символ графика
request.buyLimit(volumeprice);

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

   ulong _pending(const string nameconst double lotconst double p,
      const double stop = 0const double take = 0,
      ENUM_ORDER_TYPE_TIME duration = ORDER_TIME_GTCdatetime until = 0,
      const double origin = 0)
   {
      action = TRADE_ACTION_PENDING;
      if(!setSymbol(name)) return 0;
      if(!setVolumePrices(lotpstoptakeorigin)) return 0;
      if(!setExpiration(durationuntil)) return 0;
      if((SymbolInfoInteger(nameSYMBOL_ORDER_MODE) & (1 << (type / 2))) == 0)
      {
         Print(StringFormat("pending orders %s not allowed for %s",
            EnumToString(type), name));
         return 0;
      }
      ZeroMemory(result);
      if(OrderSend(thisresult)) return result.order;
      return 0;
   }

Заполнение поля action и вызов методов setSymbol и setVolumePrices уже знакомы нам по предыдущим торговым операциям.

Многострочный оператор if гарантирует, что готовящаяся операция присутствует среди разрешенных операций по символу, прописанных в свойстве SYMBOL_ORDER_MODE. Целочисленное деление типа type пополам с последующим сдвигом 1 на полученное значение взводит правильный бит в маске разрешенных типов ордеров — это особенность сочетания констант в перечислении ENUM_ORDER_TYPE и свойства SYMBOL_ORDER_MODE. Например, ORDER_TYPE_BUY_STOP и ORDER_TYPE_SELL_STOP имеют значения 4 и 5, которые после деления на 2 дают 2 (с учетом отбрасывания дробной части). Операция 1 << 2 имеет результат 4, равный SYMBOL_ORDER_STOP.

Особенностью отложенных ордеров является обработка срока истечения. Этим занимается метод setExpiration. В нем следует убедиться, что заданный режим истечения ENUM_ORDER_TYPE_TIME duration разрешен для символа, а дата и время until правильно заполнены.

   bool setExpiration(ENUM_ORDER_TYPE_TIME duration = ORDER_TIME_GTCdatetime until = 0)
   {
      const int modes = (int)SymbolInfoInteger(symbolSYMBOL_EXPIRATION_MODE);
      if(((1 << duration) & modes) != 0)
      {
         type_time = duration;
         if((duration == ORDER_TIME_SPECIFIED || duration == ORDER_TIME_SPECIFIED_DAY)
            && until == 0)
         {
            Print(StringFormat("datetime is 0, "
               "but it's required for order expiration mode %s",
               EnumToString(duration)));
            return false;
         }
         if(until > 0 && until <= TimeTradeServer())
         {
            Print(StringFormat("expiration datetime %s is in past, server time is %s",
               TimeToString(until), TimeToString(TimeTradeServer())));
            return false;
         }
         expiration = until;
      }
      else
      {
         Print(StringFormat("order expiration mode %s is not allowed for %s",
            EnumToString(duration), symbol));
         return false;
      }
      return true;
   }

Битовая маска разрешенных режимов доступна в свойстве SYMBOL_EXPIRATION_MODE. Сочетание битов в маске и констант ENUM_ORDER_TYPE_TIME таково, что нам достаточно вычислить выражение 1 << duration и наложить его на маску: ненулевое значение служит признаком наличия режима.

Для режимов ORDER_TIME_SPECIFIED и ORDER_TIME_SPECIFIED_DAY поле expiration с конкретным значением datetime не может быть пустым. Причем указанные дата и время не могут быть в прошлом.

Поскольку метод _pending, представленный ранее, отправляет в конце запрос на сервер с помощью OrderSend, наша программа должна убедиться, что ордер с полученным тикетом действительно был создан (это особенно важно для лимитных ордеров, которые могут выводиться во внешнюю торговую систему). Поэтому в методе completed, который используется для "блокирующего" контроля результата, добавим ветвь для операции TRADE_ACTION_PENDING.

   bool completed()
   {
      // прежний код обработки
      // TRADE_ACTION_DEAL
      // TRADE_ACTION_SLTP
      // TRADE_ACTION_CLOSE_BY
      ...
      else if(action == TRADE_ACTION_PENDING)
      {
         return result.placed(timeout);
      }
      ...
      return false;
   }

В структуре MqlTradeResultSync добавим метод placed.

   bool placed(const ulong msc = 1000)
   {
      if(retcode != TRADE_RETCODE_DONE
         && retcode != TRADE_RETCODE_DONE_PARTIAL)
      {
         return false;
      }
      
      if(!wait(orderExistmsc))
      {
         Print("Waiting for order: #" + (string)order);
         return false;
      }
      return true;
   }

Его главная задача — дождаться появления ордера с помощью ожидания в функции orderExist: она уже использовалась на первом этапе проверки открытия позиции.

Для тестирования нового функционала реализуем эксперт PendingOrderSend.mq5. Он позволяет выбрать с помощью входных переменных тип отложенного ордера и все его атрибуты, а затем выполнить запрос с подтверждением.

enum ENUM_ORDER_TYPE_PENDING
{                                                        // Строки интерфейса UI
   PENDING_BUY_STOP = ORDER_TYPE_BUY_STOP,               // ORDER_TYPE_BUY_STOP
   PENDING_SELL_STOP = ORDER_TYPE_SELL_STOP,             // ORDER_TYPE_SELL_STOP
   PENDING_BUY_LIMIT = ORDER_TYPE_BUY_LIMIT,             // ORDER_TYPE_BUY_LIMIT
   PENDING_SELL_LIMIT = ORDER_TYPE_SELL_LIMIT,           // ORDER_TYPE_SELL_LIMIT
   PENDING_BUY_STOP_LIMIT = ORDER_TYPE_BUY_STOP_LIMIT,   // ORDER_TYPE_BUY_STOP_LIMIT
   PENDING_SELL_STOP_LIMIT = ORDER_TYPE_SELL_STOP_LIMIT// ORDER_TYPE_SELL_STOP_LIMIT
};
 
input string Symbol;             // Symbol (empty = current _Symbol)
input double Volume;             // Volume (0 = minimal lot)
input ENUM_ORDER_TYPE_PENDING Type = PENDING_BUY_STOP;
input int Distance2SLTP = 0;     // Distance to SL/TP in points (0 = no)
input ENUM_ORDER_TYPE_TIME Expiration = ORDER_TIME_GTC;
input datetime Until = 0;
input ulong Magic = 1234567890;
input string Comment;

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

Однократная установка ордера выполняется, как и в некоторых предыдущих примерах, по таймеру (поэтому следует предварительно убедиться, что рынок открыт).

void OnTimer()
{
   // один раз выполняем и ждем изменений настроек пользователем
   EventKillTimer();
   
   const string symbol = StringLen(Symbol) == 0 ? _Symbol : Symbol;
   if(PlaceOrder((ENUM_ORDER_TYPE)TypesymbolVolume,
      Distance2SLTPExpirationUntilMagicComment))
   {
      Alert("Pending order placed - remove it manually, please");
   }
}

Функция PlaceOrder принимает все настройки в качестве параметров, отправляет запрос и возвращает признак успеха (ненулевой тикет). Для ордеров всех поддерживаемых типов заранее предустановлены расстояния от текущей цены, рассчитываемые как часть дневного размаха котировок.

ulong PlaceOrder(const ENUM_ORDER_TYPE type,
   const string symbolconst double lot,
   const int sltpENUM_ORDER_TYPE_TIME expirationdatetime until,
   const ulong magic = 0const string comment = NULL)
{
   static double coefficients[] = // индексируется типом ордера
   {
      0  ,   // ORDER_TYPE_BUY - не используется
      0  ,   // ORDER_TYPE_SELL - не используется
     -0.5,   // ORDER_TYPE_BUY_LIMIT - слегка под ценой
     +0.5,   // ORDER_TYPE_SELL_LIMIT - слегка над ценой
     +1.0,   // ORDER_TYPE_BUY_STOP - далеко над ценой
     -1.0,   // ORDER_TYPE_SELL_STOP - далеко под ценой
     +0.7,   // ORDER_TYPE_BUY_STOP_LIMIT - средне над ценой 
     -0.7,   // ORDER_TYPE_SELL_STOP_LIMIT - средне под ценой
      0  ,   // ORDER_TYPE_CLOSE_BY - не используется
   };
   ...

Например, коэффициент -0.5 для ORDER_TYPE_BUY_LIMIT означает, что ордер будет поставлен ниже текущей цены на половину дневного размаха (отбой внутрь диапазона), а +1.0 для ORDER_TYPE_BUY_STOP — что ордер окажется на верхней границе диапазона (на пробой).

Сам дневной размах вычисляется следующим образом.

   const double range = iHigh(symbolPERIOD_D11) - iLow(symbolPERIOD_D11);
   Print("Autodetected daily range: ", (float)range);
   ...

Находим значения объема и пункта, которые потребуются ниже.

   const double volume = lot == 0 ? SymbolInfoDouble(symbolSYMBOL_VOLUME_MIN) : lot;
   const double point = SymbolInfoDouble(symbolSYMBOL_POINT);

Ценовой уровень установки ордера рассчитываем в переменной price по приведенным коэффициентам от общего диапазона.

   const double price = TU::GetCurrentPrice(typesymbol) + range * coefficients[type];

Поле stoplimit должно заполняться только для ордеров *_STOP_LIMIT. Значения для него хранит переменная origin.

   const bool stopLimit =
      type == ORDER_TYPE_BUY_STOP_LIMIT ||
      type == ORDER_TYPE_SELL_STOP_LIMIT;
   const double origin = stopLimit ? TU::GetCurrentPrice(typesymbol) : 0;

При срабатывании ордеров этих двух типов новый отложенный ордер будет ставиться на цену, являющуюся сейчас текущей. Действительно, в данном сценарии цена движется от текущего значения до уровня price, где и происходит активация ордера, а потому "прежняя текущая" цена становится корректным уровнем для отскока, обозначенным лимитным ордером. Ниже мы проиллюстрируем эту ситуацию.

Защитные уровни определяются с привлечением объекта TU::TradeDirection, причем в случае стоп-лимитных ордеров отсчет ведется от origin.

   TU::TradeDirection dir(type);
   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 request(symbol);
   
   request.magic = magic;
   request.comment = comment;
   // request.type_filling = SYMBOL_FILLING_FOK;

Здесь же можно выбрать режим заливки. По умолчанию, MqlTradeRequestSync автоматически выбирает первый из разрешенных режимов ENUM_ORDER_TYPE_FILLING.

В зависимости от выбранного пользователем типа ордера, вызываем тот или иной торговый метод.

   ResetLastError();
   // заполняем и проверяем нужные поля, отправляем запрос
   ulong order = 0;
   switch(type)
   {
   case ORDER_TYPE_BUY_STOP:
      order = request.buyStop(volumepricestoptakeexpirationuntil);
      break;
   case ORDER_TYPE_SELL_STOP:
      order = request.sellStop(volumepricestoptakeexpirationuntil);
      break;
   case ORDER_TYPE_BUY_LIMIT:
      order = request.buyLimit(volumepricestoptakeexpirationuntil);
      break;
   case ORDER_TYPE_SELL_LIMIT:
      order = request.sellLimit(volumepricestoptakeexpirationuntil);
      break;
   case ORDER_TYPE_BUY_STOP_LIMIT:
      order = request.buyStopLimit(volumepriceoriginstoptakeexpirationuntil);
      break;
   case ORDER_TYPE_SELL_STOP_LIMIT:
      order = request.sellStopLimit(volumepriceoriginstoptakeexpirationuntil);
      break;
   }
   ...

Если тикет получен, дожидаемся его появления в торговом окружении терминала.

   if(order != 0)
   {
      Print("OK order sent: #="order);
      if(request.completed()) // ожидаем результат (подтверждение ордера)
      {
         Print("OK order placed");
      }
   }
   Print(TU::StringOf(request));
   Print(TU::StringOf(request.result));
   return order;
}

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

Autodetected daily range: 0.01413
OK order sent: #=1282106395
OK order placed
TRADE_ACTION_PENDING, EURUSD, ORDER_TYPE_BUY_STOP, V=0.01, ORDER_FILLING_FOK, »
  » @ 1.11248, SL=1.10248, TP=1.12248, ORDER_TIME_GTC, M=1234567890
DONE, #=1282106395, V=0.01, Request executed, Req=91
Alert: Pending order placed - remove it manually, please

На графике это выглядит так.

Отложенный ордер ORDER_TYPE_BUY_STOP

Отложенный ордер ORDER_TYPE_BUY_STOP

Удалим ордер вручную и поменяем тип ордера на ORDER_TYPE_BUY_STOP_LIMIT. В результате получим более сложную картину.

Отложенный ордер ORDER_TYPE_BUY_STOP_LIMIT

Отложенный ордер ORDER_TYPE_BUY_STOP_LIMIT

Цена, где расположена верхняя пара штрих-пунктирных линий, является ценой срабатывания ордера, в результате чего будет поставлен ORDER_TYPE_BUY_LIMIT ордер на уровне текущей цены, со значениями Stop Loss и Take Profit, помеченными красными линиями. Уровень Take Profit будущего ордера ORDER_TYPE_BUY_LIMIT практически совпадает с уровнем активации только что созданного предварительного ордера ORDER_TYPE_BUY_STOP_LIMIT.

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

Отложенные ордера всех типов

Отложенные ордера всех типов

В результате его запуска с настройками по умолчанию вы можете получить такие записи в журнале:

Autodetected daily range: 0.01413
OK order placed: #=1282032135
TRADE_ACTION_PENDING, EURUSD, ORDER_TYPE_BUY_LIMIT, V=0.01, ORDER_FILLING_FOK, »
  » @ 1.08824, ORDER_TIME_GTC, M=1234567890
DONE, #=1282032135, V=0.01, Request executed, Req=73
OK order placed: #=1282032136
TRADE_ACTION_PENDING, EURUSD, ORDER_TYPE_SELL_LIMIT, V=0.01, ORDER_FILLING_FOK, »
  » @ 1.10238, ORDER_TIME_GTC, M=1234567890
DONE, #=1282032136, V=0.01, Request executed, Req=74
OK order placed: #=1282032138
TRADE_ACTION_PENDING, EURUSD, ORDER_TYPE_BUY_STOP, V=0.01, ORDER_FILLING_FOK, »
  » @ 1.10944, ORDER_TIME_GTC, M=1234567890
DONE, #=1282032138, V=0.01, Request executed, Req=75
OK order placed: #=1282032141
TRADE_ACTION_PENDING, EURUSD, ORDER_TYPE_SELL_STOP, V=0.01, ORDER_FILLING_FOK, »
  » @ 1.08118, ORDER_TIME_GTC, M=1234567890
DONE, #=1282032141, V=0.01, Request executed, Req=76
OK order placed: #=1282032142
TRADE_ACTION_PENDING, EURUSD, ORDER_TYPE_BUY_STOP_LIMIT, V=0.01, ORDER_FILLING_FOK, »
  » @ 1.10520, X=1.09531, ORDER_TIME_GTC, M=1234567890
DONE, #=1282032142, V=0.01, Request executed, Req=77
OK order placed: #=1282032144
TRADE_ACTION_PENDING, EURUSD, ORDER_TYPE_SELL_STOP_LIMIT, V=0.01, ORDER_FILLING_FOK, »
  » @ 1.08542, X=1.09531, ORDER_TIME_GTC, M=1234567890
DONE, #=1282032144, V=0.01, Request executed, Req=78
Alert: 6 pending orders placed - remove them manually, please