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

Vasiliy Sokolov | 10 августа, 2017

Оглавление

Введение

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

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

Второе изменение, описанное в настоящей статье, связано с введением процедуры полноценного управления отложенными ордерами. Если раньше отложенными ордерами приходилось управлять непосредственно в коде итоговой стратегии, то теперь часть таких функций делегировано движку CStrategy. Теперь итоговый класс-стратегия может переопределить методы SupportBuyPending и SupportSellPending и начать управлять отложенными ордерами, аналогично управлению активными позициями.

Доступ к индикаторам в предыдущих версиях CStrategy

Чтобы понять проблематику вопроса, обратимся к решению по работе с индикаторами из вышеупомянутой третьей статьи серии. В ней предлагалось работать с любым индикатором через класс-обертку. Так, например, для работы с индикатором iMA в примере использовался класс-обертка CIndMovingAverage. Сам класс CIndMovingAverage состоял из методов, устанавливающих либо возвращающих то или иное свойство индикатора, а также из метода Init, который вызывал системную функцию iMA. Класс индикатора хотя и имел простую структуру, но занимал достаточно большой объем кода, который необходимо было реализовать самому пользователю. Приведем исходный код этого класса, чтобы оценить объем работ, возлагаемый на него:

//+------------------------------------------------------------------+
//|                                                MovingAverage.mqh |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#include <Strategy\Message.mqh>
#include <Strategy\Logs.mqh>
//+------------------------------------------------------------------+
//| defines                                                          |
//+------------------------------------------------------------------+
class CIndMovingAverage
  {
private:
   int               m_ma_handle;         // Хэндл индикатора
   ENUM_TIMEFRAMES   m_timeframe;         // Таймфрейм
   int               m_ma_period;         // Период
   int               m_ma_shift;          // Смещение
   string            m_symbol;            // Инструмент
   ENUM_MA_METHOD    m_ma_method;         // Метод скользящей средней
   uint              m_applied_price;     // Хэндл индикатора, на котором требуется рассчитать значение Moving Average,
                                          // либо одно из ценовых значений ENUM_APPLIED_PRICE
   CLog*             m_log;               // Логирование
   void              Init(void);
public:
                     CIndMovingAverage(void);

/*Params*/
   void              Timeframe(ENUM_TIMEFRAMES timeframe);
   void              MaPeriod(int ma_period);
   void              MaShift(int ma_shift);
   void              MaMethod(ENUM_MA_METHOD method);
   void              AppliedPrice(int source);
   void              Symbol(string symbol);

   ENUM_TIMEFRAMES   Timeframe(void);
   int               MaPeriod(void);
   int               MaShift(void);
   ENUM_MA_METHOD    MaMethod(void);
   uint              AppliedPrice(void);
   string            Symbol(void);

/*Out values*/
   double            OutValue(int index);
  };
//+------------------------------------------------------------------+
//| Конструктор по умолчанию                                         |
//+------------------------------------------------------------------+
CIndMovingAverage::CIndMovingAverage(void) : m_ma_handle(INVALID_HANDLE),
                                             m_timeframe(PERIOD_CURRENT),
                                             m_ma_period(12),
                                             m_ma_shift(0),
                                             m_ma_method(MODE_SMA),
                                             m_applied_price(PRICE_CLOSE)
  {
   m_log=CLog::GetLog();
  }
//+------------------------------------------------------------------+
//| Инициализация                                                    |
//+------------------------------------------------------------------+
CIndMovingAverage::Init(void)
  {
   if(m_ma_handle!=INVALID_HANDLE)
     {
      bool res=IndicatorRelease(m_ma_handle);
      if(!res)
        {
         string text="Realise iMA indicator failed. Error ID: "+(string)GetLastError();
         CMessage *msg=new CMessage(MESSAGE_WARNING,__FUNCTION__,text);
         m_log.AddMessage(msg);
        }
     }
   m_ma_handle=iMA(m_symbol,m_timeframe,m_ma_period,m_ma_shift,m_ma_method,m_applied_price);
   if(m_ma_handle==INVALID_HANDLE)
     {
      string params="(Period:"+(string)m_ma_period+", Shift: "+(string)m_ma_shift+
                    ", MA Method:"+EnumToString(m_ma_method)+")";
      string text="Create iMA indicator failed"+params+". Error ID: "+(string)GetLastError();
      CMessage *msg=new CMessage(MESSAGE_ERROR,__FUNCTION__,text);
      m_log.AddMessage(msg);
     }
  }
//+------------------------------------------------------------------+
//| Установка таймфрейма                                             |
//+------------------------------------------------------------------+
void CIndMovingAverage::Timeframe(ENUM_TIMEFRAMES tf)
  {
   m_timeframe=tf;
   if(m_ma_handle!=INVALID_HANDLE)
      Init();
  }
//+------------------------------------------------------------------+
//| Возвращает текущий таймфрейм                                     |
//+------------------------------------------------------------------+
ENUM_TIMEFRAMES CIndMovingAverage::Timeframe(void)
  {
   return m_timeframe;
  }
//+------------------------------------------------------------------+
//| Устанавливает период усреднения скользящей средней               |
//+------------------------------------------------------------------+
void CIndMovingAverage::MaPeriod(int ma_period)
  {
   m_ma_period=ma_period;
   if(m_ma_handle!=INVALID_HANDLE)
      Init();
  }
