English 中文 Español Deutsch 日本語 Português
Реализация Take Profit в виде лимитных ордеров без изменения оригинального кода советника

Реализация Take Profit в виде лимитных ордеров без изменения оригинального кода советника

MetaTrader 5Примеры | 11 октября 2018, 10:54
28 354 36
Dmitriy Gizlyk
Dmitriy Gizlyk

Содержание

Введение

На различных форумах пользователи критикуют MetaTrader 5 за его рыночное исполнение уровней тейк-профита. Такие посты можно найти и на форуме этого сайта. Пользователи пишут об отрицательном влиянии проскальзывания на финансовый результат операции при исполнении тейк-профита. В качестве альтернативы предлагается использовать лимитные ордера для замены стандартного тейк-профита.

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

1. Проблематика вопроса

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

Перед построением самой системы лимитных ордеров давайте рассмотрим теоретические аспекты, которые нам предстоит решить в ее алгоритме.

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

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

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

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

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

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

Еще один момент касается изменения ранее выставленного тейк-профита. Часто при использовании различных стратегий приходится отслеживать и корректировать тейк-профит открытой позиции. Для нас здесь два варианта развития событий.

  1. Если мы вносим изменения в код такого советника, то, дабы не искать все возможные варианты изменения тейк-профита в коде, мы просто подменяем вызов функции OrderSend на вызов метода нашего класса, где уже и проверяем наличие выставленного ранее лимитного ордера и соответствие его новому уровню. В случае необходимости изменяем ранее выставленный ордер. Или игнорируем команду, если ранее выставленный лимитный ордер соответствует новым требованиям.
  2. Мы используем купленный советник и у нас нет доступа к его коду; наша программа не открывает позицию, а только осуществляет подмену тейк-профита. В таком случае, велика вероятность появления тейк-профита у позиции, к которой мы уже ранее выставили лимитные ордера. И тогда мы должны перепроверить существующие лимитные ордера на актуальность и скорректировать их. При этом обнулить поле тейк-профита позиции.

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

2. Принципы построения связи позиция — лимитный ордер

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

Потенциально, изменение позиции может произойти в любой момент, когда открыта торговая сессия. Но фактически, это происходит не так уж и часто, а проверка на каждом тике значительно увеличит выполняемые советником операции. И тут нам на помощь приходят события. В документации MQL5 мы находим "событие Trade генерируется при завершении торговой операции на торговом сервере". В результате этого события запускается функция OnTrade. Следовательно, запускать функцию проверки соответствия открытых позиций и выставленных лимитных тейк-профитов мы можем из этой функции. Это позволит нам не проверять соответствие на каждом тике и, в тоже время, не пропустить никаких изменений.

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

3. Создаем класс лимитного тейк-профита

Подытожим вышесказанное. Функциональность нашего класса можно разделить на 2 логических процесса:

  1. Внесение изменений в отправку торговых приказов на сервер.
  2. Мониторинг и корректировка открытых позиций и выставленных лимитных ордеров.

Для удобства использования оформим наш алгоритм в класс CLimitTakeProfit и сделаем в нем все функции статичными, что позволит использовать методы класса без объявления его экземпляра в коде программы.

class CLimitTakeProfit : public CObject
  {
private:
   static CSymbolInfo       c_Symbol;
   static CArrayLong        i_TakeProfit; //fixed take profit
   static CArrayDouble      d_TakeProfit; //percent to close at take profit
   
public:
                     CLimitTakeProfit();
                    ~CLimitTakeProfit();
//---
   static void       Magic(int value)  {  i_Magic=value; }
   static int        Magic(void)       {  return i_Magic;}
//---
   static void       OnlyOneSymbol(bool value)  {  b_OnlyOneSymbol=value;  }
   static bool       OnlyOneSymbol(void)        {  return b_OnlyOneSymbol; }
//---
   static bool       OrderSend(const MqlTradeRequest &request, MqlTradeResult &result);
   static bool       OnTrade(void);
   static bool       AddTakeProfit(uint point, double percent);
   static bool       DeleteTakeProfit(uint point);
   
protected:
   static int        i_Magic;          //Magic number to control
   static bool       b_OnlyOneSymbol;  //Only position of one symbol under control
//---
   static bool       SetTakeProfits(ulong position_ticket, double new_tp=0);
   static bool       SetTakeProfits(string symbol, double new_tp=0);
   static bool       CheckLimitOrder(MqlTradeRequest &request);
   static void       CheckLimitOrder(void);
   static bool       CheckOrderInHistory(ulong position_id, string comment, ENUM_ORDER_TYPE type, double &volume, ulong call_position=0);
   static double     GetLimitOrderPriceByComment(string comment);
  };

