Кроссплатфоменный торговый советник: Менеджер ордеров

Enrico Lambino | 2 июня, 2017


Оглавление

Введение

Как уже обсуждалось в предыдущих статьях этой серии (1, 2, 3), MetaTrader 4 и MetaTrader 5 имеют некоторые различия, которые затрудняют простое копирование исходного файла MQL4 и его обработку компилятором MQL5. Одно из наиболее очевидных различий между платформами — в том, насколько по-разному они осуществляют торговые операции. В этой статье рассматривается вопрос создания класса COrderManager. В одиночку или вместе с другими, вспомогательными, классами он взял бы на себя выполнение торговых операций, а также обслуживание сделок, в которые вошел советник.


Цели

Менеджер ордеров из этой статьи будет способен выполнять следующие операции:

  1. Расчет размера лота
  2. Стоп-лосс и тейк-профит
  3. Разнообразные параметры, необходимые для входа в сделку (время истечения, комментарии, мэджики ордеров)
  4. Некоторые предварительные условия перед отправкой ордера
  5. Управление ордерами и историей ордеров

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

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

Прежде чем эксперт откроет на рынке сделку, нужно, чтобы сложились некоторые предварительные условия. Это торговые сигналы, наблюдаемые в условиях рынка, временные ограничения, а также максимальное количество активных сделок в определенный момент времени и максимальное число сделок, которое может быть открыто за весь период работы советника (между OnInit и OnDeinit). Для менеджера ордеров последние два условия будут включены в качестве окончательных перед отправкой ордера на рынок. Это будет реализовано так, чтобы во время работы советника не могли быть открыты одинаковые, дублирующие друг друга сделки. С другой стороны, остальные предварительные условия могут быть делегированы неким другим компонентам советника, помимо менеджера ордеров.


Базовая реализация

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

  1. Как различается отправка торговых запросов
  2. Как различается документирование торговых действий
  3. В MQL5 есть некоторые функции, у которых нет эквивалента в MQL

Здесь есть несколько моментов, которые различаются в MQL4 и MQL5. Рассмотрим функцию OrderSend (mql4, mql5), как показано в документации для обеих платформ:

(MQL4)

int  OrderSend(
   string   symbol,              // символ
   int      cmd,                 // операция
   double   volume,              // объем
   double   price,               // цена
   int      slippage,            // проскальзывание
   double   stoploss,            // стоп-лосс
   double   takeprofit,          // тейк-профит
   string   comment=NULL,        // комментарий
   int      magic=0,             // мэджик
   datetime expiration=0,        // срок истечения отложенного ордера
   color    arrow_color=clrNONE  // цвет
   );

(MQL5)

bool  OrderSend(
   MqlTradeRequest&  request,      // структура запроса
   MqlTradeResult&   result        // структура ответа
   );

Функция в MQL4 имеет более простой подход. Функция в MQL5 сложнее, зато в ней количество параметров уменьшено всего до двух, которые содержат данные (struct) о запросе и результате, соответственно. Это препятствие было по большей части описано в предыдущей статье, которая касалась импортирования некоторых компонентов стандартной библиотеки MQL5 в MQL4, в частности — классов CExpertTrade и CExpertTradeX. Таким образом, наш менеджер ордеров просто использует эти классы, чтобы обеспечить совместимость между двумя языками при осуществлении торгового запроса.

Другой аспект — способ, которым обрабатывается выход из сделки или отмена ордера в MetaTrader 4 и MetaTrader 5. Большой разницы в способе удаления отложенных ордеров нет, зато есть гигантское различие в способах, которыми рыночные ордера (MQL4) или позиции (MQL5) удаляются с рынка. В MQL4 закрытие рыночного ордера достигается путем вызова функции OrderClose. В MetaTrader 5 аналогичный эффект реализовывается вызовом функции PositionClose или отправкой торгового запроса по позиции с таким же объемом, что и текущая, но в противоположном направлении.

В MetaTrader 5 каждое торговое действие записывается. Вне зависимости от того, будет это вход в сделку, модификация или выход, это действие оставит свой след в торговой истории, и данные по нему будут доступны торговому советнику. В MetaTrader 4 это не так. К примеру, когда отложенному ордеру присваивается ID, зачастую этот же ID будет использоваться в течение всей жизни ордера, даже если была достигнута его цена срабатывания и он превратился в рыночный, прежде чем закрыться. Чтобы увидеть полное развитие какой-либо сделки, нужно обратиться в журнал эксперта, что может быть довольно трудоемкой задачей. Кроме того, файлы логов предназначены для чтения человеком, и в MQL4 нет встроенной функции, которая упрощала бы доступ к этой информации для эксперта.