//+------------------------------------------------------------------+
//| Возвращает текущий период усреднения скользящей средней          |
//+------------------------------------------------------------------+
int CIndMovingAverage::MaPeriod(void)
  {
   return m_ma_period;
  }
//+------------------------------------------------------------------+
//| Устанавливает тип скользящей средней                             |
//+------------------------------------------------------------------+
void CIndMovingAverage::MaMethod(ENUM_MA_METHOD method)
  {
   m_ma_method=method;
   if(m_ma_handle!=INVALID_HANDLE)
      Init();
  }
//+------------------------------------------------------------------+
//| Возвращает тип скользящей средней                                |
//+------------------------------------------------------------------+
ENUM_MA_METHOD CIndMovingAverage::MaMethod(void)
  {
   return m_ma_method;
  }
//+------------------------------------------------------------------+
//| Возвращает смещение скользящей средней                           |
//+------------------------------------------------------------------+
int CIndMovingAverage::MaShift(void)
  {
   return m_ma_shift;
  }
//+------------------------------------------------------------------+
//| Устанавливает смещение скользящей средней                        |
//+------------------------------------------------------------------+
void CIndMovingAverage::MaShift(int ma_shift)
  {
   m_ma_shift=ma_shift;
   if(m_ma_handle!=INVALID_HANDLE)
      Init();
  }
//+------------------------------------------------------------------+
//| Устанавливает тип цены, для которого рассчитывается средняя      |
//+------------------------------------------------------------------+
void CIndMovingAverage::AppliedPrice(int price)
  {
   m_applied_price = price;
   if(m_ma_handle != INVALID_HANDLE)
      Init();
  }
//+------------------------------------------------------------------+
//| Возвращает тип цены, для которого рассчитывается средняя         |
//+------------------------------------------------------------------+
uint CIndMovingAverage::AppliedPrice(void)
  {
   return m_applied_price;
  }
//+------------------------------------------------------------------+
//| Устанавливает символ, на котором должен быть рассчитан индикатор |
//+------------------------------------------------------------------+
void CIndMovingAverage::Symbol(string symbol)
  {
   m_symbol=symbol;
   if(m_ma_handle!=INVALID_HANDLE)
      Init();
  }
//+------------------------------------------------------------------+
//| Возвращает символ, для которого рассчитывается индикатор         |
//+------------------------------------------------------------------+
string CIndMovingAverage::Symbol(void)
  {
   return m_symbol;
  }
//+------------------------------------------------------------------+
//| Возвращает значение скользящей средней по индексу index          |
//+------------------------------------------------------------------+
double CIndMovingAverage::OutValue(int index)
  {
   if(m_ma_handle==INVALID_HANDLE)
      Init();
   double values[];
   if(CopyBuffer(m_ma_handle,0,index,1,values))
      return values[0];
   return EMPTY_VALUE;
  }

Согласитесь, объем кода впечатляет. А ведь это — только пример одного, не самого сложного индикатора. Данный подход усложняется и тем, для MetaTrader созданы сотни различных индикаторов — как стандартных, так и пользовательских. Каждый из них имеет свой, относительно уникальный набор свойств и параметров. Если следовать предложенному подходу, нужно для каждого такого индикатора писать свою собственную обертку. 

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

Функция IndicatorCreate — основа универсального интерфейса

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

Практика использования показала, что наиболее простой, а главное — универсальный доступ к индикатору обычно происходит через функции iCustom и IndicatorCreate.

Обе функции позволяют создавать как стандартные, так и пользовательские индикаторы. Функция iCustom требует указания фактических параметров индикатора в момент компиляции программы. Функция IndicatorCreate устроена иначе. В качестве параметров она принимает массив структур MqlParams. Именно благодаря формату второй функции становится возможным создать обобщенную процедуру доступа к произвольному индикатору. При этом знать параметры индикатора заранее не требуется, за счет чего процедура доступа становится действительно универсальной.

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

//+------------------------------------------------------------------+
//|                                                        Test2.mq5 |
//|                                 Copyright 2017, Vasiliy Sokolov. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#property version   "1.00"

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
{
   // Получаем хэндл индикатора с помощью iMA
   int h_ima = iMA(Symbol(), Period(), 13, 0, MODE_SMA, PRICE_CLOSE); 
   // Получаем этот же хэндл индикатора с помощью IndicatorCreate
   MqlParam ma_params_1[4];
   ma_params_1[0].type = TYPE_INT;
   ma_params_1[0].integer_value = 13;
   ma_params_1[1].type = TYPE_INT;
   ma_params_1[1].integer_value = 0;
   ma_params_1[2].type = TYPE_INT;
   ma_params_1[2].integer_value = MODE_SMA;
   ma_params_1[3].type = TYPE_INT;
   ma_params_1[3].integer_value = PRICE_CLOSE;
   int h_ma_c = IndicatorCreate(Symbol(), Period(), IND_MA, 4, ma_params_1);
   if(h_ima == h_ma_c)
      printf("Хэндлы индикаторов равны");
   else
      printf("Хэндлы индикаторов не равны");
}

Представленный скрипт получает доступ к одному и тому же индикатору через два разных интерфейса: функции iMA и IndicatorCreate. Обе функции возвращают один и тот же хэндл, в чем легко убедиться: запуск индикатора выведет сообщение "Хэндлы индикаторов равны". Однако доступ к индикатору через IndicatorCreate сопряжен с утомительным конфигурированием массива MqlParams. Каждому элементу MqlParam требуется задать два свойства: тип переменной и значение этой переменной. Во многом из-за этой громоздкости функция IndicatorCreate используется нечасто. Однако именно этот интерфейс вызова позволяет получить доступ к абсолютно любому индикатору MQL. Поэтому его мы и будем использовать.