Методы Magic, OnlyOneSymbol, AddTakeProfit и DeleteTakeProfit являются методами настройки работы класса. Magic — указывает по каким меджик номерам отслеживать позиции (применимо к хедж-счетам), при указании "-1" класс будет работать со всеми позициями. OnlyOneSymbol дает указанию классу работать только с позициями инструмента графика, на котором запущен советник. Методы AddTakeProfit и DeteleTakeProfit предназначены для добавления и удаления уровней фиксированного тейк-профита с указанием закрываемого объема в процентах к начальному объему позиции.

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

3.1. Блок внесения изменений в отправку торговых приказов

За мониторинг отправляемых советником ордеров отвечает метод OrderSend. Я не случайно сделал название и форму вызова данного метода аналогичными стандартной функции отправки ордеров в MQL5. Такой подход облегчит нам встраивание нашего алгоритма в код ранее написанного советника, подменив стандартную функцию нашим методом.

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

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

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

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

bool CLimitTakeProfit::OrderSend(MqlTradeRequest &request,MqlTradeResult &result)
  {
   if((b_OnlyOneSymbol && request.symbol!=_Symbol) ||
      (i_Magic>=0 && request.magic!=i_Magic) || !(request.action==TRADE_ACTION_SLTP && request.tp>0))
      return(::OrderSend(request,result));
//---
   if(((request.position>0 && SetTakeProfits(request.position,request.tp)) ||
       (request.position<=0 && SetTakeProfits(request.symbol,request.tp))) && request.tp>0)
      request.tp=0;
   if((request.position>0 && PositionSelectByTicket(request.position)) ||
      (request.position<=0 && PositionSelect(request.symbol)))
     {
      if(PositionGetDouble(POSITION_SL)!=request.sl || PositionGetDouble(POSITION_TP)!=request.tp)
         return(::OrderSend(request,result)); 
     }
//---
   return true;
  }

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