В MQL5 есть некоторые функции, которые просто недоступны в MQL4. Пример этого — тип заполнения ордера. В MQL5 есть следующие опции заполнения запрошенного объема ордера

  • ORDER_FILLING_FOK — когда запрошенный объем не может быть исполнен, ордер отменяется
  • ORDER_FILLING_IOC — когда запрошенный объем не может быть исполнен, используется максимальный доступный объем, а оставшийся объем отменяется
  • ORDER_FILLING_RETURN — когда запрошенный объем не может быть исполнен, используется максимальный доступный объем. Ордер с оставшимся объемом остается на рынке.

В MetaTrader 4 торговый запрос просто либо заполняется, либо отменяется: по сути, это эквивалентно опции ORDER_FILLING_FOK. Другие два типа заполнения недоступны.

Как бы то ни было, эти политики заполнения ордеров реализуются, только если запрошенный объем выше, чем доступный объем на рынке. Такое случается нечасто, особенно в случае низкоризковых настроек и низкого баланса счета. Реализовать поведение ORDER_FILLING_IOC и ORDER_FILLING_RETURN в MQL4 сложно, если не сказать невозможно или непрактично. Основная причина этого — в том, что в советниках нет способа определить, какой объем доступен сейчас на рынке для определенного торгового запроса. Но даже если бы способ был, эта информация слишком волатильна, слишком часто изменяется.

Таким образом, чтобы гарантировать совместимость MQL4 и MQL5, ORDER_FILLING_FOK будет единственным используемым режимом (как и установлено по умолчанию в MetaTrader 5). Между тем, существуют события, когда эксперт рассчитывает размер лота для торгового запроса, превышающий SYMBOL_VOLUME_MAX — максимальный допустимый объем по любой сделке, установленный брокером. В MetaTrader 5 это реализуется автоматическим разбиением сделки на несколько отдельных, но такая возможность недоступна в MetaТrader 4 (в этом случае происходит просто отмена торгового запроса). Поэтому в кроссплатформенном эксперте лучше проверить соответствие запроса максимально допустимому объему заранее. Сделать это рекомендуется после того, как получен или рассчитан объем сделки, но перед отправкой торгового запроса для входа в рынок с использованием менеджера ордеров.

На нижеприведенном рисунке проиллюстрировано, как менеджер ордеров будет реализовывать вход в сделку:

Диаграмма входа

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

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

COrder* COrderManagerBase::TradeOpen(const string,ENUM_ORDER_TYPE)
  {
   return NULL;
  }

Расчет торгового объема

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

double COrderManagerBase::LotSizeCalculate(const double price,const ENUM_ORDER_TYPE type,const double stoploss)
  {
   if(CheckPointer(m_moneys))
      return m_moneys.Volume(m_symbol.Name(),0,type,stoploss);
   return m_lotsize;
  }

Метод проверяет указатель на экземпляр CMoneys, который выступает просто в роли контейнера для объектов манименеджмента, осуществляемого менеджером ордеров (точно так же, как и COrders является контейнером для экземпляров COrder). Эти объекты манименеджмента будут описаны в отдельной статье. На данном этапе работы достаточно знать, по крайней мере, что существует отдельный компонент, который рассчитывает размер лота, и что рассчитанный размер лота будет действительным. Если экземпляр для манименеджмента не был передан менеджеру ордеров, то он будет использовать просто размер лота по умолчанию через своего члена класса, m_lotsize.

Расчет стоп-лосса и тейк-профита

Стоп-лосс и тейк-профит рассчитываются методами StopLossCalculate и TakeProfitCalculate, соответтвенно. Нижеприведенные фрагменты кода показывают, как реализован каждый из этих методов в менеджере ордеров:

double COrderManagerBase::StopLossCalculate(const ENUM_ORDER_TYPE type,const double price)
  {
   if(CheckPointer(m_main_stop))
      return m_main_stop.StopLossTicks(type,price);
   return 0;
  }
double COrderManagerBase::TakeProfitCalculate(const ENUM_ORDER_TYPE type,const double price)
  {
   if(CheckPointer(m_main_stop))
      return m_main_stop.TakeProfitTicks(type,price);
   return 0;
  }

Расчет стоп-уровней делегируется отдельным объектам класса, которые будут членами менеджера ордеров (это тоже будет рассмотрено в отдельной статье). Стоп-объекты будут также иметь свою отдельную имплементацию для MetaTrader 4 и MetaTrader 5. Однако в случае, если указатель на стоп-объект менеджеру ордеров не передан, рассчитываемый стоп-лосс/тейк-профит будет по умолчанию равен нулю (то есть, не будет SL/TP).

