English 中文 Español Deutsch 日本語 Português
Универсальный торговый эксперт: Пользовательские стратегии и вспомогательные торговые классы (Часть 3)

Универсальный торговый эксперт: Пользовательские стратегии и вспомогательные торговые классы (Часть 3)

MetaTrader 5Примеры | 20 января 2016, 13:46
10 338 15
Vasiliy Sokolov
Vasiliy Sokolov

Содержание

 

Введение

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

В третьей части серии мы подробно опишем примеры советников на основе торгового движка CStrategy, а также некоторые вспомогательные алгоритмы, которые могут понадобиться для разработки советника. Особое внимание уделяется процедуре логирования. В самом деле, логирование, несмотря на свою сугубо вспомогательную функцию, очень важный элемент любой более-менее сложной системы. С хорошим логировщиком можно быстро понять причины неисправности и найти место, в котором эта неисправность произошла. Данный логировщик написан с помощью специального приема программирования, называемого паттерн "Одиночка" (Singleton). Его рассмотрение будет интересным для тех, кто интересуется не только организацией торгового процесса, но и созданием алгоритмов для решения нестандартных задач.

Также в этой статье описываются алгоритмы, которые позволяют обращаться к рыночным данным через удобные и интуитивно понятные индексы. Действительно, работать с данными через индексы вроде Close[1] или High[0] — это многими любимая особенность работы в MetaTrader 4. Так зачем отказываться от нее, если ее точно так же можно использовать в MetaTrader 5? Эта статья объясняет, как это сделать, и подробно описывает алгоритмы, которые это реализуют.

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

 

Логирование сообщений, классы CMessage и CLog, паттерн "Одиночка"

Логирование относится к общим вспомогательным задачам. Как правило, в небольших программах используется обычная функция Print или printf, печатающая сообщение об ошибке в терминал MetaTrader 5:

...
double closes[];
if(CopyClose(Symbol(), Period(), 0, 100, closes) < 100)
   printf("Слишком мало данных.");
...

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

Самым очевидным методом класса логирования является метод AddMessage(). Например, если Log является объектом нашего логировщика CLog, то можно написать следующую конструкцию:

Log.AddMessage("Осторожно! Количество полученных баров меньше необходимого");

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

  • время создания;
  • источник сообщения;
  • тип сообщения (информационное, предупреждающее, сообщение об ошибке).

Было бы также удобно, чтобы наше сообщение, помимо этих данных, содержало дополнительную информацию:

  • идентификатор системной ошибки;
  • идентификатор торговой ошибки (если было произведено торговое действие);
  • время торгового сервера на момент создания сообщения.

Все эти данные удобнее всего объединить в специальном классе CMessage. Благодаря тому, что наше сообщение будет классом, в будущем в него будет легко добавить дополнительные данные и методы по работе с ними. Итак, приведем заголовок данного класса:

//+------------------------------------------------------------------+
//|                                                         Logs.mqh |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#include <Object.mqh>
#include <Arrays\ArrayObj.mqh>

#define UNKNOW_SOURCE "unknown"     // обозначение неопределенного источника сообщений
//+------------------------------------------------------------------+
//| Тип сообщения                                                    |
//+------------------------------------------------------------------+
enum ENUM_MESSAGE_TYPE
  {
   MESSAGE_INFO,                    // Информационное сообщение
   MESSAGE_WARNING,                 // Предупреждающее сообщение
   MESSAGE_ERROR                    // Возникновение ошибки
  };
//+------------------------------------------------------------------+
//| Сообщение, передаваемое классу логирования                       |
//+------------------------------------------------------------------+
class CMessage : public CObject
  {
private:
   ENUM_MESSAGE_TYPE m_type;               // Тип сообщения
   string            m_source;             // Источник сообщения
   string            m_text;               // Текст сообщения
   int               m_system_error_id;    // Содержит идентификатор СИСТЕМНОЙ ошибки
   int               m_retcode;            // Содержит код возврата торгового сервера
   datetime          m_server_time;        // Время торгового сервера в момент создания сообщения
   datetime          m_local_time;         // Локальное время в момент создания сообщения
   void              Init(ENUM_MESSAGE_TYPE type,string source,string text);
public:
                     CMessage(void);
                     CMessage(ENUM_MESSAGE_TYPE type);
                     CMessage(ENUM_MESSAGE_TYPE type,string source,string text);
   void              Type(ENUM_MESSAGE_TYPE type);
   ENUM_MESSAGE_TYPE Type(void);
   void              Source(string source);
   string            Source(void);
   void              Text(string text);
   string            Text(void);
   datetime          TimeServer(void);
   datetime          TimeLocal();
   void              SystemErrorID(int error);
   int               SystemErrorID();
   void              Retcode(int retcode);
   int               Retcode(void);
   string            ToConsoleType(void);
   string            ToCSVType(void);
  };

Первое, что встречается в заголовке, это перечисление ENUM_MESSAGE_TYPE. Оно определяет тип создаваемого сообщения. Тип сообщения может быть информационным (MESSAGE_INFO), предупреждающим (MESSAGE_WARNING) или может сигнализировать о возникновении ошибки (MESSAGE_ERROR).