bool CLimitTakeProfit::SetTakeProfits(ulong position_ticket, double new_tp=0)
  {
   if(!PositionSelectByTicket(position_ticket) || (b_OnlyOneSymbol && PositionGetString(POSITION_SYMBOL)!=_Symbol))
      return false;
   if(!c_Symbol.Name(PositionGetString(POSITION_SYMBOL)) || !c_Symbol.Select() || !c_Symbol.Refresh() || !c_Symbol.RefreshRates())
      return false;
//---
   double min_sell_limit=c_Symbol.NormalizePrice(c_Symbol.Ask()+c_Symbol.StopsLevel()*c_Symbol.Point());
   double max_buy_limit=c_Symbol.NormalizePrice(c_Symbol.Bid()-c_Symbol.StopsLevel()*c_Symbol.Point());

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

   MqlTradeRequest tp_request={0};
   MqlTradeResult tp_result={0};
   tp_request.action =  TRADE_ACTION_PENDING;
   tp_request.magic  =  PositionGetInteger(POSITION_MAGIC);
   tp_request.type_filling =  ORDER_FILLING_RETURN;
   tp_request.position=position_ticket;
   tp_request.symbol=c_Symbol.Name();
   int total=i_TakeProfit.Total();
   double tp_price=(new_tp>0 ? new_tp : PositionGetDouble(POSITION_TP));
   if(tp_price<=0)
      tp_price=GetLimitOrderPriceByComment("TPP_"+IntegerToString(position_ticket));
   double open_price=PositionGetDouble(POSITION_PRICE_OPEN);
   int tp_int=(tp_price>0 ? (int)NormalizeDouble(MathAbs(open_price-tp_price)/c_Symbol.Point(),0) : INT_MAX);
   double position_volume=PositionGetDouble(POSITION_VOLUME);
   double closed=0;
   double closed_perc=0;
   double fix_closed_per=0;

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

   for(int i=0;i<total;i++)
     {
      tp_request.comment="TP"+IntegerToString(i)+"_"+IntegerToString(position_ticket);
      if(i_TakeProfit.At(i)<tp_int && d_TakeProfit.At(i)>0)
        {
         if(closed>=position_volume || fix_closed_perc>=100)
            break;

Следующим шагом заполним недостающие элементы структуры торгового запроса. Для этого рассчитываем объем нового лимитного ордера и укажем тип ордера и цену его открытия.

//---
         double lot=position_volume*MathMin(d_TakeProfit.At(i),100-closed)/(100-fix_closed_perc);
         lot=MathMin(position_volume-closed,lot);
         lot=c_Symbol.LotsMin()+MathMax(0,NormalizeDouble((lot-c_Symbol.LotsMin())/c_Symbol.LotsStep(),0)*c_Symbol.LotsStep());
         lot=NormalizeDouble(lot,2);
         tp_request.volume=lot;
         switch((ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE))
           {
            case POSITION_TYPE_BUY:
              tp_request.type=ORDER_TYPE_SELL_LIMIT;
              tp_request.price=c_Symbol.NormalizePrice(open_price+i_TakeProfit.At(i)*c_Symbol.Point());
              break;
            case POSITION_TYPE_SELL:
              tp_request.type=ORDER_TYPE_BUY_STOP;
              tp_request.price=c_Symbol.NormalizePrice(open_price-i_TakeProfit.At(i)*c_Symbol.Point());
              break;
           }

После заполнения структуры торгового запроса проверим — не выставлялся ли ранее лимитный ордер с такими параметрами, для чего воспользуемся методом CheckLimitOrder (алгоритм метода рассмотрим ниже), передав в него заполненную структуру запроса. Если ордер ранее был выставлен, то добавляем выставленный объем ордера к сумме выставленных объемов по позиции. Данная операция нужна для контроля соответствия объемов позиции и выставленных лимитных ордеров.

         if(CheckLimitOrder(tp_request))
           {
            if(tp_request.volume>=0)
              {
               closed+=tp_request.volume;
               closed_perc=closed/position_volume*100;
              }
            else
              {
               fix_closed_per-=tp_request.volume/(position_volume-tp_request.volume)*100;
              }
            continue;
           }

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

         switch(tp_request.type)
           {
            case ORDER_TYPE_BUY_LIMIT:
              tp_request.price=MathMin(tp_request.price,max_buy_limit);
              break;
            case  ORDER_TYPE_SELL_LIMIT:
              tp_request.price=MathMax(tp_request.price,min_sell_limit);
              break;
           }
         if(::OrderSend(tp_request,tp_result))
           {
            closed+=tp_result.volume;
            closed_perc=closed/position_volume*100;
            ZeroMemory(tp_result);
           }
        }
     }

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

   if(tp_price>0 && position_volume>closed)
     {
      tp_request.price=tp_price;
      tp_request.comment="TPP_"+IntegerToString(position_ticket);
      tp_request.volume=position_volume-closed;
      if(tp_request.volume<c_Symbol.LotsMin())
         return false;
      tp_request.volume=c_Symbol.LotsMin()+MathMax(0,NormalizeDouble((tp_request.volume-c_Symbol.LotsMin())/c_Symbol.LotsStep(),0)*c_Symbol.LotsStep());
      tp_request.volume=NormalizeDouble(tp_request.volume,2);
//---
      switch((ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE))
        {
         case POSITION_TYPE_BUY:
           tp_request.type=ORDER_TYPE_SELL_LIMIT;
           break;
         case POSITION_TYPE_SELL:
           tp_request.type=ORDER_TYPE_BUY_LIMIT;
           break;
        }
      if(CheckLimitOrder(tp_request) && tp_request.volume>=0)
        {
         closed+=tp_request.volume;
         closed_perc=closed/position_volume*100;
        }
      else
        {
         switch(tp_request.type)
           {
            case ORDER_TYPE_BUY_LIMIT:
              tp_request.price=MathMin(tp_request.price,max_buy_limit);
              break;
            case  ORDER_TYPE_SELL_LIMIT:
              tp_request.price=MathMax(tp_request.price,min_sell_limit);
              break;
           }
         if(tp_request.volume<=0)
           {
            tp_request.volume=position_volume-closed;
            tp_request.volume=c_Symbol.LotsMin()+MathMax(0,NormalizeDouble((tp_request.volume-c_Symbol.LotsMin())/c_Symbol.LotsStep(),0)*c_Symbol.LotsStep());
            tp_request.volume=NormalizeDouble(tp_request.volume,2);
           }
         if(::OrderSend(tp_request,tp_result))
           {
            closed+=tp_result.volume;
            closed_perc=closed/position_volume*100;
            ZeroMemory(tp_result);
           }
        }
     }      

В заключении метода проверяем закрывает ли объем выставленных лимитных счетов объем позиции. Если да, то обнуляем значение тейк-профита в позиции и выходим из функции.

   if(closed>=position_volume && PositionGetDouble(POSITION_TP)>0)
     {
      ZeroMemory(tp_request);
      ZeroMemory(tp_result);
      tp_request.action=TRADE_ACTION_SLTP;
      tp_request.position=position_ticket;
      tp_request.symbol=c_Symbol.Name();
      tp_request.sl=PositionGetDouble(POSITION_SL);
      tp_request.tp=0;
      tp_request.magic=PositionGetInteger(POSITION_MAGIC);
      if(!OrderSend(tp_request,tp_result))
         return false;
     }
   return true;
  }