Закрытие ордера или позиции

Нижеприведенная картинка иллюстрирует, как менеджер ордеров будет закрывать позиции и удалять ордера:

диаграмма для выхода

Как показано на рисунке, метод сначала проверяет, действителен ли указатель на экземпляр COrder. Потом он переходит к получению корректных экземпляров объектов символов и сделок, которые понадобятся для обработки запроса на выход. Затем он или удаляет, или закрывает ордер, в зависимости от его типа. После успешного закрытия или удаления ордера экземпляр тоже переносится из списка активных ордеров в список истории менеджера ордеров (архивируется). Метод также устанавливает флаги, которые отмечают объект как закрытый.

Проверка настроек

Проверка настроек менеджера ордеров осуществляется обращением метода Validate класса. Код этого метода продемонстрирован ниже:

bool COrderManagerBase::Validate(void) const
  {
   if(CheckPointer(m_moneys)==POINTER_DYNAMIC)
     {
      if(!m_moneys.Validate())
         return false;
     }
   if(CheckPointer(m_stops)==POINTER_DYNAMIC)
     {
      if(!m_stops.Validate())
         return false;
     }
   return true;
  }

Этот код подобен методу ValidationSettings, который часто присутствует в некоторых классах Стандартной библиотеки MQL5. Он просто вызывает методы Validate своих членов объекта и возвращает false каждый раз, когда проверка объекта не удается (в итоге приводя к отказу функции OnInit эксперта или возвращая INIT_FAILED). Метод предназначен для вызова во время выполнения функции Initialization советника.

Подсчет количества сделок

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

int COrderManagerBase::OrdersTotal(void) const
  {
   return m_orders.Total();
  }
int COrderManagerBase::OrdersHistoryTotal(void) const
  {
   return m_orders_history.Total();
  }
int COrderManagerBase::TradesTotal(void) const
  {
   return m_orders.Total()+m_orders_history.Total()+m_history_count;
  }
В обоих случаях при подсчете ордеров мы используем стандартную идеологию MetaЕrader 4 (каждый ордер, независимо от того, рыночный он или отложенный, — это одна позиция). К примеру, в реализации на MQL4 первые две строчки метода TradeOpen выглядят так:
int trades_total =TradesTotal();
int orders_total = OrdersTotal();

Важно отметить, что здесь OrdersTotal относится к методу, специфичному для класса, а не функции OrdersTotal, которая есть и в MQL4, и в MQL5. Если вместо этого использовать собственные функции языков, OrdersTotal должен вызываться оператором области видимости, предшествующим названию функции.

int orders_total = ::OrdersTotal();

Архивирование экземпляра COrder

Поскольку менеджер ордеров будет сохранять свой независимый список открытых сделок, формируя его аналогично Metatrader 4 и Metatrader 5 (но совместимо с обеими платформами), он должен иметь способ помечать экземпляры COrder как уже занесенные в историю. В зависимости от того, являются ли ордера историей либо они еще активны на рынке, они хранятся в экземплярах класса COrders m_orders или m_orders_history. Как следствие, кроссплатформенный советник для обеих версий должен будет проверять, не закрылась ли сделка или ордер.

Из-за различий в том, как платформы документируют открытые на рынке сделки, менеджер ордеров должен сохранять их в свой собственный, независимый список. После успешного открытия ордера создается экземпляр COrder, который в итоге добавляется в m_orders. Вскоре после того, как ордер или позиция выходит из рынка, менеджер ордеров должен переместить этот экземпляр в m_orders_history. Метод ArchiveOrder класса, который будет использоваться в обеих версиях, показан ниже:

bool COrderManagerBase::ArchiveOrder(COrder *order)
  {
   return m_orders_history.Add(order);
  }

MQL4-специфичная имплементация

Открытие ордера или позиции

Нижеприведенный фрагмент кода демонстрирует метод TradeOpen MQL4-специфичного наследника класса CorderManagerBase:

COrder* COrderManager::TradeOpen(const string symbol,ENUM_ORDER_TYPE type)
  {
   int trades_total = TradesTotal();
   int orders_total = OrdersTotal();
   m_symbol = m_symbol_man.Get(symbol);
   if (!CheckPointer(m_symbol))
      return NULL;
   if(!IsPositionAllowed(type))
      return NULL;
   if(m_max_orders>orders_total && (m_max_trades>trades_total || m_max_trades<=0))
     {
      ENUM_ORDER_TYPE ordertype = type;
      double price=PriceCalculate(ordertype);
      double sl=0,tp=0;
      if(CheckPointer(m_main_stop)==POINTER_DYNAMIC)
        {
         sl = m_main_stop.StopLossCustom()?m_main_stop.StopLossCustom(symbol,type,price):m_main_stop.StopLossCalculate(symbol,type,price);
         tp = m_main_stop.TakeProfitCustom()?m_main_stop.TakeProfitCustom(symbol,type,price):m_main_stop.TakeProfitCalculate(symbol,type,price);
        }
      double lotsize=LotSizeCalculate(price,type,sl);
      ulong ticket = SendOrder(type,lotsize,price,sl,tp);
      if (ticket>0)
      {
         if (OrderSelect((int)ticket,SELECT_BY_TICKET))
            return m_orders.NewOrder(OrderTicket(),OrderSymbol(),OrderMagicNumber(),(ENUM_ORDER_TYPE)::OrderType(),::OrderLots(),::OrderOpenPrice());
      }            
     }
   return NULL;
  }

Функция принимает два параметра — имя символа или инструмента и тип открываемого ордера. Она начинается с получения значений, необходимых для обработки запроса: общее число сделок, количество ордеров и объект символа по ордеру (показанный в первом аргументе метода).

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

Закрытие ордера или позиции

Нижеследующий фрагмент кода демонстрирует метод CloseOrder MQL4-специфичного наследника класса COrferManagerBase:

bool COrderManager::CloseOrder(COrder *order,const int index=-1)
  {
   bool closed=true;
   if(CheckPointer(order)==POINTER_DYNAMIC)
     {
      if(!CheckPointer(m_symbol) || StringCompare(m_symbol.Name(),order.Symbol())!=0)
         m_symbol=m_symbol_man.Get(order.Symbol());
      if(CheckPointer(m_symbol))
         m_trade=m_trade_man.Get(order.Symbol());
      if(order.Volume()>0)
        {
         if(order.OrderType()==ORDER_TYPE_BUY || order.OrderType()==ORDER_TYPE_SELL)
            closed=m_trade.OrderClose((ulong)order.Ticket());
         else
            closed=m_trade.OrderDelete((ulong)order.Ticket());
        }
      if(closed)
        {
         int idx = index>=0?index:FindOrderIndex(GetPointer(order));
         if(ArchiveOrder(m_orders.Detach(idx)))
           {
            order.Close();
            order.Volume(0);
           }
        }
     }
   return closed;
  }

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

Функция принимает два параметра — объект ордера и его индекс в списке активных ордеров/открытых позиций. Если указатель на объект класса Order действителен, мы получаем корректный экземпляр CExpertTradeX и CSymbolInfo, чтобы закрыть ордер, и затем помещаем его в историю торгового терминала путем вызова соответствующей функции.

Как только ордер или позиция закроется, объект COrder потребует обновления. Сначала он удаляется из списка активных ордеров, после чего передвигается в конец списка исторических ордеров. Затем объект помечается как закрытый, и его объем обнуляется.

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


MQL5-специфичная имплементация

Открытие ордера или позиции

Нижеследующий фрагмент кода демонстриурет метод TradeOpen MQL5-специфичного потомка COrderManagerBase:

COrder* COrderManager::TradeOpen(const string symbol,ENUM_ORDER_TYPE type)
  {
   double lotsize=0.0,price=0.0;
   int trades_total =TradesTotal();
   int orders_total = OrdersTotal();
   m_symbol=m_symbol_man.Get(symbol);
   if(!IsPositionAllowed(type))
      return NULL;
   if(m_max_orders>orders_total && (m_max_trades>trades_total || m_max_trades<=0))
     {
      price=PriceCalculate(type);
      lotsize=LotSizeCalculate(price,type,m_main_stop==NULL?0:m_main_stop.StopLossCalculate(symbol,type,price));
      if (SendOrder(type,lotsize,price,0,0))
         return m_orders.NewOrder((int)m_trade.ResultOrder(),m_trade.RequestSymbol(),    (int)m_trade.RequestMagic(),m_trade.RequestType(),m_trade.ResultVolume(),m_trade.ResultPrice());
     }      
   return NULL;
  }

Как мы можем видеть, имплементация не кардинально отличается от аналога на MQL4. Однако в этом коде есть одно ключевое отличие: нам не нужно получать значения для уровней стоп-лосса и тейк-профита. Причина этого кроется в том, что поведение стоп-уровней в MetaTrader 5 отличается от их поведения в платформе-предшественнике. Программист, у которого есть опыт работы с советниками, написанными на MQL5, догадается, что в этой библиотеке мы будем использовать отложенные ордера как аналог стоп-уровней в MQL4, устанавливаемых на стороне брокера.

Закрытие ордера или позиции