Сам класс состоит из разнообразных Get/Set-методов, устанавливающих либо считывающих различные атрибуты сообщения. Для того чтобы было легко создавать сообщения в одной строке, у класса CMessage есть соответствующий перегруженный конструктор, который необходимо вызвать с параметрами, указывающими текст сообщения, его тип и источник. Например, если нам необходимо создать в функции OnTick сообщение, предупреждающее о слишком малом количестве загруженных данных, это можно сделать следующим образом:

void OnTick(void)
  {
   double closes[];
   if(CopyClose(Symbol(),Period(),0,100,closes)<100)
      CMessage message=new CMessage(MESSAGE_WARNING,__FUNCTION__,"Слишком мало данных");
  }

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

Теперь настало время рассмотреть сам класс логирования — CLog. Данный класс выполняет функции хранилища сообщений CMessage. Также одной из наиболее интересных его функций является отправка PUSH-уведомлений на мобильные терминалы при помощи функции SendNotification. Эта функция бывает крайне полезной, когда нет возможности отслеживать работу экспертов постоянно. Вместо этого можно отправлять на мобильные устройства пользователя сообщения о том, что что-то пошло не так.

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

Рассмотрим заголовок этого класса и методы, реализующие паттерн "Одиночка":

//+------------------------------------------------------------------+
//|                                                         Logs.mqh |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#include <Object.mqh>
#include <Arrays\ArrayObj.mqh>
#include "Message.mqh"

//+------------------------------------------------------------------+
//| Класс, организующий логирование сообщений, в виде синглтона      |
//+------------------------------------------------------------------+
class CLog
{
private:
   static CLog*      m_log;                     // Указатель на глобальный статический экземпляр
   CArrayObj         m_messages;                // Список сохраненных сообщений
   bool              m_terminal_enable;         // Истина, если требуется выводить полученое сообщение в торговый терминал
   bool              m_push_enable;             // Если истина, происходит отправка Push-уведомлений
   ENUM_MESSAGE_TYPE m_push_priority;           // Содержит заданный приоритет вывода сообщений в окно терминала
   ENUM_MESSAGE_TYPE m_terminal_priority;       // Содержит заданный приоритет отсылки сообщений на мобильные устройства адресатов
   bool              m_recursive;               // Флаг, указывающий на рекурсивный вызов деструктора
   bool              SendPush(CMessage* msg);
   void              CheckMessage(CMessage* msg);
                     CLog(void);                // Приватный конструктор
   string            GetName(void);
   void              DeleteOldLogs(int day_history = 30);
   void              DeleteOldLog(string file_name, int day_history);
                     ~CLog(void){;}
public:
   static CLog*      GetLog(void);              // Метод, для получения статического объекта
   bool              AddMessage(CMessage* msg);
   void              Clear(void);
   bool              Save(string path);
   CMessage*         MessageAt(int index)const;
   int               Total(void);
   void              TerminalEnable(bool enable);
   bool              TerminalEnable(void);
   void              PushEnable(bool enable);
   bool              PushEnable(void);
   void              PushPriority(ENUM_MESSAGE_TYPE type);
   ENUM_MESSAGE_TYPE PushPriority(void);
   void              TerminalPriority(ENUM_MESSAGE_TYPE type);
   ENUM_MESSAGE_TYPE TerminalPriority(void);
   bool              SaveToFile(void);
   static bool       DeleteLog(void);
};
CLog* CLog::m_log;

Класс CLog в качестве приватного члена хранит указатель на статический объект самого себя. Это может показаться странной конструкцией программирования, но в ней есть смысл. Единственный конструктор класса является приватным и недоступен для вызова. Вместо вызова конструктора можно воспользоваться методом GetLog:

//+------------------------------------------------------------------+
//| Возвращает объект-логировщик                                     |
//+------------------------------------------------------------------+
static CLog* CLog::GetLog()
{
   if(CheckPointer(m_log) == POINTER_INVALID)
      m_log = new CLog();
   return m_log;
}

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

Удаление объекта происходит также один-единственный раз. Для этого используется метод DeleteLog:

//+------------------------------------------------------------------+
//| Удаляет объект-логировщик                                        |
//+------------------------------------------------------------------+
bool CLog::DeleteLog(void)
{
   bool res = CheckPointer(m_log) != POINTER_INVALID;
   if(res)
      delete m_log;
   return res;
}

Метод, в случае существования объекта m_log, удаляет его и возвращает истину.

Может показаться, что описываемая система логирования достаточно сложна, однако и возможности, которые она поддерживает, весьма внушительны. Например, можно ранжировать сообщения по их типу или отправлять сообщения в качестве PUSH-уведомлений. В конечном счете, решать, использовать данную систему или нет, должен сам пользователь. Благодаря тому, что она реализована в отдельных модулях Message.mqh и Logs.mqh, ее можно использовать как отдельно от описываемого проекта, так и совместно.

 

Доступ к котировкам с помощью индексов, используемых в MetaTrader 4

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

