Универсальный торговый эксперт: работа с отложенными ордерами и поддержка хеджинга (часть 5)

Vasiliy Sokolov | 27 апреля, 2016

Оглавление


Введение

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

Кроме того, последние версии MetaTrader 5 стали поддерживать разнонаправленную торговлю на счетах с хеджингом (читайте статью "В MetaTrader 5 добавлена хеджинговая система учета позиций"). Это также потребовало внести определенные изменения в код CStrategy, чтобы последние версии этого комплекса могли корректно работать на новых типах счетов. Изменений в коде для поддержки возможности хеджирования потребовалось сделать совсем немного, что говорит о правильном изначально выбранном подходе: любые изменения и расширения, какими бы глобальными они ни были, не приводят к утрате работоспособности торгового движка. Напротив, с новыми инструментами MetaTrader 5, такими как разнонаправленная торговля, появляется много интересных возможностей, которые непременно обретут свою реализацию в новых версиях CStrategy.

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

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


Доступ к текущим ценам через методы Ask, Bid и Last. Переопределение функции Digits

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

double ask = SymbolInfoDouble(ExpertSymbol(), SYMBOL_ASK);
int digits = SymbolInfoInteger(ExpertSymbol(), SYMBOL_DIGITS);
ask = NormalizeDouble(ask, digits);

Заметим, что хотя получение цены ask требует использования всего одной функции — SymbolInfoDouble —, для надежной работы необходимо также нормализовать полученное значение до точности текущего инструмента. Поэтому получение фактической цены ask в действительности требует большего количества действий. То же самое относится к алгоритмам получения цен Bid и Last.

Чтобы упростить работу пользователей, в класс CStrategy были введены три метода: Ask(), Bid() и Last(). Каждый из них получает соответствующую цену и нормализует ее в соответствии с текущим инструментом: 

//+------------------------------------------------------------------+
//| Возвращает цену Ask.                                             |
//+------------------------------------------------------------------+
double CStrategy::Ask(void)
  {
   double ask = SymbolInfoDouble(ExpertSymbol(), SYMBOL_ASK);
   int digits = (int)SymbolInfoInteger(ExpertSymbol(), SYMBOL_DIGITS);
   ask = NormalizeDouble(ask, digits);
   return ask;
  }
//+------------------------------------------------------------------+
//| Возвращает цену Bid.                                             |
//+------------------------------------------------------------------+
double CStrategy::Bid(void)
  {
   double bid = SymbolInfoDouble(ExpertSymbol(), SYMBOL_BID);
   int digits = (int)SymbolInfoInteger(ExpertSymbol(), SYMBOL_DIGITS);
   bid = NormalizeDouble(bid, digits);
   return bid;
  }
//+------------------------------------------------------------------+
//| Возвращает цену Last.                                             |
//+------------------------------------------------------------------+
double CStrategy::Last(void)
  {
   double last = SymbolInfoDouble(ExpertSymbol(), SYMBOL_LAST);
   int digits = (int)SymbolInfoInteger(ExpertSymbol(), SYMBOL_DIGITS);
   last = NormalizeDouble(last, digits);
   return last;
  }

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

Помимо организации доступа к текущим ценам Ask, Bid и Last через одноименные методы, CStrategy переопределяет системную функцию Digits. Эта функция возвращает количество знаков после запятой для текущего инструмента. Может показаться, что переопределение этой функции бессмысленно, однако это не так. Дело в том, что рабочий символ экземпляра эксперта может отличаться от символа, на котором запущен исполняющий модуль, содержащий стратегии. В этом случае вызов системной функции Digits может ввести в заблуждение. Она вернет количество знаков после запятой не для рабочего инструмента, а для того символа, на котором запущен сам эксперт. Чтобы этого не происходило, функция Digits в CStartegy переопределена одноименным методом. При обращении к этой функции фактически вызывается именно этот метод. Он возвращает количество знаков после запятой именно для рабочего символа эксперта. Вот исходный код этого метода:

//+------------------------------------------------------------------+
//| Возвращает количество знаков после запятой для рабочего символа  |
//| инструмента                                                      |
//+------------------------------------------------------------------+
int CStrategy::Digits(void)
  {
   int digits = (int)SymbolInfoInteger(ExpertSymbol(), SYMBOL_DIGITS);
   return digits;
  }

Необходимо помнить об этой особенности и понимать значение этого переопределения.

 

Поддержка счетов с хеджингом