Нижеприведенный фрагмент кода демонстрирует метод СloseOrder MQL5-специфичного наследника класса COrderManagerBase:

bool COrderManager::CloseOrder(COrder *order,const int index=-1)   {    bool closed=true;    COrderInfo ord;    if(!CheckPointer(order))       return true;    if(order.Volume()<=0)       return true;    if(!CheckPointer(m_symbol) || StringCompare(m_symbol.Name(),order.Symbol())!=0)       m_symbol=m_symbol_man.Get(order.Symbol());    if(CheckPointer(m_symbol))       m_trade=m_trade_man.Get(order.Symbol());    if(ord.Select(order.Ticket()))    {       closed=m_trade.OrderDelete(order.Ticket());    }      else      {       ResetLastError();       if(IsHedging())       {          closed=m_trade.PositionClose(order.Ticket());       }         else         {          if(COrder::IsOrderTypeLong(order.OrderType()))             closed=m_trade.Sell(order.Volume(),0,0,0);          else if(COrder::IsOrderTypeShort(order.OrderType()))             closed=m_trade.Buy(order.Volume(),0,0,0);         }      }    if(closed)      {

      if(ArchiveOrder(m_orders.Detach(index)))         {          order.Close();          order.Volume(0);         }      }    return closed;   }

При закрытии ордера или позиции в MQL5 мы должны учитывать режим учета позиций (неттинг или хеджинг). Но сначала нам нужно определить, чем является закрываемая сущность — MQL5-ордером или позицией. Чтобы этого добиться, мы можем использовать функции OrderSelect и HistoryOrderSelect. Но чтобы сократить код этого метода и упростить процесс, мы просто используем класс COrderInfo из Стандартной библиотеки MQL5.

Ордер в MQL5 появляется как результат торгового запроса, что часто приводит к появлению сделки или набора сделок (примерно эквивалентно рыночному ордеру в MetaТrader 4). При этом, если запрос не выполняется в режиме Instant Execution, то он относится к отложенному ордеру (в противоположность ситуации в MetaТrader 4, где ордера могут быть рыночными или отложенными). Теперь, чтобы определить элемент для выхода из рынка, метод сначала проверяет, является ли он отложенным ордером, с использованием COrderInfo. Если это так, то отложенный ордер удаляется. Если проверка с помощью COrderInfo не удается, мы убеждаемся в том, что это рыночный ордер или позиция. Для режима хеджинга позиция просто закрывается с использованием функции PositionClose. В режиме неттинга мы просто "нейтрализуем" позицию, открывая противоположную с аналогичным объемом.

Создание экземпляра СOrder

Мы уже рассматривали, как менеджер ордеров открывает и закрывает сделки. В предыдущей статье мы уже знакомились со способом модификации класса CExpertTrade, так, чтобы он стал совместимым с обеими торговыми платформами. Теперь мы видим, что есть разница в том, как реализованы стоп-лосс и тейк-профит в обеих платформах, и что это с большим трудом обрабатывается менеджером ордеров. Остальная часть процесса касается инициализации экземпляра COrder, который вызывается методом NewOrder класса COrders. Ниже показан код метода Init класса COrdersBase:

COrder* COrdersBase::NewOrder(const ulong ticket,const string symbol,const int magic,const ENUM_ORDER_TYPE type,const double volume,const double price)
  {
   COrder *order=new COrder(ticket,symbol,type,volume,price);
   if(CheckPointer(order)==POINTER_DYNAMIC)
      if(InsertSort(GetPointer(order)))
      {  
         order.Magic(magic);
         order.Init(GetPointer(this),m_stops);
         return order;
      }  
   return NULL;
  }

Как мы можем увидеть, метод Init класса COrder принимает в качестве второго аргумента некий пользовательский объект (CStops). Это контейнер для стоп-объектов (таких, как ранее продемонстрированный m_main_stop). Этот объект класса будет рассмотрен в отдельной статье.

Модификация ордера или позиции

До сих пор мы еще ни разу не показали какого-либо кода, который дает возможность менеджеру ордеров модифицировать существующие позиции. Это можно делегировать другому стоп-объекту (CStop и CorderStop), который будет обсуждаться в отдельной статье. Такие объекты должны будут отвечать за любые обновления или изменения стоп-уровней позиции, а также за координацию с объектом класса COrder, к которому они относятся.

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

Пример

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

#include "MQLx\Base\OrderManager\OrderManagerBase.mqh"

Обратите внимание, что в этом объявлении мы используем кавычки вместо "<" и ">". Мы собираемся поместить библиотеку в тот же каталог, что и файл исходного кода советника.