Для полноты картины рассмотрим алгоритм метода CheckLimitOrder. Функционально данный метод проверяет наличие выставленного ранее лимитного ордера по подготовленному торговому запросу. При наличии уже выставленного ордера метод возвращает значение true и новый ордер не выставляется.

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

bool CLimitTakeProfit::CheckLimitOrder(MqlTradeRequest &request)
  {
   double min_sell_limit=c_Symbol.NormalizePrice(c_Symbol.Ask()+c_Symbol.StopsLevel()*c_Symbol.Point());
   double max_buy_limit=c_Symbol.NormalizePrice(c_Symbol.Bid()-c_Symbol.StopsLevel()*c_Symbol.Point());

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

   for(int i=0;i<total;i++)
     {
      ulong ticket=OrderGetTicket((uint)i);
      if(ticket<=0)
         continue;
      if(OrderGetString(ORDER_COMMENT)!=request.comment)
         continue;

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

      if(OrderGetDouble(ORDER_VOLUME_INITIAL) != request.volume || OrderGetInteger(ORDER_TYPE)!=request.type)
        {
         MqlTradeRequest del_request={0};
         MqlTradeResult del_result={0};
         del_request.action=TRADE_ACTION_REMOVE;
         del_request.order=ticket;
         if(::OrderSend(del_request,del_result))
            return false;
         request.volume=OrderGetDouble(ORDER_VOLUME_INITIAL);
        }

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

      if(MathAbs(OrderGetDouble(ORDER_PRICE_OPEN)-request.price)>=c_Symbol.Point())
        {
         MqlTradeRequest mod_request={0};
         MqlTradeResult mod_result={0};
         mod_request.action=TRADE_ACTION_MODIFY;
         mod_request.price=request.price;
         mod_request.magic=request.magic;
         mod_request.symbol=request.symbol;
         switch(request.type)
           {
            case ORDER_TYPE_BUY_LIMIT:
              if(mod_request.price>max_buy_limit)
                 return true;
              break;
            case ORDER_TYPE_SELL_LIMIT:
              if(mod_request.price<min_sell_limit)
                 return true;
              break;
           }
         bool mod=::OrderSend(mod_request,mod_result);
        }
      return true;
     }

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

   if(!PositionSelectByTicket(request.position))
      return true;
//---
   return CheckOrderInHistory(PositionGetInteger(POSITION_IDENTIFIER),request.comment, request.type, request.volume);
  }

В зависимости от типа счета у нас возможно два варианта отработки лимитного ордера:

  1. Прямая отработка в позиции (неттинговые счета).
  2. Лимитный ордер открыл встречную позицию, а потом проведено встречное закрытие позиций (хеджинговые счета).

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