double close = Close[0];

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

  1. Определить массив-получатель, в который будут скопированы котировки в нужном количестве.
  2. Скопировать требуемые котировки с помощью одной из функций группы Copy* (функции для доступа к таймсериям и данных индикаторов).
  3. Обратиться по нужному индексу скопированного массива.

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

double closes[];
double close = 0.0;
if(CopyClose(Symbol(), Period(), 0, 1, closes))
   close = closes[0];
else
   printf("Не удалось скопировать цену закрытия.");

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

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

double close = Close[0];

Необходимо написать следующий класс-обертку для цен закрытия:

//+------------------------------------------------------------------+
//| Доступ к ценам закрытия бара инструмента.                        |
//+------------------------------------------------------------------+
class CClose : public CSeries
  {
public:
   double operator[](int index)
     {
      double value[];
      if(CopyClose(m_symbol, m_timeframe, index, 1, value) == 0)return 0.0;
      return value[0];
     }
  };

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

Данный простой набор классов включен в набор Series.mqh. Он предоставляет удобный интерфейс для доступа к котировкам в стиле MetaTrader 4 и активно используется в самом торгом движке.

Отличительной особенностью данных классов является их платформонезависимость. Например, в MetaTrader 5 торговый эксперт может вызывать метод одного из этих классов, "думая", что он обращается к котировкам напрямую. Но в MetaTrader 4 этот метод доступа также будет работать, только уже вместо использования специализированного класса-обертки будет осуществляться фактический доступ к системным сериям вроде Open, High, Low или Close.

 

Использование объектно-ориентированных индикаторов

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

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

  1. символ, на котором рассчитывается скользящая средняя;
  2. таймфрейм или период графика;
  3. период усреднения;
  4. тип скользящей средней (простая, экспоненциальная, взвешенная и т.д.);
  5. сдвиг индикатора относительно ценового бара;
  6. используемая цена (одна из OHLC-цен бара либо расчетный буфер другого индикатора).

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

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

CMovingAverageExp MAExpert;     // Создаем эксперт, торгующий по двум скользящим средним.
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Конфигурируем быструю скользящую среднюю эксперта
   MAExpert.FastMA.Symbol("EURUSD");
   MAExpert.FastMA.Symbol(PERIOD_M10);
   MAExpert.FastMA.Period(13);
   MAExpert.FastMA.AppliedPrice(PRICE_CLOSE);
   MAExpert.FastMA.MaShift(1);
//--- Конфигурируем медленную скользящую среднюю эксперта
   MAExpert.SlowMA.Symbol("EURUSD");
   MAExpert.SlowMA.Symbol(PERIOD_M15);
   MAExpert.SlowMA.Period(15);
   MAExpert.SlowMA.AppliedPrice(PRICE_CLOSE);
   MAExpert.SlowMA.MaShift(1);

   return(INIT_SUCCEEDED);
  }

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

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

В качестве примера такого объектного индикатора, приведем исходный код класса CIndMovingAverage, который, в свою очередь, базируется на системном индикаторе 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;
  }

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

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

 

Методы, которые необходимо переопределить пользовательскому эксперту

В первой статье "Универсальный торговый эксперт: Торговые режимы стратегий" мы подробно рассмотрели торговые режимы стратегии и ее основные методы, которые необходимо переопределить. Теперь настало время заняться практикой.

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

Виртуальный метод Событие/действие Назначение
OnSymbolChanged Вызывается при изменении названия торгового символа В случае смены торгового инструмента индикаторы эксперта необходимо переинициализировать. Данное событие позволяет выполнить переинициализацию индикаторов эксперта.
OnTimeframeChanged Смена рабочего таймфрейма В случае смены рабочего таймфрейма индикаторы эксперта необходимо переинициализировать. Данное событие позволяет выполнить переинициализацию индикаторов эксперта.
ParseXmlParams Парсинг пользовательских параметров стратегии, загруженных через XML-файл Стратегии необходимо самостоятельно распознать XML-параметры, переданные в данный метод, и сконфигурировать свои настройки соответствующим образом.
ExpertNameFull Возвращает полное имя эксперта Полное имя эксперта состоит из названия стратегии и, как правило, уникального набора параметров самой стратегии. Экземпляр стратегии должен самостоятельно определить для себя полное имя. Это имя также используется в визуальной панели, в раскрывающем списке Agent.
OnTradeTransaction Возникает в случае возникновения торгового события Некоторым стратегиям для своей работы необходимо анализировать торговые события. Это событие позволяет передать конечному эксперту торговое событие и проанализировать его.
InitBuy Инициирует покупку Один из основных методов, который необходимо переопределить. В данном методе необходимо совершить покупку, если сформировались подходящие торговые условия для нее.
InitSell Инициирует продажу Один из основных методов, который необходимо переопределить. В данном методе необходимо совершить продажу, если сформировались подходящие торговые условия для нее.
SupportBuy Сопровождает ранее открытую длинную позицию Ранее открытую длинную позицию необходимо сопроводить. Например, поставить для нее защитную остановку Stop Loss или закрыть позицию в случае возникновения сигнала на выход из позиции. Все эти действия необходимо выполнить в данном методе.
SupportSell Сопровождает ранее открытую короткую позицию Ранее открытую короткую позицию необходимо сопроводить. Например, поставить для нее защитную остановку Stop Loss или закрыть позицию в случае возникновения сигнала на выход из позиции. Все эти действия необходимо выполнить в данном методе.

 Таблица 1. Виртуальные методы и их назначение