CUnIndicator — универсальный индикатор CStrategy

Благодаря объектно-ориентированному программированию мы можем скрыть большую часть конфигурирования элементов массива MqlParams от пользователя, предоставив ему удобный интерфейс для установки произвольного параметра. Создадим CUnIndicator — класс-обертку над функцией IndicatorCreate. С его помощью можно будет  последовательно задавать произвольное количество параметров для индикатора. При этом тип того или иного параметра указывать будет не нужно. Благодаря шаблонам, переданный тип будет определяться автоматически. Также наш класс будет иметь удобные индексаторы в виде квадратных скобок '[]', внутри которых можно будет указать как индекс значения индикатора, так и время, на момент которого необходимо получить это значение. 

Работа с CUnIndicator будет сводится к следующим этапам.

Напишем небольшой скрипт, создающий индикатор Moving Average с помощью CUnIndicator:

//+------------------------------------------------------------------+
//|                                                        Test2.mq5 |
//|                                 Copyright 2017, Vasiliy Sokolov. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Strategy\Indicators.mqh>
CUnIndicator UnMA;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
{
   // Получаем хэндл индикатора с помощью iMA
   int h_ima = iMA(Symbol(), Period(), 13, 0, MODE_SMA, PRICE_CLOSE); 
   // Получаем хэндл индикатора с помощью CUnIndicator
   UnMA.SetParameter(13);
   UnMA.SetParameter(0);
   UnMA.SetParameter(MODE_SMA);
   int handle = UnMA.Create(Symbol(), Period(), "Examples\\Custom Moving Average");
}

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

double value = UnMA[1];

Разберем более сложный пример. Создадим индикатор, содержащий несколько буферов — например, стандартный MACD. Каждую строку снабдим подробными комментариями:

//+------------------------------------------------------------------+
//|                                                        Test2.mq5 |
//|                                 Copyright 2017, Vasiliy Sokolov. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Strategy\Indicators.mqh>
input int FastEMA = 12;
input int SlowEMA = 26;
input int SignalSMA = 9;

CUnIndicator UnMACD;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
{
   UnMACD.SetParameter(FastEMA);                            // Установим значение быстрой скользящей средней
   UnMACD.SetParameter(SlowEMA);                            // Установим значение медленной скользящей средней
   UnMACD.SetParameter(SignalSMA);                          // Установим период сигнальной линии
   int handle = UnMACD.Create(Symbol(), Period(),           // Создадим индикатор, задав символ и таймфрейм для него
                "Examples\\MACD", PRICE_CLOSE);
   UnMACD.SetBuffer(MAIN_LINE);                             // Установим буфер по умолчанию - значения MACD
   double macd = UnMACD[1];                                 // Получим значение MACD на предыдущем баре
   UnMACD.SetBuffer(SIGNAL_LINE);                           // Установим буфером по умолчанию сигнальную линию
   double signal = UnMACD[1];                               // Получим значение сигнальной линии на предыдущем баре
   datetime time_span = TimeCurrent() - PeriodSeconds();    // Рассчитаем время открытия предыдущего бара
   double signal_by_time = UnMACD[time_span];               // Получим значения сигнальной линии для времени
   printf("MACD: " + DoubleToString(macd, Digits()) +       // Выведем значения MACD и сигнальной линии на предыдущем баре
         "; Signal: " + DoubleToString(signal, Digits()));
   if(signal == signal_by_time)                             // Доступ по времени и индексу будет совпадать
      printf("значения, взятые по индексу и времени, равны ");
}
//+------------------------------------------------------------------+

Самый интересный момент в этом примере — переключение внутренних буферов индикатора с помощью метода SetBuffer. Так, значение, возвращаемое UnMACD[1], будет различаться в зависимости от того, какой буфер установлен в текущий момент. В первый раз UnMACD[1] возвращает значения MACD на предыдущем баре. Однако если в качестве буфера по умолчанию установить SIGNAL_LINE, UnMACD[1] возвращает уже значение сигнальной линии.

Доступ к значениям индикатора возможен и по индексу, и по времени. В примере рассчитывается время time_span, равное открытию предыдущего бара. Если вместо индекса UnMACD указать это время, то будет возвращено то же значение, что и при UnMACD[1].

Внутреннее устройство CUnIndicator

Настало время разобрать внутреннее устройство CUnIndicator. Приведем исходный код этого класса полностью:

//+------------------------------------------------------------------+
//|                                                   Indicators.mqh |
//|                                 Copyright 2017, Vasiliy Sokolov. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#include "Message.mqh"
#include "Logs.mqh"
//+------------------------------------------------------------------+
//| Базовый класс индикатора                                         |
//+------------------------------------------------------------------+
class CUnIndicator
{
private:
   MqlParam m_params[];
   int      m_params_count;
   int      m_current_buffer;
   int      m_handle;
   static   CLog*    Log;
   bool     m_invalid_handle;
   void     PushName(string name);
public:
            CUnIndicator(void);
   void     SetBuffer(int index);
   template <typename T>
   bool     SetParameter(T value);
   int      Create(string symbol, ENUM_TIMEFRAMES period, string name);
   int      Create(string symbol, ENUM_TIMEFRAMES period, string name, int app_price);
   int      Create(string symbol, ENUM_TIMEFRAMES period, ENUM_INDICATOR ind_type);
   int      Create(string symbol, ENUM_TIMEFRAMES period, ENUM_INDICATOR ind_type, int app_price);
   void     InitByHandle(int handle);
   void     IndicatorRelease(void);
   double   operator[](int index);
   double   operator[](datetime time);
   int      GetHandle(void);
};
CLog        *CUnIndicator::Log;
//+------------------------------------------------------------------+
//| Инициализация без указания имени                                 |
//+------------------------------------------------------------------+
CUnIndicator::CUnIndicator(void) : m_params_count(0),
                                   m_handle(INVALID_HANDLE),
                                   m_current_buffer(0),
                                   m_invalid_handle(false)
{
   Log = CLog::GetLog(); 
}