bool CLimitTakeProfit::CheckOrderInHistory(ulong position_id, string comment, ENUM_ORDER_TYPE type, double &volume, ulong call_position=0)
  {
   if(!HistorySelectByPosition(position_id))
      return true;
   int total=HistoryDealsTotal();
   bool hedging=(AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING);
//---
   for(int i=0;i<total;i++)
     {
      ulong ticket=HistoryDealGetTicket((uint)i);
      ticket=HistoryDealGetInteger(ticket,DEAL_ORDER);
      if(!HistoryOrderSelect(ticket))
         continue;
      if(ticket<=0)
         continue;

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

      if(hedging && HistoryOrderGetInteger(ticket,ORDER_POSITION_ID)!=position_id && HistoryOrderGetInteger(ticket,ORDER_POSITION_ID)!=call_position)
        {
         if(CheckOrderInHistory(HistoryOrderGetInteger(ticket,ORDER_POSITION_ID),comment,type,volume))
            return true;
         if(!HistorySelectByPosition(position_id))
            continue;
        }

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

      if(HistoryOrderGetString(ticket,ORDER_COMMENT)!=comment)
         continue;
      if(HistoryOrderGetInteger(ticket,ORDER_TYPE)!=type)
         continue;
//---
      volume=-OrderGetDouble(ORDER_VOLUME_INITIAL);
      return true;
     }
   return false;
  }

С полным кодом всех методов и функций можно ознакомиться во вложении.

3.2. Блок обработки торговых операций

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

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

Алгоритм метода начинается с проведения подготовительной работы: получим количество открытых по счету позиций и проверим тип счета.

bool CLimitTakeProfit::OnTrade(void)
  {
   int total=PositionsTotal();
   bool result=true;
   bool hedhing=AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING;

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

   for(int i=0;i<total;i++)
     {
      ulong ticket=PositionGetTicket((uint)i);
      if(ticket<=0 || (b_OnlyOneSymbol && PositionGetString(POSITION_SYMBOL)!=_Symbol))
         continue;
//---
     if(i_Magic>0)
        {
         if(hedhing && PositionGetInteger(POSITION_MAGIC)!=i_Magic)
            continue;
        }

Затем, для хеджинговых, счетов проверим — не является ли позиция отработкой нашего лимитного тейк-профита. Если да, то проведем операцию встречного закрытия позиций. После успешного встречного закрытия позиций переходим к следующей позиции.

      if(hedhing)
        {
         string comment=PositionGetString(POSITION_COMMENT);
         if(StringFind(comment,"TP")==0)
           {
            int start=StringFind(comment,"_");
            if(start>0)
              {
               long ticket_by=StringToInteger(StringSubstr(comment,start+1));
               long type=PositionGetInteger(POSITION_TYPE);
               if(ticket_by>0 && PositionSelectByTicket(ticket_by) && type!=PositionGetInteger(POSITION_TYPE))
                 {
                  MqlTradeRequest   request  ={0};
                  MqlTradeResult    trade_result   ={0};
                  request.action=TRADE_ACTION_CLOSE_BY;
                  request.position=ticket;
                  request.position_by=ticket_by;
                  if(::OrderSend(request,trade_result))
                     continue;
                 }
              }
           }
        }

В завершении цикла вызовем метод SetTakeProfits для проверки и выставления лимитных ордеров по позиции. Алгоритм метода был описан выше.

      result=(SetTakeProfits(PositionGetInteger(POSITION_TICKET)) && result);
     }

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

   CheckLimitOrder();
//---
   return result;
  }

Алгоритм этого метода построен на переборе всех выставленных ордеров, среди которых по комментариям отбираются наши ордера.