Наиболее важные методы, которые необходимо переопределить, это InitBuy, InitSell, SupportBuy и SupportSell. В таблице они выделены жирным шрифтом. Если, например, забыть переопределить метод InitBuy, то пользовательская стратегия не будет совершать покупок. Если не переопределить один из Support методов, то открытая позиция может быть никогда не закрыта. Поэтому при создании эксперта крайне внимательно относитесь к переопределению этих методов.

Если вы хотите, чтобы торговый движок автоматически загружал стратегию из XML-файла и конфигурировал ее параметры согласно настройкам, взятым из этого файла, вам также необходимо переопределить метод ParseXmlParams. В этом методе стратегия самостоятельно должна определить параметры, которые ей передаются, и понять, как изменить собственные настройки согласно этим параметрам. Работа с XML-параметрами будет более подробно описана в следующей, четвертой части серии статей "Универсальный торговый эксперт: Торговля в группе и управление портфелем стратегий (Часть 4)". Пример переопределения метода ParseXmlParams приводится в листинге стратегии, основанной на канале Боллинджера.

 

Пример советника, торгующего по двум скользящим средним

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

  • При смене таймфрейма и символа изменить настройки индикаторов быстрой и медленной скользящей средней, для чего переопределить методы OnSymbolChanged и OnTimeframeChanged.
  • Переопределить методы InitBuy, InitSell, SupportBuy и SupportSell. Разместить в этих методах торговую логику эксперта (правила на открытие и сопровождение позиций).

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

//+------------------------------------------------------------------+
//|                                                      Samples.mqh |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#include <Strategy\Strategy.mqh>
#include <Strategy\Indicators\MovingAverage.mqh>
//+------------------------------------------------------------------+
//| Пример классической стратегии на двух скользящих средних.        |
//| Если быстрая скользящая средняя пересекает медленную снизу вверх |
//| то покупаем, если сверху вниз - то продаем.                      |
//+------------------------------------------------------------------+
class CMovingAverage : public CStrategy
  {
private:
   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:
   CIndMovingAverage FastMA;        // Быстрая скользящая средняя
   CIndMovingAverage SlowMA;        // Медленная скользящая средняя
                     CMovingAverage(void);
   virtual string    ExpertNameFull(void);
  };
//+------------------------------------------------------------------+
//| Инициализация.                                                   |
//+------------------------------------------------------------------+
CMovingAverage::CMovingAverage(void)
  {
  }
//+------------------------------------------------------------------+
//| Реагируем на изменение символа                                   |
//+------------------------------------------------------------------+
void CMovingAverage::OnSymbolChanged(string new_symbol)
  {
   FastMA.Symbol(new_symbol);
   SlowMA.Symbol(new_symbol);
  }
//+------------------------------------------------------------------+
//| Реагируем на изменение таймфрейма                                |
//+------------------------------------------------------------------+
void CMovingAverage::OnTimeframeChanged(ENUM_TIMEFRAMES new_tf)
  {
   FastMA.Timeframe(new_tf);
   SlowMA.Timeframe(new_tf);
  }
//+------------------------------------------------------------------+
//| Покупаем, когда быстрая скользящая средняя выше медленной.       |
//+------------------------------------------------------------------+
void CMovingAverage::InitBuy(const MarketEvent &event)
  {
   if(!IsTrackEvents(event))return;                      // Обрабатываем только нужное событие!
   if(positions.open_buy > 0) return;                    // Если есть хотя бы одна длиная позиция - покупать больше не надо, т.к. мы уже купили!
   if(FastMA.OutValue(1) > SlowMA.OutValue(1))           // Если позиций на покупку нет, смотрим, что сейчас быстрая средняя выше медленной:
      Trade.Buy(MM.GetLotFixed(), ExpertSymbol(), "");   // Если выше - покупаем.
  }
//+------------------------------------------------------------------+
//| Закрываем длинную позицию, когда быстрая скользящая средняя      |
//| ниже медленной.                                                  |
//+------------------------------------------------------------------+
void CMovingAverage::SupportBuy(const MarketEvent &event,CPosition *pos)
  {
   if(!IsTrackEvents(event))return;                      // Обрабатываем только нужное событие!
   if(FastMA.OutValue(1) < SlowMA.OutValue(1))           // Если быстрая средняя ниже медленной -   
      pos.CloseAtMarket("Exit by cross over");           // Закрываем позицию.
  }