//+------------------------------------------------------------------+
//| Деинициализация индикатора                                       |
//+------------------------------------------------------------------+
CUnIndicator::IndicatorRelease(void)
{
   if(m_handle != INVALID_HANDLE)
      IndicatorRelease(m_handle);
   ArrayResize(m_params, 1);
   m_params_count = 1;
   m_current_buffer = 0;
   m_handle = INVALID_HANDLE;
}

template <typename T>
bool CUnIndicator::SetParameter(T value)
{
   
   string type = typename(value);
   MqlParam param;
   if(type == "string")
   {
      param.type = TYPE_STRING;
      param.string_value = (string)value;
   }
   else if(type == "int")
   {
      param.type = TYPE_INT;
      param.integer_value = (long)value;
   }
   else if(type == "double")
   {
      param.type = TYPE_DOUBLE;
      param.double_value = (double)value;
   }
   else if(type == "bool")
   {
      param.type = TYPE_BOOL;
      param.integer_value = (int)value;
   }
   else if(type == "datetime")
   {
      param.type = TYPE_DATETIME;
      param.integer_value = (datetime)value;
   }
   else if(type == "color")
   {
      param.type = TYPE_COLOR;
      param.integer_value = (color)value;
   }
   else if(type == "ulong")
   {
      param.type = TYPE_ULONG;
      param.integer_value = (long)value;
   }
   else if(type == "uint")
   {
      param.type = TYPE_UINT;
      param.integer_value = (uint)value;
   }
   else
   {
      param.type = TYPE_INT;
      param.integer_value = (int)value;
   }
   m_params_count++;
   if(ArraySize(m_params) < m_params_count)
      ArrayResize(m_params, m_params_count);
   m_params[m_params_count-1].double_value = param.double_value;
   m_params[m_params_count-1].integer_value = param.integer_value;
   m_params[m_params_count-1].string_value = param.string_value;
   m_params[m_params_count-1].type = param.type;
   return true;
}
//+------------------------------------------------------------------+
//| Возвращает хэндл индикатора                                      |
//+------------------------------------------------------------------+
int CUnIndicator::GetHandle(void)
{
   return m_handle;
}
//+------------------------------------------------------------------+
//| Устанавливает текущий буфер индикатора                           |
//+------------------------------------------------------------------+
void CUnIndicator::SetBuffer(int index)
{
   m_current_buffer = index;
}
//+------------------------------------------------------------------+
//| Инициализирует индикатор (создает его хэндл). Возвращает хэндл   |
//| индикатора в случае успеха, либо возвращает INVALID_HANDLE,      |
//| если индикатор не удалось создать                                |
//+------------------------------------------------------------------+
int CUnIndicator::Create(string symbol, ENUM_TIMEFRAMES period, string name)
{
   PushName(name);
   m_handle = IndicatorCreate(symbol, period, IND_CUSTOM, m_params_count, m_params);
   if(m_handle == INVALID_HANDLE && m_invalid_handle == false)
   {
      string text = "CUnIndicator '" + m_params[0].string_value + "' was not created. Check it's params. Last error:" + (string)GetLastError();
      CMessage* msg = new CMessage(MESSAGE_ERROR, __FUNCTION__, text);
      Log.AddMessage(msg);
      m_invalid_handle = true;
   }
   return m_handle;
}
//+------------------------------------------------------------------+
//| Инициализирует индикатор (создает его хэндл). Возвращает хэндл   |
//| индикатора в случае успеха, либо возвращает INVALID_HANDLE,      |
//| если индикатор не удалось создать                                |
//+------------------------------------------------------------------+
int CUnIndicator::Create(string symbol, ENUM_TIMEFRAMES period, ENUM_INDICATOR ind_type)
{
   if(ind_type == IND_CUSTOM)
   {
      string text = "CUnIndicator '" + m_params[0].string_value + "' was not created. Indicator type can not be IND_CUSTOM";
      CMessage* msg = new CMessage(MESSAGE_ERROR, __FUNCTION__, text);
      Log.AddMessage(msg);
      m_invalid_handle = true;
      return INVALID_HANDLE;
   }
   m_handle = IndicatorCreate(symbol, period, ind_type, m_params_count, m_params);
   if(m_handle == INVALID_HANDLE && m_invalid_handle == false)
   {
      string text = "CUnIndicator '" + m_params[0].string_value + "' was not created. Check it's params. Last error:" + (string)GetLastError();
      CMessage* msg = new CMessage(MESSAGE_ERROR, __FUNCTION__, text);
      Log.AddMessage(msg);
      m_invalid_handle = true;
   }
   return m_handle;
}
//+------------------------------------------------------------------+
//| Инициализирует индикатор (создает его хэндл). Возвращает хендл   |
//| индикатора в случае успеха, либо возвращает INVALID_HANDLE в     |
//| случае если индикатор не удалось создать                         |
//+------------------------------------------------------------------+
int CUnIndicator::Create(string symbol,ENUM_TIMEFRAMES period,ENUM_INDICATOR ind_type,int app_price)
{
   SetParameter(app_price);
   return Create(symbol, period, ind_type);
}
//+------------------------------------------------------------------+
//| Размещает имя индикатора по нулевому индексу массива m_params[]  |
//+------------------------------------------------------------------+
void CUnIndicator::PushName(string name)
{
   int old_size = ArraySize(m_params);
   int size = ArrayResize(m_params, ArraySize(m_params) + 1);
   for(int i = 0; i < old_size; i++)
      m_params[i+1] = m_params[i];
   m_params[0].type = TYPE_STRING;
   m_params[0].string_value = name;
}
//+------------------------------------------------------------------+
//| Инициализирует индикатор (создает его хэндл). Возвращает хендл   |
//| индикатора в случае успеха, либо возвращает INVALID_HANDLE,      |
//| если индикатор не удалось создать                                |
//+------------------------------------------------------------------+
int CUnIndicator::Create(string symbol, ENUM_TIMEFRAMES period, string name, int app_price)
{
   SetParameter(app_price);
   return Create(symbol, period, name);
}
//+------------------------------------------------------------------+
//| Инициализирует класс индикатора на основе уже существующего      |
//| хэндла индикатора                                                |
//+------------------------------------------------------------------+
void CUnIndicator::InitByHandle(int handle)
{
   this.IndicatorRelease();
   m_handle = handle;
}
//+------------------------------------------------------------------+
//| Возвращает значение индикатора по индексу 'index'                |
//+------------------------------------------------------------------+
double CUnIndicator::operator[](int index)
{
   double values[];
   if(m_handle == INVALID_HANDLE)
      return EMPTY_VALUE;
   if(CopyBuffer(m_handle, m_current_buffer, index, 1, values) == 0)
   {
      string text = "Failed copy buffer of indicator. Last error: " + (string)GetLastError();
      CMessage* msg = new CMessage(MESSAGE_ERROR, __FUNCTION__, text);
      Log.AddMessage(msg);
      return EMPTY_VALUE;
   }
   return values[0];
}
//+------------------------------------------------------------------+
//| Возвращает значение индикатора на момент времени 'time'          |
//+------------------------------------------------------------------+
double CUnIndicator::operator[](datetime time)
{
   double values[];
   if(m_handle == INVALID_HANDLE)
      return EMPTY_VALUE;
   
   if(CopyBuffer(m_handle, m_current_buffer, time, 1, values) == 0)
   {
      string text = "Failed copy buffer of indicator. Last error: " + (string)GetLastError();
      CMessage* msg = new CMessage(MESSAGE_ERROR, __FUNCTION__, text);
      Log.AddMessage(msg);
      return EMPTY_VALUE;
   }
   return values[0];
}

