Модификация уровней Stop Loss и/или Take Profit позиции

У открытой позиции MQL-программа может менять защитные ценовые уровни Stop Loss и Take Profit. Для этой цели предназначен элемент TRADE_ACTION_SLTP в перечислении ENUM_TRADE_REQUEST_ACTIONS, то есть при заполнении структуры MqlTradeRequest в поле action следует записывать TRADE_ACTION_SLTP.

Это единственное обязательное поле. Необходимость заполнения других полей обуславливается режимом работы счета ENUM_ACCOUNT_MARGIN_MODE. Так на счетах с неттингом обязательно заполнять поле symbol, но можно опустить тикет позиции. На счетах с хеджированием, наоборот, обязательно указывать тикет позиции position, но можно опустить символ. Это объясняется особенностями идентификации позиции на счетах разных типов. Напомним, при неттинге по каждому символу может существовать только одна позиция.

В целях унификации кода рекомендуется заполнять оба поля при наличии информации.

Непосредственно защитные ценовые уровни устанавливаются в полях sl и tp. Допускается задать только sl или только tp. Для удаления защитных уровней следует присвоить им нулевые значения.

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

Поле

Неттинг

Хедж

action

*

*

symbol

*

+

position

+

*

sl

+

+

tp

+

+

В структуре MqlTradeRequestSync для выполнения операции модификации защитных уровней введем несколько перегрузок метода adjust.

struct MqlTradeRequestSyncpublic MqlTradeRequest
{
   ...
   bool adjust(const ulong posconst double stop = 0const double take = 0);
   bool adjust(const string name, const double stop = 0const double take = 0);
   bool adjust(const double stop = 0const double take = 0);
   ...
};

Как мы видели выше, в зависимости от окружения, модификацию можно делать только по тикету или только по символу позиции — эти варианты учтены в первых двух прототипах.

Кроме того, поскольку структура может уже быть использована для предыдущих запросов, в ней могут быть заполнены поля position и symbol. Тогда можно вызывать метод с последним прототипом.

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

private:
   bool _adjust(const ulong posconst string name,
      const double stop = 0const double take = 0)
   {
      action = TRADE_ACTION_SLTP;
      position = pos;
      type = (ENUM_ORDER_TYPE)PositionGetInteger(POSITION_TYPE);
      if(!setSymbol(name)) return false;
      if(!setSLTP(stoptake)) return false;
      ZeroMemory(result);
      return OrderSend(thisresult);
   }

Суть проста — заполняем все поля структуры по вышеприведенным правилам, вызывая ранее описанные методы setSymbol и setSLTP, а затем отправляем запрос на сервер. Результатом является статус успеха (true) или ошибки (false).

Исходные параметры для запроса каждый из перегруженных методов adjust подготавливает по своему. Вот, например, как это сделано при наличии тикета позиции.

public:
   bool adjust(const ulong posconst double stop = 0const double take = 0)
   {
      if(!PositionSelectByTicket(pos))
      {
         Print("No position: P=" + (string)pos);
         return false;
      }
      return _adjust(posPositionGetString(POSITION_SYMBOL), stoptake);
   }

Здесь с помощью встроенной функции PositionSelectByTicket делается проверка наличия позиции и её выделение в торговом окружении терминала, что необходимо для последующего чтения её свойств, в данном случае — символа (PositionGetString(POSITION_SYMBOL)). Затем происходит вызов универсального варианта adjust.

При модификации позиции по имени символа (что доступно только на неттинг-счете) можно использовать другой вариант adjust.

   bool adjust(const string name, const double stop = 0const double take = 0)
   {
      if(!PositionSelect(name))
      {
         Print("No position: " + s);
         return false;
      }
      
      return _adjust(PositionGetInteger(POSITION_TICKET), name, stoptake);
   }

Здесь выбор позиции происходит с помощью встроенной функции PositionSelect, а из её свойств получается номер тикета (PositionGetInteger(POSITION_TICKET)).

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

Вариант метода adjust с самым минималистским набором параметров — только уровнями stop и take — выглядит следующим образом.

   bool adjust(const double stop = 0const double take = 0)
   {
      if(position != 0)
      {
         if(!PositionSelectByTicket(position))
         {
            Print("No position with ticket P=" + (string)position);
            return false;
         }
         const string s = PositionGetString(POSITION_SYMBOL);
         if(symbol != NULL && symbol != s)
         {
            Print("Position symbol is adjusted from " + symbol + " to " + s);
         }
         symbol = s;
      }
      else if(AccountInfoInteger(ACCOUNT_MARGIN_MODE)
         != ACCOUNT_MARGIN_MODE_RETAIL_HEDGING
         && StringLen(symbol) > 0)
      {
         if(!PositionSelect(symbol))
         {
            Print("Can't select position for " + symbol);
            return false;
         }
         position = PositionGetInteger(POSITION_TICKET);
      }
      else
      {
         Print("Neither position ticket nor symbol was provided");
         return false;
      }
      return _adjust(positionsymbolstoptake);
   }

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