//+------------------------------------------------------------------+
//| Покупаем, когда быстрая скользящая средняя выше медленной.       |
//+------------------------------------------------------------------+
void CMovingAverage::InitSell(const MarketEvent &event)
  {
   if(!IsTrackEvents(event))return;                      // Обрабатываем только нужное событие!
   if(positions.open_sell > 0) return;                   // Если есть хотя бы одна короткая позиция - продавать больше не надо, т.к. мы уже продали!
   if(FastMA.OutValue(1) < SlowMA.OutValue(1))           // Если позиций на продажу нет, смотрим, что сейчас быстрая средняя выше медленной:
      Trade.Sell(1.0, ExpertSymbol(), "");               // Если выше - покупаем.
  }
//+------------------------------------------------------------------+
//| Закрываем короткую позицию, когда быстрая скользящая средняя     |
//| выше медленной.                                                  |
//+------------------------------------------------------------------+
void CMovingAverage::SupportSell(const MarketEvent &event,CPosition *pos)
  {
   if(!IsTrackEvents(event))return;                      // Обрабатываем только нужное событие!
   if(FastMA.OutValue(1) > SlowMA.OutValue(1))           // Если быстрая средняя выше медленной -    
      pos.CloseAtMarket("Exit by cross under");          // Закрываем позицию.
  }
//+------------------------------------------------------------------+
//| Отфильтровывает поступающие события. Если переданное событие     |
//| не обрабатывается стратегией, возвращает ложь, если обрабатыва-  |
//| ется - возвращает истину.                                        |
//+------------------------------------------------------------------+
bool CMovingAverage::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;
  }

Приведенный код достаточно прост для понимания. Однако требуется пояснить некоторые моменты. Торговый движок CStrategy вызывает методы InitBuy, InitSell, SupportBuy и SuportSell (методы торговой логики) при наступлении любого события, например при изменении стакана, прихода нового тика или изменения таймера. Как правило, вызов этих методов происходит крайне часто. Однако сам эксперт использует весьма ограниченный набор событий. Конкретно данный эксперт использует лишь одно событие — образование нового бара. Поэтому остальные события, которые вызывают методы торговой логики, необходимо игнорировать. Для этого и служит метод IsTrackEvents. Он проверяет, является ли переданное ему событие отслеживаемым, и если это так — возвращает истину, в противном случае возвращает ложь.

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

  1. торговое событие является открытием нового бара;
  2. больше нет открытой позиции в заданном направлении;
  3. быстрая скользящая средняя находится выше (для покупки) или ниже (для продажи) медленной скользящей средней.

Условия, которые необходимо выполнить для закрытия позиции, еще проще:

  1. торговое событие является открытием нового бара;
  2. быстрая скользящая средняя находится ниже (для закрытия длинной позиции) или выше (для закрытия короткой позиции) медленной скользящей средней.

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

Заметим, что собственно логика эксперта, без учета определений методов и его класса, описывается всего 18 строчками кода. Притом, половина из этих строк (условия для продажи), является зеркальным отражением другой половины (условиями для покупки). Такое упрощение логики возможно только при использовании вспомогательных библиотек, какой и является торговый движок CStrategy.

 

Пример советника на основе пробития канала BollingerBands

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

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

//+------------------------------------------------------------------+
//|                                                ChannelSample.mqh |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#include <Strategy\Strategy.mqh>
//+------------------------------------------------------------------+
//| defines                                                          |
//+------------------------------------------------------------------+
class CChannel : public CStrategy
  {
private:
   int               m_handle;   // Хэндл индикатора, который будем использовать
   int               m_period;   // Период Боллинджера
   double            m_std_dev;  // Размер стандартного отклонения
   bool              IsTrackEvents(const MarketEvent &event);
protected:
   virtual void      OnSymbolChanged(string new_symbol);
   virtual void      OnTimeframeChanged(ENUM_TIMEFRAMES new_tf);
   virtual void      InitBuy(const MarketEvent &event);
   virtual void      SupportBuy(const MarketEvent &event,CPosition *pos);
   virtual void      InitSell(const MarketEvent &event);
   virtual void      SupportSell(const MarketEvent &event,CPosition *pos);
   virtual bool      ParseXmlParams(CXmlElement *params);
   virtual string    ExpertNameFull(void);
public:
                     CChannel(void);
                    ~CChannel(void);
   int               PeriodBands(void);
   void              PeriodBands(int period);
   double            StdDev(void);
   void              StdDev(double std);
  };
//+------------------------------------------------------------------+
//| Конструктор по умолчанию                                         |
//+------------------------------------------------------------------+
CChannel::CChannel(void) : m_handle(INVALID_HANDLE)
  {
  }
//+------------------------------------------------------------------+
//| Деструктор освобождает используемый хендл индикатора             |
//+------------------------------------------------------------------+
CChannel::~CChannel(void)
  {
   if(m_handle!=INVALID_HANDLE)
      IndicatorRelease(m_handle);
  }
//+------------------------------------------------------------------+
//| Реагируем на изменение символа                                   |
//+------------------------------------------------------------------+
void CChannel::OnSymbolChanged(string new_symbol)
  {
   if(m_handle!=INVALID_HANDLE)
      IndicatorRelease(m_handle);
   m_handle=iBands(new_symbol,Timeframe(),m_period,0,m_std_dev,PRICE_CLOSE);
  }