Из листинга кода видно, что метод SetParameter — шаблонный. Он принимает на вход универсальный аргумент T, в зависимости от типа которого выбирается нужный тип параметра ENUM_DATATYPE. Этот параметр устанавливается для структуры MqlParam. Множественные строковые проверки неоптимальны с точки зрения скорости, однако на производительности это не сказывается, так как функция SetParameter должна вызываться только один раз — в момент инициализации самого эксперта.

Класс снабжен многочисленными вариантами метода Create. Благодаря этому можно создавать как пользовательские индикаторы (указывая при этом строковое имя индикатора), так и стандартные индикаторы, тип которых можно задать через INDICATOR_TYPE. Например, создать скользящую среднюю как пользовательский индикатор можно так:

UnMA.Create(Symbol(), Period(), "Examples\\Custom Moving Average");

Здесь UnMA — экземпляр CUnIndicator. Создание того же индикатора в виде стандартного происходит чуть иначе:

UnMA.Create(Symbol(), Period(), IND_MA);

Также класс CUnIndicator содержит метод InitByHandle. Остановимся на нем подробнее. Как известно, многие индикаторы могут быть рассчитаны не только на ценах инструмента, но и на данных другого индикатора. Благодаря этому, можно создать цепочку индикаторов, рассчитывающих свои значения на выходных значениях предыдущего. Предположим, что надо рассчитать значения Stochastic на скользящей средней. Для этого необходимо инициализировать два индикатора: один для расчета скользящей средней, другой — для расчета стохастика. Сделать это можно следующим образом:

//+------------------------------------------------------------------+
//|                                                        Test3.mq5 |
//|                                 Copyright 2017, Vasiliy Sokolov. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Strategy\Indicators.mqh>
input int MaPeriod = 12;
input int StochK = 3;
input int StochD = 12;
input int StochSmoth = 3;

CUnIndicator UnSMA;
CUnIndicator UnStoch;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
{
   // Задаем параметры и создаем индикатор SMA
   UnSMA.SetParameter(12);
   UnSMA.SetParameter(0);
   UnSMA.SetParameter(MODE_SMA);
   UnSMA.SetParameter(PRICE_CLOSE);
   int sma_h = UnSMA.Create(Symbol(), Period(),
      "\\Examples\Custom Moving Average");
   // Задаем параметры и создаем индикатор Stoch,
   // рассчитанных на данных SMA
   UnStoch.SetParameter(StochK);
   UnStoch.SetParameter(StochD);
   UnStoch.SetParameter(StochSmoth);
   UnStoch.InitByHandle(sma_h);
   double stoch_on_sma = UnStoch[1];
}
//+------------------------------------------------------------------+