Для этого советника нам потребуются по меньшей мере три указателя, которые должны быть глобально объявлены в программе: это COrderManager, CSymbolInfo и CSymbolManager:

COrderManager *order_manager;
CSymbolManager *symbol_manager;
CSymbolInfo *symbol_info;

Внутри функции OnInit мы должны будем инициализировать эти три указателя, особенно для экземпляра CSymbolInfo, которому требуется назначить определенное имя инструмента во время инициализации.

int OnInit()
  {
//---
   order_manager = new COrderManager();
   symbol_manager = new CSymbolManager();
   symbol_info = new CSymbolInfo();
   if (!symbol_info.Name(Symbol()))
   {
      Print("symbol not set");
      return (INIT_FAILED);
   }   
   symbol_manager.Add(GetPointer(symbol_info));   
   order_manager.Init(symbol_manager,NULL);
//---
   return(INIT_SUCCEEDED);
  }

Внутри функции OnDeinit нам нужно будет удалить эти три указателя, чтобы они не "съедали" память устройства (по крайней мере, внутри торговой платформы):

void OnDeinit(const int reason)
  {
//---
   delete symbol_info;
   delete symbol_manager;
   delete order_manager;
  }

В функции OnTick мы должны реализовать саму стратегию. Эксперт в этом примере будет использовать простой метод определения нового бара (по результатам проверки числа баров на графике). Количество предыдущих баров должно сохраняться в статической переменной (или в глобальной переменной). То же верно для переменной direction, которая будет использоваться для хранения предыдущего направления, в котором эксперт открыл сделку (или нуля, если сделка первая). Однако, поскольку функция подсчета баров на графике различается в двух версиях языка, мы должны разделить имплементацию следующим образом:

static int bars = 0;
static int direction = 0;
int current_bars = 0;
#ifdef __MQL5__
   current_bars = Bars(NULL,PERIOD_CURRENT);
#else 
   current_bars = Bars;
#endif

Для версии MQL4 мы просто используем предопределенную переменную Bars (можно также использовать обращение к функции iBars). С другой стороны, для версии MQL5 мы используем обращение к функции Bars.

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

if (bars<current_bars)
   {   
      symbol_info.RefreshRates();
      COrder *last = order_manager.LatestOrder();
      if (CheckPointer(last) && !last.IsClosed())
         order_manager.CloseOrder(last);
      if (direction<=0)
      {
         Print("Entering buy trade..");
         order_manager.TradeOpen(Symbol(),ORDER_TYPE_BUY,symbol_info.Ask());
         direction = 1;
      }
      else
      {
         Print("Entering sell trade..");
         order_manager.TradeOpen(Symbol(),ORDER_TYPE_SELL,symbol_info.Bid());
         direction = -1;
      }   
      bars = current_bars;
   }

Чтобы быть уверенными в том, что в начальной и во всех последующих версиях советника используется один и тот же код, мы переместим написанный к настоящему моменту код в заголовочный файл, а потом будем ссылаться на него в основном исходном файле (и в MQL4, и в MQL5-версиях). Оба исходных файла (test_ordermanager.mq4 или test_ordermanager.mq5, в зависимости от версии платформы) будут иметь одну строчку кода, ссылающуюся на главный заголовочный файл:

#include "test_ordermanager.mqh"

Нижеприведенные таблицы показывают результаты запуска советника в MetaTrader 4, а также в неттинговом и хеджинговом режиме MetaТrader 5, в по мере их появления в соответствующих отчетах Тестера стратегий. Для краткости, в статье приводятся только первые десять сделок (полный отчет вы можете найти в приложенных файлах в конце статьи).

MT4:

# Время Тип Ордер Объем Цена S / L T / P Прибыль Баланс
1. 2017.01.02 00:00 Buy 1. 0.10 1.05102 0.00000 0.00000
2 2017.01.02 01:00 Close 1. 0.10 1.05172 0.00000 0.00000 7.00 10007.00
3 2017.01.02 01:00 Sell 2 0.10 1.05172 0.00000 0.00000
4 2017.01.02 02:00 Filled 2 0.10 1.05225 0.00000 0.00000 -5.30 10001.70
5 2017.01.02 02:00 Buy 3 0.10 1.05225 0.00000 0.00000
6 2017.01.02 03:00 Close 3 0.10 1.05192 0.00000 0.00000 -3.30 9998.40
7 2017.01.02 03:00 Sell 4 0.10 1.05192 0.00000 0.00000
8 2017.01.02 04:00 Close 4 0.10 1.05191 0.00000 0.00000 0.10 9998.50
9 2017.01.02 04:00 Buy 5 0.10 1.05191 0.00000 0.00000
10 2017.01.02 05:00 Close 5 0.10 1.05151 0.00000 0.00000 -4.00 9994.50
11 2017.01.02 05:00 Sell 6 0.10 1.05151 0.00000 0.00000
12 2017.01.02 06:00 Close 6 0.10 1.05186 0.00000 0.00000 -3.50 9991.00
13 2017.01.02 06:00 Buy 7 0.10 1.05186 0.00000 0.00000
14 2017.01.02 07:00 Close 7 0.10 1.05142 0.00000 0.00000 -4.40 9986.60
15 2017.01.02 07:00 Sell 8 0.10 1.05142 0.00000 0.00000
16 2017.01.02 08:00 Close 8 0.10 1.05110 0.00000 0.00000 3.20 9989.80
17 2017.01.02 08:00 Buy 9 0.10 1.05110 0.00000 0.00000
18 2017.01.02 09:00 Close 9 0.10 1.05131 0.00000 0.00000 2.10 9991.90
19 2017.01.02 09:00 Sell 10 0.10 1.05131 0.00000 0.00000
20 2017.01.02 10:00 Close 10 0.10 1.05155 0.00000 0.00000 -2.40 9989.50


MT5 (netting):

Время открытия Ордер Символ Тип Объём Цена S / L T / P Время Состояние Комментарий
2017.01.02 00:00:00 2 EURUSD Buy 0.10 / 0.10 1.05140 2017.01.02 00:00:00 заполнение
2017.01.02 01:00:00 3 EURUSD Sell 0.10 / 0.10 1.05172 2017.01.02 01:00:00 заполнение
2017.01.02 01:00:00 4 EURUSD Sell 0.10 / 0.10 1.05172 2017.01.02 01:00:00 заполнение
2017.01.02 02:00:00 5 EURUSD Buy 0.10 / 0.10 1.05293 2017.01.02 02:00:00 заполнение
2017.01.02 02:00:00 6 EURUSD Buy 0.10 / 0.10 1.05293 2017.01.02 02:00:00 заполнение
2017.01.02 03:00:00 7 EURUSD Sell 0.10 / 0.10 1.05192 2017.01.02 03:00:00 заполнение
2017.01.02 03:00:00 8 EURUSD Sell 0.10 / 0.10 1.05192 2017.01.02 03:00:00 заполнение
2017.01.02 04:00:00 9 EURUSD Buy 0.10 / 0.10 1.05234 2017.01.02 04:00:00 заполнение
2017.01.02 04:00:00 10 EURUSD Buy 0.10 / 0.10 1.05234 2017.01.02 04:00:00 заполнение
2017.01.02 05:00:00 11 EURUSD Sell 0.10 / 0.10 1.05151 2017.01.02 05:00:00 заполнение
2017.01.02 05:00:00 12 EURUSD Sell 0.10 / 0.10 1.05151 2017.01.02 05:00:00 заполнение
2017.01.02 06:00:00 13 EURUSD Buy 0.10 / 0.10 1.05230 2017.01.02 06:00:00 заполнение
2017.01.02 06:00:00 14 EURUSD Buy 0.10 / 0.10 1.05230 2017.01.02 06:00:00 заполнение
2017.01.02 07:00:00 15 EURUSD Sell 0.10 / 0.10 1.05142 2017.01.02 07:00:00 заполнение
2017.01.02 07:00:00 16 EURUSD Sell 0.10 / 0.10 1.05142 2017.01.02 07:00:00 заполнение
2017.01.02 08:00:00 17 EURUSD Buy 0.10 / 0.10 1.05169 2017.01.02 08:00:00 заполнение
2017.01.02 08:00:00 18 EURUSD Buy 0.10 / 0.10 1.05169 2017.01.02 08:00:00 заполнение
2017.01.02 09:00:00 19 EURUSD Sell 0.10 / 0.10 1.05131 2017.01.02 09:00:00 заполнение
2017.01.02 09:00:00 20 EURUSD Sell 0.10 / 0.10 1.05131 2017.01.02 09:00:00 заполнение
2017.01.02 10:00:00 21 EURUSD Buy 0.10 / 0.10 1.05164 2017.01.02 10:00:00 заполнение


MT5 (hedging):