//+------------------------------------------------------------------+
//| Реагируем на изменение таймфрейма                                |
//+------------------------------------------------------------------+
void CChannel::OnTimeframeChanged(ENUM_TIMEFRAMES new_tf)
  {
   if(m_handle!=INVALID_HANDLE)
      IndicatorRelease(m_handle);
   m_handle=iBands(ExpertSymbol(),Timeframe(),m_period,0,m_std_dev,PRICE_CLOSE);
  }
//+------------------------------------------------------------------+
//| Возвращает период индикатора                                     |
//+------------------------------------------------------------------+
int CChannel::PeriodBands(void)
  {
   return m_period;
  }
//+------------------------------------------------------------------+
//| Устанавливает период индикатора                                  |
//+------------------------------------------------------------------+
void CChannel::PeriodBands(int period)
  {
   if(m_period == period)return;
   m_period=period;
   if(m_handle!=INVALID_HANDLE)
      IndicatorRelease(m_handle);
   m_handle=iBands(ExpertSymbol(),Timeframe(),m_period,0,m_std_dev,PRICE_CLOSE);
  }
//+------------------------------------------------------------------+
//| Устанавливает величину стандартного отклонения                   |
//+------------------------------------------------------------------+
double CChannel::StdDev(void)
  {
   return m_std_dev;
  }
//+------------------------------------------------------------------+
//| Устанавливает величину стандартного отклонения                   |
//+------------------------------------------------------------------+
void CChannel::StdDev(double std)
  {
   if(m_std_dev == std)return;
   m_std_dev=std;
   if(m_handle!=INVALID_HANDLE)
      IndicatorRelease(m_handle);
   m_handle=iBands(ExpertSymbol(),Timeframe(),m_period,0,m_std_dev,PRICE_CLOSE);
  }
//+------------------------------------------------------------------+
//| Правила открытия длинной позиции                                 |
//+------------------------------------------------------------------+
void CChannel::InitBuy(const MarketEvent &event)
  {
   if(IsTrackEvents(event))return;                    // Включаем логику только на открытии нового бара
   if(positions.open_buy > 0)return;                  // Не открывает более одной длинной позиции
   double bands[];
   if(CopyBuffer(m_handle, UPPER_BAND, 1, 1, bands) == 0)return;
   if(Close[1]>bands[0])
      Trade.Buy(1.0,ExpertSymbol());
  }
//+------------------------------------------------------------------+
//| Правила закрытия длинной позиции                                 |
//+------------------------------------------------------------------+
void CChannel::SupportBuy(const MarketEvent &event,CPosition *pos)
  {
   if(IsTrackEvents(event))return;                    // Включаем логику только на открытии нового бара
   double bands[];
   if(CopyBuffer(m_handle, BASE_LINE, 1, 1, bands) == 0)return;
   double b = bands[0];
   double s = Close[1];
   if(Close[1]<bands[0])
      pos.CloseAtMarket();
  }
//+------------------------------------------------------------------+
//| Правила открытия длинной позиции                                 |
//+------------------------------------------------------------------+
void CChannel::InitSell(const MarketEvent &event)
  {
   if(IsTrackEvents(event))return;                    // Включаем логику только на открытии нового бара
   if(positions.open_sell > 0)return;                 // Не открывает более одной длинной позиции
   double bands[];
   if(CopyBuffer(m_handle, LOWER_BAND, 1, 1, bands) == 0)return;
   if(Close[1]<bands[0])
      Trade.Sell(1.0,ExpertSymbol());
  }
//+------------------------------------------------------------------+
//| Правила закрытия длинной позиции                                 |
//+------------------------------------------------------------------+
void CChannel::SupportSell(const MarketEvent &event,CPosition *pos)
  {
   if(IsTrackEvents(event))return;     // Включаем логику только на открытии нового бара
   double bands[];
   if(CopyBuffer(m_handle, BASE_LINE, 1, 1, bands) == 0)return;
   double b = bands[0];
   double s = Close[1];
   if(Close[1]>bands[0])
      pos.CloseAtMarket();
  }
//+------------------------------------------------------------------+
//| Отфильтровывает поступающие события. Если переданное событие     |
//| не обрабатывается стратегией, возвращает ложь, если обрабатыва-  |
//| ется - возвращает истину.                                        |
//+------------------------------------------------------------------+
bool CChannel::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;
  }
//+------------------------------------------------------------------+
//| Специфические для стратегии параметры парсятся в ней самой в     |
//| данном методе, переопределенном от CStrategy                     |
//+------------------------------------------------------------------+
bool CChannel::ParseXmlParams(CXmlElement *params)
  {
   bool res=true;
   for(int i=0; i<params.GetChildCount(); i++)
     {
      CXmlElement *param=params.GetChild(i);
      string name=param.GetName();
      if(name=="Period")
         PeriodBands((int)param.GetText());
      else if(name=="StdDev")
         StdDev(StringToDouble(param.GetText()));
      else
         res=false;
     }
   return res;
  }