Из приведенного кода видно, что хэндл индикатора, созданный методом Create, запоминается и используется для создания индикатора Stochastic. В случае, когда используется пользовательский индикатор, источник данных указывать необязательно. Однако для системных индикаторов такой источник необходимо указать. Сделать это можно двумя способами: через метод SetParameter:

UnMA.SetParameter(PRICE_CLOSE);

А также через перегруженную версию метода Create:

UnMA.Create(Symbol(), Period(), IND_MA, PRICE_CLOSE);

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

Улучшенная работа с отложенными ордерами

В одной из предыдущих статей, посвященной CStrategy, был представлен объектно-ориентированный класс CPendingOrders, представляющий отложенный ордер в рамках CStrategy. CPendingOrders представляет собой класс-интерфейс. В нем нет никаких внутренних членов, кроме поля, хранящего тикет ордера. Все его методы получают соответствующие свойства через вызов трех основных системных функций — OrderGetInterer, OrderGetDouble и OrderGetString. Такая организация позволяет гарантировать целостность представления данных. Каждому отложенному ордеру в MetaTrader 5 соответствует экземпляр CPendingOrders, тикет которого равен этому реальному ордеру. Если отложенный ордер по каким-то причинам отменяется (экспертом или со стороны пользователя), то торговый движок CStrategy удаляет соответствующий экземпляр класса CPendingOrders из списка отложенных ордеров. Сам список CPendingOrders хранится в виде специального класса  COrdersEnvironment. В каждой стратегии есть свой единственный экземпляр COrdersEnvironment, называемый PendingOrders. Стратегия могла напрямую обратиться к этому объекту и выбрать из него необходимый ей отложенный ордер по индексу. 

Если стратегии необходимо было вместо рыночного ордера открыть отложенный, то она просто посылала соответствующий торговый приказ через модуль CTrade. В этом отношении установка отложенного ордера ничем не отличалась от установки рыночного аналога. Но дальше начинались различия, которые в CStrategy не учитывались. CStrategy устроен таким образом, что каждая рыночная позиция передается коду-обработчику по очереди. Так, у позиций типа POSITION_TYPE_BUY таким обработчиком является метод SupportBuy, а у позиции типа POSITION_TYPE_SELL — метод SupportSell. С отложенными ордерами все было иначе.  Каждый такой ордер попадал в доступную для эксперта коллекцию PendingOrders, однако своего собственного обработчика такие ордера не имели. Подразумевалось, что отложенные ордера должны быть обработаны где-то еще, например в OnEvent, BuySupport/SellSupport или даже в BuyInit/SellInit. При этом, если открытых позиций не было, то не было и вызовов BuySupport/SellSupport, а следовательно надежно обрабатывать отложенные ордера можно было только в OnEvent. Но обработка в этом методе нарушала общую последовательность действий. Получалось, что часть позиций обрабатываются последовательно по очереди, организованной CStrategy, а часть позиций (отложенных ордеров) обрабатывались по старинке, в едином блоке OnEvent.

В связи с этим, в новой версии CStrategy введены два дополнительных метода SupportPendingBuy и SupportPendingSell:

class CStrategy
{
protected:
   ...   
   virtual void      SupportPendingBuy(const MarketEvent &event, CPendingOrder* order);
   virtual void      SupportPendingSell(const MarketEvent &event, CPendingOrder* order);
   ...
};

Их сигнатура вызова похожа на методы SupportBuy и SupportSell: первым параметром передается событие MarketEvent, вторым — указатель на текущий ордер, выбранный CStrategy. Сам выбор ордера осуществляет движок CStrategy последовательно, методом перебора. Перебор осуществляется от конца списка ордеров к его началу, в методе CallSupport:

//+------------------------------------------------------------------+
//| Вызывает логику сопровождения позиций при условии, что торговое  |
//| состояние не равно TRADE_WAIT.                                   |
//+------------------------------------------------------------------+
void CStrategy::CallSupport(const MarketEvent &event)
  {
   ...
   for(int i = PendingOrders.Total()-1; i >= 0; i--)
   {
      CPendingOrder* order = PendingOrders.GetOrder(i);
      if(order.ExpertMagic()!=m_expert_magic)continue;
      if(order.Direction() == POSITION_TYPE_BUY)
         SupportPendingBuy(event, order);
      else
         SupportPendingSell(event, order);
   }
}

Таким образом, вызов обработчиков отложенных ордеров происходит так же, как и в случае с рыночными позициями: для каждого отложенного ордера на покупку вызывается метод SupportPendingBuy, а для каждого ордера на продажу — метод SupportPendingSell.

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

  1. открытие длиной позиции в InitBuy / Открытие короткой позиции в InitSell;
  2. сопровождение длинной позиции в SupportBuy / Сопровождение короткой позиции в SupportSell.

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

  1. установка отложенной длинной позиции в InitBuy / установка отложенной короткой позиции в InitSell;
  2. сопровождение отложенной длинной позиции в SupportPendingBuy до момента срабатывания ордера, либо его отмены / сопровождение отложенной короткой позиции в SupportPendingSell до момента срабатывания ордера, либо его отмены;
  3. сопровождение длинной позиции в SupportBuy / сопровождение короткой позиции в SupportSell.

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

Стратегия CImpulse 2.0 