Так же как и в случае методов buy/sell, представленный набор методов adjust работает "асинхронно" в том смысле, что по их завершении известен только статус отправки запроса, но нет подтверждения модификации уровней. Напомним, что в связке с биржей уровень Take Profit может выводиться на неё как лимитный ордер. Поэтому в структуре MqlTradeResultSync следует обеспечить "синхронное" ожидание, пока изменения не вступят в силу.

Общий механизм ожидания в виде метода MqlTradeResultSync::wait уже готов и был использован для ожидания открытия позиции. Метод wait получает в качестве первого параметра указатель на другой метод с предопределенным прототипом condition — для опроса в цикле, пока не выполнится требуемое условие или не случится таймаут. Этот condition-совместимый метод должен, в данном случае, выполнять прикладную проверку стоп-уровней в позиции.

Добавим такой новый метод под именем adjusted.

struct MqlTradeResultSyncpublic MqlTradeResult
{
   ...
   bool adjusted(const ulong msc = 1000)
   {
      if(retcode != TRADE_RETCODE_DONE || retcode != TRADE_RETCODE_PLACED)
      {
         return false;
      }
   
      if(!wait(checkSLTPmsc))
      {
         Print("SL/TP modification timeout: P=" + (string)position);
         return false;
      }
      
      return true;
   }

В первую очередь, разумеется, проверяем статус в поле retcode. Если он — один из штатных, продолжаем проверку самих уровней, передавая в wait вспомогательный метод checkSLTP.

struct MqlTradeResultSyncpublic MqlTradeResult
{
   ...
   static bool checkSLTP(MqlTradeResultSync &ref)
   {
      if(PositionSelectByTicket(ref.position))
      {
         return TU::Equal(PositionGetDouble(POSITION_SL), /*.?.*/)
            && TU::Equal(PositionGetDouble(POSITION_TP), /*.?.*/);
      }
      else
      {
         Print("PositionSelectByTicket failed: P=" + (string)ref.position);
      }
      return false;
   }

Данный код гарантирует, что позиция выбрана по тикету в торговом окружении терминала с помощью PositionSelectByTicket и читает свойства позиции POSITION_SL и POSITION_TP, которые надо сравнить с тем, что было в запросе. Проблема в том, что здесь мы не имеем доступа к объекту запроса и должны каким-то образом передать сюда пару значений для мест, помеченных '.?.'.

В принципе, поскольку структура MqlTradeResultSync проектируется нами, мы можем добавить в неё поля sl и tp, и заполнять их значениями из MqlTradeRequestSync перед отправкой запроса (ядро не "знает" о наших добавленных полях и оставит их нетронутыми в процессе вызова OrderSend). Но для простоты мы воспользуемся тем, что уже имеется. Поля bid и ask в структуре MqlTradeResultSync используются только для сообщения цен реквот (статус TRADE_RETCODE_REQUOTE), что не относится к запросу TRADE_ACTION_SLTP, поэтому мы можем сохранить в них значение sl и tp из заполненной MqlTradeRequestSync.

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

struct MqlTradeRequestSyncpublic MqlTradeRequest
{
   ...
   bool completed()
   {
      if(action == TRADE_ACTION_DEAL)
      {
         const bool success = result.opened(timeout);
         if(successposition = result.position;
         return success;
      }
      else if(action == TRADE_ACTION_SLTP)
      {
         // передаем исходные данные запроса для сравнения со свойствами позиции,
         // по-умолчанию их нет в структуре результата
         result.position = position;
         result.bid = sl// поле bid свободно в этом типе результата, используем под StopLoss
         result.ask = tp// поле ask свободно в этом типе результата, используем под TakeProfit
         return result.adjusted(timeout);
      }
      return false;
   }

Как видно, после установки тикета позиции и ценовых уровней из запроса, мы вызываем метод adjusted, рассмотренный выше, в котором происходит проверка: wait(checkSLTP). Теперь мы можем вернуться к вспомогательному методу checkSLTP в структуре MqlTradeResultSync и привести его к окончательному виду.

struct MqlTradeResultSyncpublic MqlTradeResult
{
   ...
   static bool checkSLTP(MqlTradeResultSync &ref)
   {
      if(PositionSelectByTicket(ref.position))
      {
         return TU::Equal(PositionGetDouble(POSITION_SL), ref.bid// sl из запроса
            && TU::Equal(PositionGetDouble(POSITION_TP), ref.ask); // tp из запроса
      }
      else
      {
         Print("PositionSelectByTicket failed: P=" + (string)ref.position);
      }
      return false;
   }

На этом расширение функционала структур MqlTradeRequestSync и MqlTradeResultSync для операции модификации Stop Loss и Take Profit завершено.

С учетом этого продолжим пример эксперта MarketOrderSend.mq5, начатый в предыдущем разделе. Добавим в него входной параметр Distance2SLTP, позволяющий указать расстояние в пунктах до уровней Stop Loss и Take Profit.

input int Distance2SLTP = 0// Distance to SL/TP in points (0 = no)

Когда он равен нулю, защитные уровни не будут ставиться.

В рабочем коде, после получения подтверждения об открытии позиции вычисляем значения уровней в переменных SL и TP, и выполняем синхронную модификацию: request.adjust(SL, TP) && request.completed().

   ...
   const ulong order = (wantToBuy ?
      request.buy(symbolvolumePrice) :
      request.sell(symbolvolumePrice));
   if(order != 0)
   {
      Print("OK Order: #="order);
      if(request.completed()) // ждем открытия позиции
      {
         Print("OK Position: P="request.result.position);
         if(Distance2SLTP != 0)
         {
            // позиция "выбрана" в торговом окружении терминала внутри 'complete',
            // поэтому не требуется делать это явным образом по тикету
            // PositionSelectByTicket(request.result.position);
            
            // при выбранной позиции можно узнать её свойства, а нам нужна цена,
            // чтобы отступить от неё на заданное количество пунктов
            const double price = PositionGetDouble(POSITION_PRICE_OPEN);
            const double point = SymbolInfoDouble(symbolSYMBOL_POINT);
            // отсчет уровней делаем с помощью вспомогательного класса TradeDirection
            TU::TradeDirection dir((ENUM_ORDER_TYPE)Type);
            // SL всегда "хуже", а TP - "лучше" цены: код един для покупки и продажи
            const double SL = dir.negative(priceDistance2SLTP * point);
            const double TP = dir.positive(priceDistance2SLTP * point);
            if(request.adjust(SLTP) && request.completed())
            {
               Print("OK Adjust");
            }
         }
      }
   }
   Print(TU::StringOf(request));
   Print(TU::StringOf(request.result));
}

В первом вызове completed после успешной покупки или продажи тикет позиции сохраняется в поле position структуры запроса. Поэтому для модификации стопов достаточно только ценовых уровней, а символ и тикет позиции уже присутствуют в request.

Попробуем выполнить с помощью эксперта покупку с настройками по умолчанию, но установив Distance2SLTP в 500 пунктов.

OK Order: #=1273913958
Waiting for position for deal D=1256506526
OK Position: P=1273913958
OK Adjust
TRADE_ACTION_SLTP, EURUSD, ORDER_TYPE_BUY, V=0.01, ORDER_FILLING_FOK, @ 1.10889, »
»  SL=1.10389, TP=1.11389, P=1273913958
DONE, Bid=1.10389, Ask=1.11389, Request executed, Req=26

Две последние строки соответствуют отладочному выводу в журнал содержимого структур request и request.result, инициированному в конце функции. В этих строках интересно, что поля хранят в себе симбиоз значений из двух запросов: сначала была открыта позиция, а потом произведена её модификация. В частности, поля с объемом (0.01) и ценой (1.10889) в запросе остались после TRADE_ACTION_DEAL, но не помешали выполнению TRADE_ACTION_SLTP. В принципе, от этого легко избавиться, выполнив обнуление структуры между двумя запросами, однако мы предпочли оставить их как есть, потому что среди заполненных полей есть и полезные: поле position получило тикет, который нам нужен для запроса модификации. Если бы мы обнулили структуру, то нужно было бы вводить переменную для промежуточного хранения тикета.

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

Также не следует удивляться тому, что в структуре с результатом мы видим запрошенные уровни sl и tp в полях под цены Bid и Ask: записал их туда метод MqlTradeRequestSync::completed с целью сравнения с фактическими изменениями позиции. При выполнении запроса ядро системы заполнило в структуре result только retcode (DONE), comment ("Request executed") и request_id (26).

Далее мы рассмотрим другой пример модификации уровней, реализующий "трейлинг стоп".