//+------------------------------------------------------------------+
//| Полное, уникальное имя эксперта                                  |
//+------------------------------------------------------------------+
string CChannel::ExpertNameFull(void)
  {
   string name=ExpertName();
   name += "[" + ExpertSymbol();
   name += "-" + StringSubstr(EnumToString(Timeframe()), 7);
   name += "-" + (string)Period();
   name += "-" + DoubleToString(StdDev(), 1);
   name += "]";
   return name;
  }

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

Обратите внимание, что в этом советнике используется непосредственный доступ к барам через специальные классы таймсерии. Например, так сравнивается последняя известная цена закрытия бара с верхней полосой Боллинджера в секции покупок (метод InitBuy):

double bands[];
if(CopyBuffer(m_handle, UPPER_BAND, 1, 1, bands) == 0)return;
if(Close[1] > bands[0])
   Trade.Buy(1.0, ExpertSymbol());

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

 

Загрузка пользовательских стратегий в торговый движок

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

  • Уникальный идентификатор стратегии (ее магический номер). Номера стратегий должны быть уникальны, даже если они создаются как экземпляры одного класса. Для указания уникального номера необходимо воспользоваться Set-методом ExpertMagic() стратегии.
  • Таймфрейм стратегии (или ее рабочий период). Если стратегия работает на нескольких рабочих периодах одновременно, указать рабочий таймфрейм все равно необходимо. В этом случае это может быть, например, наиболее часто используемый таймфрейм. Для указания рабочего периода необходимо воспользоваться Set-методом Timeframe.
  • Символ стратегии (или ее рабочий инструмент). Если стратегия работает сразу с несколькими инструментами (мультиинструментальная стратегия), указать рабочий символ все равно необходимо. Это может быть один из символов, который использует стратегия.
  • Имя стратегии. Каждая стратегия обязана, помимо перечисленных атрибутов, иметь собственное имя в виде строки string. Имя эксперта заполняется с помощью Set-метода ExpertName. Данное свойство обязательно, потому что именно оно используется при автоматическом создании стратегий из файла Strategies.xml. Также это свойство используется при отображении стратегии в пользовательской панели, описание которой будет дано в четвертой части серии статей.

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

Сам торговый движок состоит из двух основных частей:

Каждый экземпляр стратегии CStrategy должен быть загружен в менеджер стратегий CStrategyList. Менеджер стратегий позволяет загружать стратегии двумя путями:

  • Автоматически, с помощью конфигурационного файла Strategies.xml. Например, можно описать набор стратегий с их параметрами в этом файле, а при запуске эксперта на графике менеджер стратегий сам создаст нужные экземпляры стратегий, проинициализирует их параметры и добавит в свой список. Об этом способе будет подробно рассказано в следующей статье.
  • Вручную, с помощью непосредственного описания в исполняющем модуле. При этом способе в секции OnInit торгового эксперта с помощью набора инструкций создается соответствующий объект-стратегия, затем он инициализируется необходимыми параметрами и добавляется в менеджер стратегий CStrategyList.

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

//+------------------------------------------------------------------+
//|                                                        Agent.mq5 |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Strategy\StrategiesList.mqh>
#include <Strategy\Samples\ChannelSample.mqh>
#include <Strategy\Samples\MovingAverage.mqh>
CStrategyList Manager;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Конфигурируем и добавляем в список стратегий CMovingAverage
   CMovingAverage *ma=new CMovingAverage();
   ma.ExpertMagic(1215);
   ma.Timeframe(Period());
   ma.ExpertSymbol(Symbol());
   ma.ExpertName("Moving Average");
   ma.FastMA.MaPeriod(10);
   ma.SlowMA.MaPeriod(23);
   if(!Manager.AddStrategy(ma))
      delete ma;

//--- Конфигурируем и добавляем в список стратегий CChannel
   CChannel *channel=new CChannel();
   channel.ExpertMagic(1216);
   channel.Timeframe(Period());
   channel.ExpertSymbol(Symbol());
   channel.ExpertName("Bollinger Bands");
   channel.PeriodBands(50);
   channel.StdDev(2.0);
   if(!Manager.AddStrategy(channel))
      delete channel;

   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   EventKillTimer();
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   Manager.OnTick();
  }
//+------------------------------------------------------------------+
//| BookEvent function                                               |
//+------------------------------------------------------------------+
void OnBookEvent(const string &symbol)
  {
   Manager.OnBookEvent(symbol);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   Manager.OnChartEvent(id,lparam,dparam,sparam);
  }

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

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Конфигурируем и добавляем в список стратегий CMovingAverage
   CMovingAverage *ma=new CMovingAverage();
 //ma.ExpertMagic(1215);
   ma.Timeframe(Period());
   ma.ExpertSymbol(Symbol());
   ma.ExpertName("Moving Average");
   ma.FastMA.MaPeriod(10);
   ma.SlowMA.MaPeriod(23);
   if(!Manager.AddStrategy(ma))
      delete ma;
   return(INIT_SUCCEEDED);
  }

После запуска исполняющего модуля, в терминале появится следующее сообщение:

2016.01.20 14:08:54.995 AgentWrong (FORTS-USDRUB,H1)    WARNING;CStrategyList::AddStrategy;The strategy should have a magic number. Adding strategy Moving Average is impossible;2016.01.20 14:09:01

Из сообщения становится ясно, что метод CStrategyList::AddStartegy не смог добавить стратегию, так как ее магический номер не задан.

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

После того как исполняющий файл создан, его можно скомпилировать. Исходные коды рассматриваемых стратегий находятся в каталоге Include\Strategy\Samples и доступны в приложенном архиве. Скомпилированный эксперт будет готов к работе и будет включать в себя логику сразу двух торговых стратегий.

 

Заключение

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

В четвертой части серии статей "Универсальный торговый эксперт: Торговля в группе и управление портфелем стратегий (Часть 4)" мы опишем алгоритмы, с помощью которых в один исполняющий модуль эксперта ex5 можно добавлять неограниченное количество торговых логик. Также в четвертой части предлагается к рассмотрению простая пользовательская панель, с помощью которой можно управлять экспертами, находящимися внутри исполняющего модуля, например менять их торговый режим или совершать от их имени торговые действия по покупке и продаже.

Прикрепленные файлы |
strategyarticle.zip (85.87 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (15)
Andrey Nikolaev
Andrey Nikolaev | 1 февр. 2016 в 13:53

По описанию - отличная задумка...как раз пытаюсь перейти на MT5 со своими наработками...

Попытался скомпилить в качестве примера Agent.mq5 (из архива) - результат с ошибкой отсутствия библиотеки Include\Dictionary.mqh ... где ее можно взять?

Vasiliy Sokolov
Vasiliy Sokolov | 1 февр. 2016 в 14:11
Andrey Nikolaev:

По описанию - отличная задумка...как раз пытаюсь перейти на MT5 со своими наработками...

Попытался скомпилить в качестве примера Agent.mq5 (из архива) - результат с ошибкой отсутствия библиотеки Include\Dictionary.mqh ... где ее можно взять?

Пока здесь возьмите. Накладочка вышла. Уже подправил прилагаемые к статье исходники.
Andrey Nikolaev
Andrey Nikolaev | 2 февр. 2016 в 06:23
Vasiliy Sokolov:
Пока здесь возьмите. Накладочка вышла. Уже подправил прилагаемые к статье исходники.
Василий, можно еще файл Strategies.xml .... а то не понятно какая у него структура...
Mikhail Tkachev
Mikhail Tkachev | 2 февр. 2016 в 10:15
hftech:

.... По-моему, все уже давно сделали свой выбор...

У одного брокера могут быть разные наборы инструментов в МТ4 и МТ5, а у разных брокеров и подавно ...  И, например, трендследящие стратегии можно применять и на биржевых площадках, и на Форекс и CFD.
Так что насчет "все уже давно сделали свой выбор" Вы несколько преувеличили... :)
Очень хорошо, если будет набор классов, позволяющий быстро генерировать код экспертов для обоих платформ. Двумя  руками "За" !
igorbel
igorbel | 31 мар. 2016 в 10:16
Почему внешние переменные не сделалил в Agent.mq5 ? Как оптимизировать?
Графические интерфейсы I: Тестируем библиотеку в программах разных типов и в терминале MetaTrader 4 (Глава 5) Графические интерфейсы I: Тестируем библиотеку в программах разных типов и в терминале MetaTrader 4 (Глава 5)
В предыдущей главе первой части серии о графических интерфейсах в класс формы были добавлены методы, которые позволяют управлять формой посредством нажатия на ее элементах управления. В этой статье протестируем проделанную работу в разных типах MQL-программ, таких как индикаторы и скрипты. А поскольку библиотека задумывалась как кросс-платформенная (в рамках торговых платформ MetaTrader), то проведем тесты также и в MetaTrader 4.
Универсальный торговый эксперт: Событийная модель и прототип торговой стратегии (Часть 2) Универсальный торговый эксперт: Событийная модель и прототип торговой стратегии (Часть 2)
Данная статья продолжает серию заметок, посвященных универсальной модели эксперта. В этой части описывается оригинальная событийная модель на основе централизованной обработки данных, а также рассматривается структура базового класса движка — CStrategy.
Применение контейнеров для компоновки графического интерфейса: класс CGrid Применение контейнеров для компоновки графического интерфейса: класс CGrid
В данной статье описан альтернативный метод создания графического интерфейса на основе компоновки и контейнеров при помощи менеджера компоновки — класса CGrid. Класс CGrid представляет собой вспомогательный элемент управления, который действует как контейнер для других контейнеров и элементов управления с применением табличной компоновки.
Графические интерфейсы I: Функции для кнопок формы и удаление элементов интерфейса (Глава 4) Графические интерфейсы I: Функции для кнопок формы и удаление элементов интерфейса (Глава 4)
В этой главе продолжим развивать класс CWindow, дополняя его методами, которые позволят управлять формой посредством нажатия на ее элементах. Сделаем так, чтобы программу можно было закрыть через кнопку на форме, а также реализуем возможность сворачивания и разворачивания формы при необходимости.