С недавнего времени MetaTrader 5 стал поддерживать так называемые хеджирующие счета. Это счета, на которых может быть открыто одновременно несколько позиций, в том числе и разнонаправленных — как на покупку, так и на продажу. Вспомним, что все действия с позициями в CStrategy обрабатываются в специальных обработчиках  SupportBuy и SupportSell. В эти методы по очереди передаются позиции, находящиеся в списке для текущей стратегии. Неважно, одна ли эта позиция или их несколько. Открыты они могут быть как на одном символе, так и на разных. Механизм обработки и передачи этих позиций будет единым. Поэтому изменений, которые потребуются для поддержки хеджирующих счетов, будет немного. Прежде всего, необходимо будет переписать метод RebuildPosition. При изменении торгового окружения (совершения новых сделок) этот метод перестроит все позиции из списка. В зависимости от того, какой режим счета используется, перестроение будет различаться. Для неттинговых счетов будет использован алгоритм выбора позиции по символу. Для счетов с хеджингом алгоритм выбора позиции будет использовать индекс позиции в общем списке.

Приведем старую версию метода RebuildPosition:

//+------------------------------------------------------------------+
//| Перестраивает списки позиций.                                    |
//+------------------------------------------------------------------+
void CStrategy::RebuildPositions(void)
{
   ActivePositions.Clear();
   for(int i = 0; i < PositionsTotal(); i++)
   {
      string symbol = PositionGetSymbol(i);
      PositionSelect(symbol);
      CPosition* pos = new CPosition();
      ActivePositions.Add(pos);
   }
}

В новой версии RebuildPosition, в зависимости от типа счета, будут использованы два алгоритма выбора позиции:

//+------------------------------------------------------------------+
//| Перестраивает списки позиций.                                    |
//+------------------------------------------------------------------+
void CStrategy::RebuildPositions(void)
  {
   ActivePositions.Clear();
   ENUM_ACCOUNT_MARGIN_MODE mode=(ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE);
   if(mode!=ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
     {
      for(int i=0; i<PositionsTotal(); i++)
        {
         string symbol=PositionGetSymbol(i);
         PositionSelect(symbol);
         CPosition *pos=new CPosition();
         ActivePositions.Add(pos);
        }
     }
   else
     {
      for(int i=0; i<PositionsTotal(); i++)
        {
         ulong ticket=PositionGetTicket(i);
         PositionSelectByTicket(ticket);
         CPosition *pos=new CPosition();
         ActivePositions.Add(pos);
        }
     }
  }

Обратите внимание, что как для хеджируемых, так и для классических счетов используется один и тот же класс позиции — CPosition. После того, как позиция выбрана, доступ к ее свойствам осуществляется через методы PositionGetInteger, PositionGetDouble и PositionGetString. При этом не имеет значения, была ли выбрана хеджируемая или обычная позиция. И в том, и в другом случае доступ к свойствам позиции один и тот же. Именно поэтому на разных типах счетов представляется возможным использовать один и тот же класс позиции — CPosition. 

Заметим также, что каких-либо других методов внутри класса CStrategy, которые потребовали бы переписывания, нет. CStrategy спроектирован таким образом, что работа стратегий, базирующихся на этом движке, зависит от контекста. Это значит, что если стратегия работает на хеждируемых счетах, открывая по нескольку позиций в одном направлении, то и вести эти позиции она будет параллельно, видя каждую как отдельный класс CPosition. Если же наоборот, на счете может быть открыта лишь одна позиция, то стратегия будет сопровождать только ее одну, но в виде того же объекта CPosition.  

Помимо дополнения метода RebuildPosition, необходимо также изменить внутреннее содержание некоторых методов CPosition. Располагаясь в файле PositionMT5.mqh, этот класс содержит методы, базирующиеся на вызове системных функций. Также CPosition активно использует стандартный торговый класс CTrade. В последнюю версию CTrade были внесены дополнительные изменения, позволяющие использовать свойства хеджируемых позиций. Например, хеджируемую позицию можно закрыть позицией встречного направления, для чего вызывается новый метод CTrade::PositionCloseBy. Ниже приведены методы CPosition, которые изменили свое содержание по сравнению с предыдущими версиями CPosition:

//+------------------------------------------------------------------+
//|                                                  PositionMT5.mqh |
//|                                 Copyright 2016, Vasiliy Sokolov. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2016, Vasiliy Sokolov."
#property link      "http://www.mql5.com"
#include <Object.mqh>
#include "Logs.mqh"
#include <Trade\Trade.mqh>
#include "Trailing.mqh"
//+------------------------------------------------------------------+
//| Класс активной позиции, для классических стратегий               |
//+------------------------------------------------------------------+
class CPosition : public CObject
  {
   ...
  };
...
//+------------------------------------------------------------------+
//| Возвращает абсолютный уровень стоп-лосса для текущей позиции.    |
//| Если уровень стоп-лосса не установлен, возвращает 0.0            |
//+------------------------------------------------------------------+
double CPosition::StopLossValue(void)
{
   if(!IsActive())
      return 0.0;
   return PositionGetDouble(POSITION_SL);
}
//+------------------------------------------------------------------+
//| Устанавливает абсолютный уровень стоп-лосса                      |
//+------------------------------------------------------------------+
bool CPosition::StopLossValue(double sl)
{
   if(!IsActive())
      return false;
   return m_trade.PositionModify(m_id, sl, TakeProfitValue());
}
//+------------------------------------------------------------------+
//| Возвращает абсолютный уровень стоп-лосса для текущей позиции.    |
//| Если уровень стоп-лосса не установлен, возвращает 0.0            |
//+------------------------------------------------------------------+
double CPosition::TakeProfitValue(void)
{
   if(!IsActive())
      return 0.0;
   return PositionGetDouble(POSITION_TP);
}
//+------------------------------------------------------------------+
//| Устанавливает абсолютный уровень стоп-лосса                      |
//+------------------------------------------------------------------+
bool CPosition::TakeProfitValue(double tp)
  {
   if(!IsActive())
      return false;
   return m_trade.PositionModify(m_id, StopLossValue(), tp);
  }
//+------------------------------------------------------------------+
//| Закрывает текущую позицию по рынку, устанавливая закрывающий     |
//| комментарий, равный comment                                       |
//+------------------------------------------------------------------+
bool CPosition::CloseAtMarket(string comment="")
  {
   if(!IsActive())
      return false;
   ENUM_ACCOUNT_MARGIN_MODE mode=(ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE);
   if(mode != ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
      return m_trade.PositionClose(m_symbol);
   return m_trade.PositionClose(m_id);
  }
//+------------------------------------------------------------------+
//| Возвращает текущий объем позиции.                                |
//+------------------------------------------------------------------+
double CPosition::Volume(void)
  {
   if(!IsActive())
      return 0.0;
   return PositionGetDouble(POSITION_VOLUME);
  }
//+------------------------------------------------------------------+
//| Возвращает текущую прибыль позиции в валюте депозита.            |
//+------------------------------------------------------------------+
double CPosition::Profit(void)
  {
   if(!IsActive())
      return 0.0;
   return PositionGetDouble(POSITION_PROFIT);
  }
//+------------------------------------------------------------------+
//| Возвращает истину, если позиция активна. Возвращает ложь         |
//| в противном случае.                                              |
//+------------------------------------------------------------------+
bool CPosition::IsActive(void)
{
   ENUM_ACCOUNT_MARGIN_MODE mode=(ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE);
   if(mode!=ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
      return PositionSelect(m_symbol);
   else
      return PositionSelectByTicket(m_id);
}
//+------------------------------------------------------------------+

Как видно, основу всех этих методов составляет вызов другого метода IsActive. Этот метод возвращает истину, если активная позиция, представленная объектом CPosition, реально существует в системе. В действительности, все, что делает метод — это выбор позиции одним из двух способов, в зависимости от типа счета. Если счет классический, с нетто-позициями, выбор позиции происходит с помощью функции PositionSelect, которой требуется указать символ позиции. Если счет хеджируемый, позиция выбирается по ее тикету, с помощью новой функции PositionSelectByTicket. Результат выбора (true или false) возвращается вызывающей процедуре. Именно этот метод задает дальнейший контекст работы с позицией. Так как все торговые функции CPosition базируются на торговом классе CTrade, то изменять торговый алгоритм также не потребовалось. CTrade прекрасно модифицирует, открывает и закрывает как обычные, так и разнонаправленные позиции.  

 

Работа с отложенными ордерами в предыдущих версиях CStrategy

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

Первоначально торговый движок CStrategy создавался без учета возможности работы с отложенными ордерами. Однако это не означало, что в рамках стратегии, написанной на основе класса CStrategy, работа с отложенными ордерами была невозможна. Просто для работы с ними необходимо было использовать стандартные функции MetaTrader 5 вроде OrdersTotal() и OrderSelect.

Предположим, что эксперт на каждом новом баре выставляет или модифицирует ранее отложенный stop-ордер таким образом, чтобы его цена срабатывания была на 0.25% выше (для покупки) или ниже (для продажи) от текущей цены. Идея такова: если в течение бара цена совершила резкое движение (импульс), то такой ордер будет исполнен, и эксперт войдет в момент сильного движения. Если же движение было недостаточно сильным, то на протяжении текущего бара ордер исполнен не будет, и его необходимо будет перенести на новый уровень в ожидании нового импульса. С полной реализацией этой стратегии вы сможете ознакомиться ниже, в разделах, посвященных описанию этого алгоритма, названного CImpulse. Эта простая система, как следует из ее названия, основана на импульсе. Она требует единственного входа — срабатывания отложенного ордера. Как мы уже знаем, в CStrategy для входа в позицию существуют специальные переопределенные методы BuyInit и SellInit. Следовательно, именно в этих методах необходимо расположить алгоритмы работы с отложенными ордерами. Без прямой поддержки от CStrategy данный код для покупок будет следующим:

//+------------------------------------------------------------------+
//| Логика работы с отложенными ордерами на покупку.                 |
//+------------------------------------------------------------------+
void CMovingAverage::InitBuy(const MarketEvent &event)
  {
   if(!IsTrackEvents(event))return;                      // Обрабатываем только нужное событие!
   if(positions.open_buy > 0) return;                    // Если есть хотя бы одна длиная позиция - покупать больше не надо, т.к. мы уже купили!
   int buy_stop_total = 0;
   for(int i = OrdersTotal()-1; i >= 0; i--)
   {
      ulong ticket = OrderGetTicket(i);
      if(!OrderSelect(ticket))continue;
      ulong magic = OrderGetInteger(ORDER_MAGIC);
      if(magic != ExpertMagic())continue;
      string symbol = OrderGetString(ORDER_SYMBOL);
      if(symbol != ExpertSymbol())continue;
      ENUM_ORDER_TYPE order_type = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE);
      if(order_type == ORDER_TYPE_BUY_STOP)
      {
         buy_stop_total++;
         Trade.OrderModify(ticket, Ask()*0.0025, 0, 0, 0);
      }
   }
   if(buy_stop_total == 0)
      Trade.BuyStop(MM.GetLotFixed(), Ask() + Ask()*0.0025, ExpertSymbol(), 0, 0, NULL);
  }

Метод IsTrackEvents определяет, что пришедшее событие соответствует открытию нового бара на текущем символе. Затем эксперт смотрит количество открытых позиций в направлении покупки. Если есть хотя бы одна длинная позиция, то покупать больше не надо, и логика завершается. Далее идет перебор всех текущих отложенных ордеров. В цикле по очереди перебираются все их индексы. Каждый из этих ордеров выбирается по индексу, а затем анализируется его магический номер и символ. Если эти оба параметра соответствуют параметрам эксперта, считается, что ордер принадлежит текущему торговому эксперту, и счетчик количества ордеров увеличивается на единицу. Сам ордер модифицируется — его цена входа меняется на текущую цену + 0.25% от нее. Если отложенных ордеров нет, о чем свидетельствует счетчик buy_stop_order, равный нулю, то выставляется новый отложенный ордер на расстоянии 0.25% от текущей цены.

Заметим, что непосредственного открытия позиций в InitBuy нет. CStrategy не накладывает такого ограничения на методы открытия позиции. Формально в данных методах может располагаться любая логика эксперта. Однако для правильной обработки необходимо, чтобы эта логика была связана именно с открытием позиции, через отложенные либо рыночные ордера. 

Как видно из приведенного примера, работа с отложенными ордерами осуществляется по тому же принципу, что и работа с обычными позициями. Главное требование остается тем же: необходимо разделить логику покупки и логику продажи, описав их в отдельных методах BuyInit и SellInit соответственно. В BuyInit необходимо обрабатывать только отложенные ордера на покупку. В SellInit необходимо обрабатывать отложенные ордера только на продажу. В остальном логика работы с отложенными ордерами напоминает классическую схему работы, принятую в MetaTrader 4: перебор ордеров, выбор ордеров принадлежащих текущему эксперту, анализ торгового окружения и принятие решения об открытии или изменении уже существующего ордера. 

 

Работа с отложенными ордерами при помощи CPendingOrders и COrdersEnvironment

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

Чтобы поддерживать данные возможности и впредь, было решено расширить функционал CStartegy таким образом, чтобы предоставить удобный объектно-ориентированный механизм для работы с отложенными ордерами. Данный механизм представлен двумя специальными классами: CPendingOrder и COrdersEnvironment. COrder предоставляет удобный объект, содержащий все свойства отложенного ордера, которые можно получить через функции OrderGetInteger, OrderGetDouble и OrderGetString. Назначение COrdersEnvironment будет объяснено ниже.

Предположим, что объект CPendingOrder представляет отложенный ордер, который реально существует в системе. Если этот отложенный ордер удалить, тогда что должно произойти с самим объектом CPendingOrder, который его представляет? Если объект останется после того, как фактический отложенный ордер будет удален, это повлечет серьезные ошибки. Эксперт обратится к объекту CPendingOrder и, найдя его, ошибочно "решит", что отложенный ордер по-прежнему существует в системе. Чтобы этого избежать, необходимо гарантировать синхронизацию торгового окружения с объектным окружением CStrategy. Иными словами, необходимо создать механизм, позволяющий гарантировать доступ только к тем объектам, которые в реальности существуют. Именно этим и занимается класс COrdersEnvironment. Его реализация достаточно проста, однако она позволяет получить доступ только к тем объектам CPendingOrders, которые представляют реальные отложенные ордера.

Основу класса  COrdersEnvironment составляют методы GetOrder и Total. Первый возвращает объект CPendingOrders, соответствующий отложенному ордеру по определенному индексу в системе отложенных ордеров MetaTrader 5. Второй метод возвращает общее количество отложенных ордеров. Настало время рассмотреть этот класс подробней. Приведем его исходный код ниже:

//+------------------------------------------------------------------+
//| Класс для работы с отложенными ордерами                          |
//+------------------------------------------------------------------+
class COrdersEnvironment
{
private:
   CDictionary    m_orders;         // Общее количество всех отложенных ордеров
public:
                  COrdersEnvironment(void);
   int            Total(void);
   CPendingOrder* GetOrder(int index);
};
//+------------------------------------------------------------------+
//| Необходимо знать текущий символ и магический номер эксперта      |
//+------------------------------------------------------------------+
COrdersEnvironment::COrdersEnvironment(void)
{
}
//+------------------------------------------------------------------+
//| Возвращает отложенный ордер                                      |
//+------------------------------------------------------------------+
CPendingOrder* COrdersEnvironment::GetOrder(int index)
{
   ulong ticket = OrderGetTicket(index);
   if(ticket == 0)
      return NULL;
   if(!m_orders.ContainsKey(ticket))
      return m_orders.GetObjectByKey(ticket);
   if(OrderSelect(ticket))
      return NULL;
   CPendingOrder* order = new CPendingOrder(ticket);
   m_orders.AddObject(ticket, order);
   return order;
}
//+------------------------------------------------------------------+
//| Возвращает количество отложенных ордеров                         |
//+------------------------------------------------------------------+
int COrdersEnvironment::Total(void)
{
   return OrdersTotal();   
}

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

Метод GetOrder требует для своей работы указать индекс отложенного ордера в системе. Так как всегда точно известно общее количество ордеров, получаемое с помощью метода Total, то индекс требуемого ордера также всегда известен и в точности соответствует индексу фактического отложенного ордера в системе. Далее метод GetOrder получает идентификатор отложенного ордера по его индексу. Если по каким-то причинам ордер был удален, то идентификатор ордера будет равен нулю, и следовательно, эксперту будет возвращена константа NULL, сигнализируя о том, что ордер по запрашиваемому индексу не найден.

Каждый объект, созданный динамически, требует явного своего удаления с помощью специального оператора delete. Так как GetOrder создает объекты CPendingOrders динамически, с помощью оператора new, то также требуется удаление этих объектов. Чтобы избавить пользователя от требования удаления объекта после его получения, был использован специальный прием — размещение созданного объекта в специальном контейнере-словаре внутри объекта COrdersEnvironment. В словаре доступ к элементу осуществляется по его уникальному ключу, в данном случае — по идентификатору ордера. В таком случае, если ордер по-прежнему существует, ранее созданный объект, представляющий этот ордер, уже скорее всего был создан и размещен в контейнере. Именно он и возвращается функцией GetOrder. Если обращение к ордеру с данным идентификатором происходит впервые, то создается новый объект CPendingOrder, после чего он размещается в словаре, а ссылка на него возвращается пользователю.

Что дает данный подход? Прежде всего, метод GetOrder гарантирует, что вернет только тот объект, который представляет реально существующий отложенный ордер. За это отвечает системная функция OrderGetTicket. Во-вторых, объект будет создаваться только в том случае, если ранее он еще не был создан. Это позволяет сэкономить дополнительные ресурсы компьютера. И, наконец, в-третьих, данный алгоритм освобождает пользователя от удаления полученного объекта. Так как все объекты хранятся в словаре, они будут удалены автоматически после деинициализации самого COrdersEnvironment. 

 

Обращение к отложенным ордерам в коде эксперта

Настало время переписать логику работы с отложенными ордерами, представленную в предыдущем разделе "Работа с отложенными ордерами в предыдущих версиях CStrategy". С использованием классов CPendingOrder и COrdersEnvironment код будет выглядит следующим образом:

//+------------------------------------------------------------------+
//| Покупаем, когда быстрая скользящая средняя выше медленной.       |
//+------------------------------------------------------------------+
void CMovingAverage::InitBuy(const MarketEvent &event)
  {
   if(!IsTrackEvents(event))return;                      // Обрабатываем только нужное событие!
   if(positions.open_buy > 0) return;                    // Если есть хотя бы одна длиная позиция - покупать больше не надо, т.к. мы уже купили!
   int buy_stop_total = 0;
   for(int i = PendingOrders.Total()-1; i >= 0; i--)
     {
      CPendingOrder* Order = PendingOrders.GetOrder(i);
      if(Order == NULL || !Order.IsMain(ExpertSymbol(), ExpertMagic()))
         continue;
      if(Order.Type() == ORDER_TYPE_BUY_STOP)
       {
         buy_stop_total++;
         Order.Modify(Ask() + Ask()*0.0025);
       }
       //delete Order; Удалять объект Order не надо!
     }
   if(buy_stop_total == 0)
      Trade.BuyStop(MM.GetLotFixed(), Ask() + Ask()*0.0025, ExpertSymbol(), 0, 0, NULL);
  }

Объект PendingsOrders представляет собой класс COrdersEnvironment. Перебор отложенных ордеров начинается так же, как и при использовании системных функций. Затем делается попытка получить объект-ордер по индексу отложенных ордеров, равному переменной i. Если ордер по каким-то причинам не получен либо он принадлежит другому эксперту, перебор ордеров продолжается с нового ордера. Для этого существует специальный метод IsMain объекта CPendingorder, который возвращает значение true, если ордер имеет тот же символ и магический номер, что и сам эксперт, тем самым определяя, принадлежит ли текущий ордер данному эксперту.

Если тип ордера соответствует типу ORDER_TYPE_BUY_STOP, это означает, что выставленный отложенный ордер не сработал, и его уровень требуется изменить согласно формуле: текущая цена + 0.25%. За изменение отложенного ордера отвечает метод Modify и его перегруженная версия. Он позволяет задать цену, а также другие параметры, которые требуется изменить.

Обратите внимание, что после того, как работа с отложенным ордером завершена, сам объект ордера удалять не надо. Ссылку на ордер нужно оставить как есть. Контейнер PendingOrders самостоятельно управляет объектами типа CPendingOrder и не требует удаления возвращенных объектов в функциях пользователя. 

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

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

 

Торговая логика эксперта CImpulse, работающего с отложенными ордерами 


Мы рассмотрели работу с отложенными ордерами и теперь можем создать полноценный эксперт, использующий возможности торгового движка по работе с ними. Наша стратегия будет основана на входах в моменты сильного движения, в направлении этого движения, поэтому она будет называться CImpulse. На открытии каждого нового бара будет отмеряться некоторое расстояние от текущей цены, выраженное в процентах. На одинаковом расстоянии от текущей цены будут выставляться отложенные ордера BuyStop и SellStop. Расстояние  задается в процентах. Если на протяжении одного бара произойдет исполнение одного из ордеров, то значит, цена в этот отрезок времени прошла достаточно большое расстояние, что сигнализирует о наличии импульса на рынке. Ордер будет исполнен и превратится в открытую позицию.

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


Рис. 1. Длинная позиция стратегии CImpulse.
 

На скриншоте видно, что в начале бара, отмеченного черным треугольником были выставлены два отложенных ордера на расстоянии 0.1% от цены открытия бара. Один из них — ордер BuyStop — сработал. Открылась новая длинная позиция. Как только цена закрытия одного из баров стала ниже скользящей средней, отображенной в виде красной линии, эксперт закрыл позицию. На рисунке 1 момент закрытия отображен в виде синего треугольника.

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

У описываемой стратегии существует одна особенность в работе с отложенными ордерами. Дело в том, что уровень BuyStop ордера может оказаться ниже текущего значения средней. В этом случае сразу после входа в позицию произойдет ее закрытие, т.к. текущая цена окажется ниже уровня средней. То же верно и в отношении короткой позиции: цена срабатывания SellStop ордера может оказаться выше уровня средней. Чтобы этого не происходило, в методах BuyInit и SellInit необходимо делать дополнительную проверку на уровень средней. Алгоритм будет выставлять отложенные BuyStop ордера только в том случае, если они выше скользящей средней. То же верно и для SellStop ордеров: они будут выставляться только в том случае, если будут находиться ниже средней.

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

Для работы на хеджируемом счете необходимо для начала его открыть — поставить соответствующий флаг в диалоговом окне "Открытие счета":

 

Рис. 2. Открытие хеджевого счета 

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

 

Класс стратегии CImpulse

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

//+------------------------------------------------------------------+
//|                                                      Impulse.mqh |
//|                                 Copyright 2016, Vasiliy Sokolov. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2016, Vasiliy Sokolov."
#property link      "http://www.mql5.com"
#include <Strategy\Strategy.mqh>
#include <Strategy\Indicators\MovingAverage.mqh>

input double StopPercent = 0.05;
//+------------------------------------------------------------------+
//| Определяет действие, которое необходимо совершать с отложенным   |
//| ордером.                                                         |
//+------------------------------------------------------------------+
enum ENUM_ORDER_TASK
{
   ORDER_TASK_DELETE,   // Удалить отложенный ордер
   ORDER_TASK_MODIFY    // Модифицировать отложенный ордер
};
//+------------------------------------------------------------------+
//| Стратегия CImpulse                                               |
//+------------------------------------------------------------------+
class CImpulse : public CStrategy
{
private:
   double            m_percent;        // Процент уровня отложенного ордера
   bool              IsTrackEvents(const MarketEvent &event);
protected:
   virtual void      InitBuy(const MarketEvent &event);
   virtual void      InitSell(const MarketEvent &event);
   virtual void      SupportBuy(const MarketEvent &event,CPosition *pos);
   virtual void      SupportSell(const MarketEvent &event,CPosition *pos);
   virtual void      OnSymbolChanged(string new_symbol);
   virtual void      OnTimeframeChanged(ENUM_TIMEFRAMES new_tf);
public:
   double            GetPercent(void);
   void              SetPercent(double percent);
   CIndMovingAverage Moving;
};
//+------------------------------------------------------------------+
//| Работа с отложенными ордерами BuyStop для открытия длинной       |
//| позиции                                                          |
//+------------------------------------------------------------------+
void CImpulse::InitBuy(const MarketEvent &event)
{
   if(!IsTrackEvents(event))return;                      
   if(positions.open_buy > 0) return;                    
   int buy_stop_total = 0;
   ENUM_ORDER_TASK task;
   double target = Ask() + Ask()*(m_percent/100.0);
   if(target < Moving.OutValue(0))                    // Цена срабатывания ордера должна быть выше скользящей средней
      task = ORDER_TASK_DELETE;
   else
      task = ORDER_TASK_MODIFY;
   for(int i = PendingOrders.Total()-1; i >= 0; i--)
   {
      CPendingOrder* Order = PendingOrders.GetOrder(i);
      if(Order == NULL || !Order.IsMain(ExpertSymbol(), ExpertMagic()))
         continue;
      if(Order.Type() == ORDER_TYPE_BUY_STOP)
      {
         if(task == ORDER_TASK_MODIFY)
         {
            buy_stop_total++;
            Order.Modify(target);
         }
         else
            Order.Delete();
      }
   }
   if(buy_stop_total == 0 && task == ORDER_TASK_MODIFY)
      Trade.BuyStop(MM.GetLotFixed(), target, ExpertSymbol(), 0, 0, NULL);
}
//+------------------------------------------------------------------+
//| Работа с отложенными ордерами SellStop для открытия короткой     |
//| позиции                                                          |
//+------------------------------------------------------------------+
void CImpulse::InitSell(const MarketEvent &event)
{
   if(!IsTrackEvents(event))return;                      
   if(positions.open_sell > 0) return;                    
   int sell_stop_total = 0;
   ENUM_ORDER_TASK task;
   double target = Bid() - Bid()*(m_percent/100.0);
   if(target > Moving.OutValue(0))                    // Цена срабатывания ордера должна быть выше скользящей средней
      task = ORDER_TASK_DELETE;
   else
      task = ORDER_TASK_MODIFY;
   for(int i = PendingOrders.Total()-1; i >= 0; i--)
   {
      CPendingOrder* Order = PendingOrders.GetOrder(i);
      if(Order == NULL || !Order.IsMain(ExpertSymbol(), ExpertMagic()))
         continue;
      if(Order.Type() == ORDER_TYPE_SELL_STOP)
      {
         if(task == ORDER_TASK_MODIFY)
         {
            sell_stop_total++;
            Order.Modify(target);
         }
         else
            Order.Delete();
      }
   }
   if(sell_stop_total == 0 && task == ORDER_TASK_MODIFY)
      Trade.SellStop(MM.GetLotFixed(), target, ExpertSymbol(), 0, 0, NULL);
}
//+------------------------------------------------------------------+
//| Сопровождение длинной позиции по скользящей средней Moving       |
//+------------------------------------------------------------------+
void CImpulse::SupportBuy(const MarketEvent &event,CPosition *pos)
{
   if(!IsTrackEvents(event))return;
   if(Bid() < Moving.OutValue(0))
      pos.CloseAtMarket();
}
//+------------------------------------------------------------------+
//| Сопровождение короткой позиции по скользящей средней Moving      |
//+------------------------------------------------------------------+
void CImpulse::SupportSell(const MarketEvent &event,CPosition *pos)
{
   if(!IsTrackEvents(event))return;
   if(Ask() > Moving.OutValue(0))
      pos.CloseAtMarket();
}
//+------------------------------------------------------------------+
//| Отфильтровывает поступающие события. Если переданное событие     |
//| не обрабатывается стратегией, возвращает ложь, если обрабатыва-  |
//| ется - возвращает истину.                                        |
//+------------------------------------------------------------------+
bool CImpulse::IsTrackEvents(const MarketEvent &event)
  {
//Обрабатываем только открытие нового бара на рабочем инструменте и таймфрейме
   if(event.type != MARKET_EVENT_BAR_OPEN)return false;
   if(event.period != Timeframe())return false;
   if(event.symbol != ExpertSymbol())return false;
   return true;
  }
//+------------------------------------------------------------------+
//| Реагируем на изменение символа                                   |
//+------------------------------------------------------------------+
void CImpulse::OnSymbolChanged(string new_symbol)
  {
   Moving.Symbol(new_symbol);
  }
//+------------------------------------------------------------------+
//| Реагируем на изменение таймфрейма                                |
//+------------------------------------------------------------------+
void CImpulse::OnTimeframeChanged(ENUM_TIMEFRAMES new_tf)
  {
   Moving.Timeframe(new_tf);
  }
//+------------------------------------------------------------------+
//| Возвращает процент пробойного уровня                             |
//+------------------------------------------------------------------+  
double CImpulse::GetPercent(void)
{
   return m_percent;
}
//+------------------------------------------------------------------+
//| Устанавливает процент пробойного уровня                          |
//+------------------------------------------------------------------+  
void CImpulse::SetPercent(double percent)
{
   m_percent = percent;
}

Стандартный файл эксперта mq5, конфигурирующий и запускающий эту стратегию в виде эксперта представлен ниже:

//+------------------------------------------------------------------+
//|                                                ImpulseExpert.mq5 |
//|                                 Copyright 2016, Vasiliy Sokolov. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2016, Vasiliy Sokolov."
#property link      "http://www.mql5.com"
#property version   "1.00"
#include <Strategy\StrategiesList.mqh>
#include <Strategy\Samples\Impulse.mqh>

CStrategyList Manager;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   CImpulse* impulse = new CImpulse();
   impulse.ExpertMagic(1218);
   impulse.Timeframe(Period());
   impulse.ExpertSymbol(Symbol());
   impulse.ExpertName("Impulse");
   impulse.Moving.MaPeriod(28);                      
   impulse.SetPercent(StopPercent);
   if(!Manager.AddStrategy(impulse))
      delete impulse;
//---
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   Manager.OnTick();
  }
//+------------------------------------------------------------------+

 

Анализ работы стратегии CImpulse

Для лучшего понимания работы алгоритма размещено специальное видео, демонстрирующее работу с отложенными ордерами в момент тестирования стратегии. Видно, что ордера BuyStop и SellStop переставляются в момент открытия нового бара на некоторое расстояние от текущей цены, образуя динамический коридор. Когда уровень отложенного ордера на покупку становится ниже средней линии, он удаляется вовсе. Однако когда цена срабатывания становится выше средней, отложенный ордер появляется вновь. То же правило работает в отношении SellStop-ордера:


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

 

Рис. 3. Сопровождение позиций на хеджируемом счете.

Та же самая логика торгового эксперта, исполненная на классическом типе счета, даст похожую, но немного различающуюся ситуацию:

 

Рис. 4. Сопровождение позиций на классическом нетто-счете. 

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

Мы можем немного изменить логику эксперта, сделав ее полиморфной, т.е., в зависимости от типа счета будет отрабатываться соответствующий алгоритм. Очевидно, что для нетто-счета позиция может быть только одна. Чтобы избежать ситуаций, когда открытие новой позиции закрывает предшествующую ей противоположную позицию, необходимо снабдить все нетто-позиции стоп-лоссом, чей уровень в точности соответствовал бы уровню пробойных ордеров противоположных направлений. Таким образом, срабатывание одного из Stop-ордеров будет означать срабатывание стоп-лосса для противоположной позиции, если она будет открыта к этому моменту.  Это логика будет включаться только на нетто-счетах. Ее необходимо разместить в методах BuySupport и SellSupport. Вот исходный код скорректированных методов поддержки стратегии:

//+------------------------------------------------------------------+
//| Сопровождение длинной позиции по скользящей средней Moving       |
//+------------------------------------------------------------------+
void CImpulse::SupportBuy(const MarketEvent &event,CPosition *pos)
{
   if(!IsTrackEvents(event))return;
   ENUM_ACCOUNT_MARGIN_MODE mode = (ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE);
   if(mode != ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
   {
      double target = Bid() - Bid()*(m_percent/100.0);
      if(target < Moving.OutValue(0))
         pos.StopLossValue(target);
      else
         pos.StopLossValue(0.0);
   }
   if(Bid() < Moving.OutValue(0))
      pos.CloseAtMarket();
}
//+------------------------------------------------------------------+
//| Сопровождение короткой позиции по скользящей средней Moving      |
//+------------------------------------------------------------------+
void CImpulse::SupportSell(const MarketEvent &event,CPosition *pos)
{
   if(!IsTrackEvents(event))return;
   ENUM_ACCOUNT_MARGIN_MODE mode = (ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE);
   if(mode != ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
   {
      double target = Ask() + Ask()*(m_percent/100.0);
      if(target > Moving.OutValue(0))
         pos.StopLossValue(target);
      else
         pos.StopLossValue(0.0);
   }
   if(Ask() > Moving.OutValue(0))
      pos.CloseAtMarket();
}

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


Рис. 5. Сопровождение позиций полиморфным экспертом на классическом нетто-счете.

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

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

 

Заключение 

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

Новые введенные методы в CStrategy позволяют легко и быстро получить доступ к текущим ценам типа Ask, Bid и Last. Переопределенный метод Digits теперь гарантированно возвращает правильное количество знаков рабочего инструмента. 

Работа с отложенными ордерами с помощью специальных классов CPendingOrders и COrdersEnvironment упрощает торговую логику. Эксперт получает доступ к отложенному ордеру через специальный объект типа CPendingOrder. Изменяя его свойства — например, уровень цены срабатывания ордера — он изменяет аналогичное свойство фактического ордера, соответствующего этому объекту. Модель объектов построена надежно. Невозможно получить доступ к объекту, которому бы не соответствовал ни один фактический отложенный ордер в системе. Работа с отложенными ордерами происходит в методах BuyInit и SellInit, которые требуется переопределить стратегии. В BuyInit необходимо работать только с отложенными ордерами типа BuyStop или BuyLimit. В SellInit необходимо работать только с ордерами SellStop или SellLimit.  

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