Время открытия Ордер Символ Тип Объём Цена S / L T / P Время Состояние Комментарий
2017.01.02 00:00:00 2 EURUSD Buy 0.10 / 0.10 1.05140 2017.01.02 00:00:00 заполнение
2017.01.02 01:00:00 3 EURUSD Sell 0.10 / 0.10 1.05172 2017.01.02 01:00:00 заполнение
2017.01.02 01:00:00 4 EURUSD Sell 0.10 / 0.10 1.05172 2017.01.02 01:00:00 заполнение
2017.01.02 02:00:00 5 EURUSD Buy 0.10 / 0.10 1.05293 2017.01.02 02:00:00 заполнение
2017.01.02 02:00:00 6 EURUSD Buy 0.10 / 0.10 1.05293 2017.01.02 02:00:00 заполнение
2017.01.02 03:00:00 7 EURUSD Sell 0.10 / 0.10 1.05192 2017.01.02 03:00:00 заполнение
2017.01.02 03:00:00 8 EURUSD Sell 0.10 / 0.10 1.05192 2017.01.02 03:00:00 заполнение
2017.01.02 04:00:00 9 EURUSD Buy 0.10 / 0.10 1.05234 2017.01.02 04:00:00 заполнение
2017.01.02 04:00:00 10 EURUSD Buy 0.10 / 0.10 1.05234 2017.01.02 04:00:00 заполнение
2017.01.02 05:00:00 11 EURUSD Sell 0.10 / 0.10 1.05151 2017.01.02 05:00:00 заполнение
2017.01.02 05:00:00 12 EURUSD Sell 0.10 / 0.10 1.05151 2017.01.02 05:00:00 заполнение
2017.01.02 06:00:00 13 EURUSD Buy 0.10 / 0.10 1.05230 2017.01.02 06:00:00 заполнение
2017.01.02 06:00:00 14 EURUSD Buy 0.10 / 0.10 1.05230 2017.01.02 06:00:00 заполнение
2017.01.02 07:00:00 15 EURUSD Sell 0.10 / 0.10 1.05142 2017.01.02 07:00:00 заполнение
2017.01.02 07:00:00 16 EURUSD Sell 0.10 / 0.10 1.05142 2017.01.02 07:00:00 заполнение
2017.01.02 08:00:00 17 EURUSD Buy 0.10 / 0.10 1.05169 2017.01.02 08:00:00 заполнение
2017.01.02 08:00:00 18 EURUSD Buy 0.10 / 0.10 1.05169 2017.01.02 08:00:00 заполнение
2017.01.02 09:00:00 19 EURUSD Sell 0.10 / 0.10 1.05131 2017.01.02 09:00:00 заполнение
2017.01.02 09:00:00 20 EURUSD Sell 0.10 / 0.10 1.05131 2017.01.02 09:00:00 заполнение
2017.01.02 10:00:00 21 EURUSD Buy 0.10 / 0.10 1.05164 2017.01.02 10:00:00 заполнение


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

PE      0       16:19:15.747    Trade   2017.01.02 01:00:00   instant sell 0.10 EURUSD at 1.05172, close #2 (1.05172 / 1.05237 / 1.05172)
GP      0       16:19:15.747    Trades  2017.01.02 01:00:00   deal #3 sell 0.10 EURUSD at 1.05172 done (based on order #3)
DS      0       16:19:15.747    Trade   2017.01.02 01:00:00   deal performed [#3 sell 0.10 EURUSD at 1.05172]

Обратите внимание на графу с надписью "close #2" справа в первой строке. Режим хеджинга указывает, какая конкретная сделка должна быть нейтрализована (закрыта). В неттинговом режиме в этом случае мы увидим только сообщение, подобное такому:

PG      0       16:20:51.958    Trade   2017.01.02 01:00:00   instant sell 0.10 EURUSD at 1.05172 (1.05172 / 1.05237 / 1.05172)
MQ      0       16:20:51.958    Trades  2017.01.02 01:00:00   deal #3 sell 0.10 EURUSD at 1.05172 done (based on order #3)
KN      0       16:20:51.958    Trade   2017.01.02 01:00:00   deal performed [#3 sell 0.10 EURUSD at 1.05172]

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

Обзор структуры

Класс COrderManager — один из наиболее сложных объектов, которые будут описаны в этой серии статей. Чтобы дать представление о том, как будет выглядеть окончательная версия менеджера ордеров с его членами объектов, рассмотрим диаграмму.

Обзор структуры менеджера ордеров

Проще говоря, менеджер ордеров будет содержать два экземпляра COrders (текущий и исторический), которые будут выполнять роль контейнеров ордеров, введенных в рынок менеджером ордеров (COrder). Каждый из этих ордеров может содержать и стоп-уровни (один или несколько), и каждый из этих уровней может иметь свои собственные методы трейлинга (один или несколько). Несмотря на то, что большинству советников не нужна такая степень сложности, в некоторых стратегиях, особенно тех, которые работают с множественными уровнями поддержки и сопротивления, такая структура будет полезной. Эти члены класса будут рассмотрены в последующих статьях серии.

Заключение

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