void CLimitTakeProfit::CheckLimitOrder(void)
  {
   int total=OrdersTotal();
   bool res=false;
//---
   for(int i=0;(i<total && !res);i++)
     {
      ulong ticket=OrderGetTicket((uint)i);
      if(ticket<=0)
         continue;
      string comment=OrderGetString(ORDER_COMMENT);
      if(StringFind(comment,"TP")!=0)
         continue;
      int pos=StringFind(comment,"_",0);
      if(pos<0)
         continue;

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

      long pos_ticker=StringToInteger(StringSubstr(comment,pos+1));
      if(!PositionSelectByTicket(pos_ticker))
        {
         MqlTradeRequest del_request={0};
         MqlTradeResult del_result={0};
         del_request.action=TRADE_ACTION_REMOVE;
         del_request.order=ticket;
         if(::OrderSend(del_request,del_result))
           {
            i--;
            total--;
           }
         continue;
        }

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

      switch((ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE))
        {
         case POSITION_TYPE_BUY:
           if(OrderGetInteger(ORDER_TYPE)==ORDER_TYPE_SELL_LIMIT)
              continue;
           break;
         case POSITION_TYPE_SELL:
           if(OrderGetInteger(ORDER_TYPE)==ORDER_TYPE_BUY_LIMIT)
              continue;
           break;
        }
      MqlTradeRequest del_request={0};
      MqlTradeResult del_result={0};
      del_request.action=TRADE_ACTION_REMOVE;
      del_request.order=ticket;
      if(::OrderSend(del_request,del_result))
        {
         i--;
         total--;
        }
     }
//---
   return;
  }

С полным кодом всех методов класса можно ознакомиться во вложении.

4. Интеграции класса в советник

После завершения работы над классом рассмотрим механизм его интеграции в уже написанный советник.

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

Вторым шагом ниже кода нашего класса создадим функцию LimitOrderSend с параметрами вызова, аналогичными функции OrderSend, единственным функционалом которой будет вызов метода CLimitTakeProfit::OrderSend. Затем с помощью директивы #define осуществим подмену оригинальной функции OrderSend на нашу. Применение такого метода дает возможность подставить наш код сразу во все функции советника, отправляющие торговые запросы. Что избавит нас от долгого поиска таких команд по всему коду советника.

bool LimitOrderSend(const MqlTradeRequest &request, MqlTradeResult &result)
 { return CLimitTakeProfit::OrderSend(request,result); } 
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
#define OrderSend(request,result)      LimitOrderSend(request,result)

Так как во многих советниках не используется функция OnTrade, мы также можем ее прописать в файле нашего класса. Но если Ваш советник использует данную функцию, то Вам придется удалить или скрыть в комментариях нижеприведенный код и добавить вызов метода CLimitTakeProfit::OnTrade в тело функции вашего советника.

void OnTrade()
  {
   CLimitTakeProfit::OnTrade();
  }

Далее, для интеграции класса в советник нам остается добавить ссылку на файл нашего класса с помощью директивы #include. Но не забываем, что наш класс должен идти до вызова других библиотек и кода советника. Ниже приведен пример добавления класса к советнику MACD Sample.mq5 из стандартной поставки терминала.

//+------------------------------------------------------------------+
//|                                          MACD Sample LimitTP.mq5 |
//|                   Copyright 2009-2017, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright   "Copyright 2009-2017, MetaQuotes Software Corp."
#property link        "http://www.mql5.com"
#property version     "5.50"
#property description "It is important to make sure that the expert works with a normal"
#property description "chart and the user did not make any mistakes setting input"
#property description "variables (Lots, TakeProfit, TrailingStop) in our case,"
#property description "we check TakeProfit on a chart of more than 2*trend_period bars"

#define MACD_MAGIC 1234502
//---
#include <Trade\LimitTakeProfit.mqh>
//---
#include <Trade\Trade.mqh>
#include <Trade\SymbolInfo.mqh>
#include <Trade\PositionInfo.mqh>
#include <Trade\AccountInfo.mqh>
//---

В коде функции OnInit можно добавить частичное закрытие позиции. И наш советник готов к использованию.

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

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(void)
  {
//--- create all necessary objects
   if(!ExtExpert.Init())
      return(INIT_FAILED);
   CLimitTakeProfit::AddTakeProfit(100,50);
//--- secceed
   return(INIT_SUCCEEDED);
  }

Демонстрация работы советника

С полным кодом советника можно ознакомиться во вложении.

Заключение

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

Программы, используемые в статье:

#
Имя
Тип
Описание
1LimitTakeProfit.mqhБиблиотека классаКласс подмены тейк-профита ордеров на лимитные ордера
2MACD Sample.mq5СоветникОригинальный советник из примеров MetaTrader 5
3MACD Sample LimitTP.mq5СоветникПример интеграции класса в советник из примеров MetaTrader 5