Лучший способ разобраться с новыми изменениями — переписать уже знакомый нам пример торговой стратегии CImpulse, который, напомню, был представлен в пятой части статьи. Суть ее в том, чтобы на каждом баре выставлять отложенный Stop-ордер, на некотором расстоянии от скользящей средней. Расстояние это выражается в процентах. Для покупки выставляется BuyStop ордер, чей уровень открытия выше скользящей средней на N процентов. Для продажи — наоборот: выставляется SellStop ордер, чей уровень ниже скользящей средней на N процентов. Закрывать позицию мы будем, когда цена закрытия станет ниже (для покупки) или выше (для продажи) средней.

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

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

input double StopPercent = 0.20;
//+------------------------------------------------------------------+
//| Определяет действие, которое необходимо совершать с отложенным   |
//| ордером.                                                         |
//+------------------------------------------------------------------+
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--) // Идет перебор отложенных ордеров в секции InitBuy, что не очень хорошо
   {
      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;
}

Наиболее проблемные моменты в реализации данной стратегии показаны желтым цветом.

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

Во-вторых, работа с отложенными ордерами происходит через полный перебор отложенных ордеров в блоках BuyInit/SellInit. В такой простой стратегии, как CImpulse, это не вызывает сложностей, однако в случае с более сложным сопровождением ордеров трудности могут возникнуть. Лучше разделять установку отложенных ордеров и процесс их сопровождения на отдельные методы, как это сделано в новой версии CStrategy.

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

Перепишем код с учетом новых возможностей CStrategy:

//+------------------------------------------------------------------+
//|                                                  Impulse 2.0.mqh |
//|           Copyright 2017, Vasiliy Sokolov, St-Petersburg, Russia |
//|                                https://www.mql5.com/ru/users/c-4 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, Vasiliy Sokolov."
#property link      "https://www.mql5.com/ru/users/c-4"
#include "Strategy\Strategy.mqh"
#include "Strategy\Indicators.mqh"

input int PeriodMA = 12;
input double StopPercent = 0.05;

//+------------------------------------------------------------------+
//| Стратегия CImpulse                                               |
//+------------------------------------------------------------------+
class CImpulse : public CStrategy
{
private:
   double            m_percent;        // Процент уровня отложенного ордера
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      SupportPendingBuy(const MarketEvent &event,CPendingOrder *order);
   virtual void      SupportPendingSell(const MarketEvent &event,CPendingOrder* order);
   virtual bool      OnInit(void);
public:
   double            GetPercent(void);
   void              SetPercent(double percent);
   CUnIndicator      UnMA;
};
//+------------------------------------------------------------------+
//| Инициализируем скользящую среднюю                                |
//+------------------------------------------------------------------+
bool CImpulse::OnInit(void)
{
   UnMA.SetParameter(12);
   UnMA.SetParameter(0);
   UnMA.SetParameter(MODE_SMA);
   UnMA.SetParameter(PRICE_CLOSE);
   m_percent = StopPercent;
   if(UnMA.Create(Symbol(), Period(), IND_MA) != INVALID_HANDLE)
      return true;
   return false;
}
//+------------------------------------------------------------------+
//| Установка отложенных ордеров BuyStop                             |
//+------------------------------------------------------------------+
void CImpulse::InitBuy(const MarketEvent &event)
{
   if(!IsTrackEvents(event))return;                                           // Создаем отложенные ордера только на открытии нового бара
   if(PositionsTotal(POSITION_TYPE_BUY, ExpertSymbol(), ExpertMagic()) > 0)   // Не должно быть открыто ни одной длинной позиции
      return;
   if(OrdersTotal(POSITION_TYPE_BUY, ExpertSymbol(), ExpertMagic()) > 0)      // Не должно быть ни одного отложенного ордера на покупку
      return;
   double target = WS.Ask() + WS.Ask()*(m_percent/100.0);                     // Рассчитываем уровень нового отложенного ордера
   if(target < UnMA[0])                                                       // Цена срабатывания ордера должна быть выше скользящей средней
      return;
   Trade.BuyStop(MM.GetLotFixed(), target, ExpertSymbol(), 0, 0, NULL);       // Устанавливаем новый BuyStop ордер
}
//+------------------------------------------------------------------+
//| Работа с отложенными ордерами BuyStop для открытия длинной       |
//| позиции                                                          |
//+------------------------------------------------------------------+
void CImpulse::SupportPendingBuy(const MarketEvent &event,CPendingOrder *order)
{
   if(!IsTrackEvents(event))return;
   double target = WS.Ask() + WS.Ask()*(m_percent/100.0);                     // Рассчитываем новый уровень отложенного ордера
   if(UnMA[0] > target)                                                       // Если новый уровень ниже текущей средней
      order.Delete();                                                         // - удаляем его
   else                                                                       // В противном случае модифицируем на новую цену
      order.Modify(target);
}
//+------------------------------------------------------------------+
//| Работа с отложенными ордерами SellStop для открытия короткой     |
//| позиции                                                          |
//+------------------------------------------------------------------+
void CImpulse::SupportPendingSell(const MarketEvent &event,CPendingOrder* order)
{
   if(!IsTrackEvents(event))return;
   double target = WS.Ask() - WS.Ask()*(m_percent/100.0);                     // Рассчитываем новый уровень отложенного ордера
   if(UnMA[0] < target)                                                       // Если новый уровень выше текущей средней
      order.Delete();                                                         // - удаляем его
   else                                                                       // В противном случае модифицируем на новую цену
      order.Modify(target);
}
//+------------------------------------------------------------------+
//| Установка отложенных ордеров SellStop                            |
//+------------------------------------------------------------------+
void CImpulse::InitSell(const MarketEvent &event)
{
   if(!IsTrackEvents(event))return;                                           // Создаем отложенные ордера только на открытии нового бара
   if(PositionsTotal(POSITION_TYPE_SELL, ExpertSymbol(), ExpertMagic()) > 0)  // Не должно быть открыто ни одной короткой позиции
      return;
   if(OrdersTotal(POSITION_TYPE_SELL, ExpertSymbol(), ExpertMagic()) > 0)     // Не должно быть ни одного отложенного ордера на продажу
      return;
   double target = WS.Bid() - WS.Bid()*(m_percent/100.0);                     // Рассчитываем уровень нового отложенного ордера
   if(target > UnMA[0])                                                       // Цена срабатывания ордера должна быть ниже скользящей средней
      return;  
   Trade.SellStop(MM.GetLotFixed(), target, ExpertSymbol(), 0, 0, NULL);      // Устанавливаем новый BuyStop ордер
}
//+------------------------------------------------------------------+
//| Сопровождение длинной позиции по скользящей средней Moving       |
//+------------------------------------------------------------------+
void CImpulse::SupportBuy(const MarketEvent &event,CPosition *pos)
{
   int bar_open = WS.IndexByTime(pos.TimeOpen());
   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 = WS.Bid() - WS.Bid()*(m_percent/100.0);
      if(target < UnMA[0])
         pos.StopLossValue(target);
      else
         pos.StopLossValue(0.0);
   }
   if(WS.Bid() < UnMA[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 = WS.Ask() + WS.Ask()*(m_percent/100.0);
      if(target > UnMA[0])
         pos.StopLossValue(target);
      else
         pos.StopLossValue(0.0);
   }
   if(WS.Ask() > UnMA[0])
      pos.CloseAtMarket();
}
//+------------------------------------------------------------------+
//| Возвращает процент пробойного уровня                             |
//+------------------------------------------------------------------+  
double CImpulse::GetPercent(void)
{
   return m_percent;
}
//+------------------------------------------------------------------+
//| Устанавливает процент пробойного уровня                          |
//+------------------------------------------------------------------+  
void CImpulse::SetPercent(double percent)
{
   m_percent = percent;
}

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   CImpulse* impulse = new CImpulse();
   impulse.ExpertMagic(140578);
   impulse.ExpertName("Impulse 2.0");
   impulse.Timeframe(Period());
   impulse.ExpertSymbol(Symbol());
   Manager.AddStrategy(impulse);
   return(INIT_SUCCEEDED);
   
}
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
   Manager.OnTick();
}
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int    id,
                  const long   &lparam,
                  const double &dparam,
                  const string &sparam)
{
   Manager.OnChartEvent(id, lparam, dparam, sparam);
   ChartRedraw(0);
}