Прикрепленные файлы |
MQL5.zip (184.41 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (36)
Maxim Dmitrievsky
Maxim Dmitrievsky | 11 окт. 2018 в 17:50
Denis Kirichenko:

Сочувствую. Но из-за такой суммы наверное стоило бы пободаться. Ещё раз повторюсь, что нужно знать нюансы Политики исполнения (читай свои права). Ну и подбирать аргументы, в том числе и угрозы пожаловаться Регулятору :-)

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

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

теперь только привелеги зарабатывают, по договоренности

Ramiz Mavludov
Ramiz Mavludov | 14 окт. 2018 в 19:30
Maxim Dmitrievsky:

бесполезно в 90% случаев, сумма тоже не важна. Висела заявка на 400к недополученной прибыли у регулируемого, в ответ отписки о некорректной работе серверов

ну да ладно, это все лирика :)

можно название в лс мне?
Sergey Chalyshev
Sergey Chalyshev | 14 окт. 2018 в 23:42
Denis Kirichenko:

Может быть. Но иногда делаю проверку логов. Претензий именно насчёт лимитников не было вообще. Вот ещё у другого известного брокера:

  • Ордер на продажу типа Limit (Take Profit) помещается в очередь на исполнение, если Цена Bid, транслируемая в Потоке Котировок, станет равной либо больше цены, указанной в Ордере. 
  • Ордер на покупку типа Limit (Take Profit) помещается в очередь на исполнение, если Цена Ask, транслируемая в Потоке Котировок, станет равной либо меньше цены, указанной в Ордере.

Имхо, если есть нарушение Политики исполнения, то можно жаловаться брокеру. Солидная контора всегда рассматривает претензии и, как правило, с пониманием на них реагирует. 

Где вы видели цену Ask "транслируемая в Потоке Котировок" ?

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

Maxim Dmitrievsky
Maxim Dmitrievsky | 15 окт. 2018 в 05:48
Ramiz Mavludov:
можно название в лс мне?

Halifax, сама по себе неплохая контора (не хуже остальных). резидентам рф зарегаться сложно там.

justinmcridge
justinmcridge | 16 авг. 2020 в 23:35
it gives me <Stack Overflow> error! And the problem is in the library file:
<LimitTakeProfit.mqh>
Anyone figured out what went wrong?
Автоматическая оптимизация советника в MetaTrader 5 Автоматическая оптимизация советника в MetaTrader 5
В данной статье описана реализация механизма самооптимизации работающего эксперта в MetaTrader 5.
100 лучших проходов оптимизации (Часть 1). Cоздание анализатора оптимизаций 100 лучших проходов оптимизации (Часть 1). Cоздание анализатора оптимизаций
В данной статье я расскажу, как создать приложение для отбора лучших проходов оптимизаций по нескольким возможным вариантам. Данное приложение умеет фильтровать и сортировать оптимизационные результаты по множеству коэффициентов. Проходы оптимизации записываются в базу данных, поэтому вы всегда можете отобрать новые параметры робота без необходимости переоптимизирования. Вдобавок ко всему это позволяет увидеть все проходов оптимизации на едином графике, рассчитывать параметрические VaR коэффициенты и строить график нормального распределения проходов и результатов торговли конкретного выделенного варианта сочетания коэффициентов. Также строятся графики некоторых из рассчитываемых коэффициентов в динамике, начиная с момента старта оптимизации (или с выбранной даты до другой выбранной даты).
Гэп - доходная стратегия или 50/50? Гэп - доходная стратегия или 50/50?
Исследование явления гэпа — ситуации существенной разницы между ценой закрытия предыдущего таймфрейма и ценой открытия следующего, и в какую сторону пойдёт дневной бар. Применение системной DLL функции GetOpenFileName.
Реверсирование: снижаем максимальную просадку и тестируем другие рынки Реверсирование: снижаем максимальную просадку и тестируем другие рынки
В этой статье мы продолжим рассматривать тему реверсирования. Мы попробуем снизить максимальную просадку по балансу до приемлемого уровня на ранее рассмотренных инструментах. Проверим, насколько при этом снизится полученная прибыль. А также проверим, как работает реверсирование на других рынках: фондовом, сырьевом, индексах и ETF, аграрном. Внимание, статья содержит очень много картинок!