На скриншоте ниже представлен фрагмент тестирования стратегии CIpmulse 2.0 в тестере стратегий. На нем видны выставленные отложенные ордера и работа с ними:


Рис. 1. Работа с отложенными ордерами в процессе тестирования стратегии Impulse 2.0

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

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

Новая структура проекта CStrategy

В предыдущих версиях торговый движок CStrategy находился сразу в нескольких подпапках MQL5. Так, например, сам движок и его вспомогательные файлы располагались в MQL5\Include\Strategy. Исходные коды, ответственные за реализацию торговой панели движка, были в MQL5\Include\Panel. Код эксперта мог располагаться в MQL5\Include\Strategy\Samples, а файл запуска эксперта mq5 — уже в MQL5\Experts\Article08. И всё это — не считая того, что различные вспомогательные компоненты вроде Dictionary.mqh или XmlBase.mqh тоже были разбросаны по многим каталогам папки Include.

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

Теперь весь проект содержится в каталоге MQL5\Experts\UnExpert. В нем расположена папка Strategy и файлы стратегий с расширением .mq5. Теперь конечная торговая стратегия — это один-единственный файл mq5, в котором содержатся как стандартные функции-обработчики событий (вроде OnInit  и OnTick), так и сам класс стратегии, основанный на CStrategy.

Все вспомогательные файлы также размещены в MQL5\Experts\UnExpert\Strategy. Это относится и к файлам по работе с XML, и к инфраструктурным файлам, наподобие Dictionary.mqh. Чтобы скомпилировать пример, достаточно открыть файл, например "MQL5\Experts\UnExpert\Impulse 2.0.mqh", и нажать кнопку "Компилировать".

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

Заключение

Мы рассмотрели новые компоненты CStrategy. Это класс CUnIndicator, реализующий универсальный интерфейс в ООП стиле для работы с любым как системным, так и произвольным индикатором MQL5, а также систему сопровождения отложенных ордеров на основе методов SupportPendingBuy и SupportPendingSell. Все эти элементы в совокупности дают мощный синергетический эффект при написании торгового эксперта. Пользователю не надо думать о низкоуровневых операциях. Практически все торговое окружение доступно для него через интуитивно понятные и лаконичные классы, а сама торговая логика задается простым переопределением соответствующих предустановленных методов. 

Сам проект теперь стал располагаться в одном месте, а его связи ограничены каталогом MQL5\Experts\UnExpert. Теперь нет необходимости размещать файлы по разным папкам директории MQL5. Это новшество также должно побудить пользователей к переходу на CStrategy или, по крайней мере, к увлекательному изучению его возможностей.