English 中文 Español Deutsch 日本語 Português
Кроссплатформенный торговый советник: Введение

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

MetaTrader 5Интеграция | 15 августа 2016, 10:49
6 312 6
Enrico Lambino
Enrico Lambino

Оглавление


Введение

В числе поводов к созданию кроссплатформенных торговых советников для терминалов MetaTrader можно перечислить следующие:

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

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

  1. Разрабатывает программу с использованием одного языка (MQL4 или MQL5)

  2. Тщательно тестирует ее

  3. Реализует ту же программу на другом языке

Недостатки этой схемы таковы:

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

  2. Могут возникнуть сложности при отладке и дальнейшем сопровождении программы

  3. Снижается производительность

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


Пример советника Hello World

Давайте начнем с простого эксперта, написанного на MQL5: советник Hello World. Типичный пример написания подобного советника в вышеупомянутой версии MQL показан ниже.

HelloWorld.mq5

#include <Object.mqh>
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CHelloWorld : public CObject
  {
public:
                     CHelloWorld(void);
                    ~CHelloWorld(void);
   virtual void      Greeting(void);
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CHelloWorld::CHelloWorld(void)
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CHelloWorld::~CHelloWorld(void)
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CHelloWorld::Greeting(void)
  {
   Print("Hello World!");
  }

CHelloWorld hello;
//+------------------------------------------------------------------+
//| Функция инициализации советника                                  |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- 

//--- 
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Функция деинициализации советника                                |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- 

  }
//+------------------------------------------------------------------+
//| Тиковая функция эксперта                                         |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- 
   hello.Greeting();
   ExpertRemove();
  }

На MQL4 мы пишем аналогичный код по тому же методу.

(HelloWorld.mq4)

#include <Object.mqh>
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CHelloWorld : public CObject
  {
public:
                     CHelloWorld(void);
                    ~CHelloWorld(void);
   virtual void      Greeting(void);
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CHelloWorld::CHelloWorld(void)
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CHelloWorld::~CHelloWorld(void)
  {
  }

CHelloWorld hello;
//+------------------------------------------------------------------+
//| Функция инициализации советника                                  |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- 

//--- 
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Функция деинициализации советника                                |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- 

  }
//+------------------------------------------------------------------+
//| Тиковая функция эксперта                                         |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- 
   hello.Greeting();
   ExpertRemove();
  }


Исходные и заголовочные файлы

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

  • результат компиляции в MQ4 — создание файла EX4.
  • результат компиляции в MQ5 — создание файла EX5.

Иными словами, как уже было сказано выше, мы не можем получить один исходный файл, который будет работать на обеих платформах. Тем не менее, можно сделать так, чтобы оба исходника ссылались на один и тот же заголовочный файл — как это проиллюстрировано картинкой ниже:

Исходные и заголовочные файлы


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

HelloWorld_SingleHeader.mqh

#include <Object.mqh>
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CHelloWorld : public CObject
  {
public:
                     CHelloWorld(void);
                    ~CHelloWorld(void);
   virtual void      Greeting(void);
   virtual void      Greeting(const string str1,const string str2);
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CHelloWorld::CHelloWorld(void)
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CHelloWorld::~CHelloWorld(void)
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CHelloWorld::Greeting(void)
  {
   Print("Hello World!");
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CHelloWorld::Greeting(const string str1,const string str2)
  {
   string str=NULL;
   Print(StringConcatenate(str,str1,str2));
  }
CHelloWorld hello;
//+------------------------------------------------------------------+
//| Функция инициализации советника                                  |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- 

//--- 
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Функция деинициализации советника                                |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- 

  }
//+------------------------------------------------------------------+
//| Тиковая функция эксперта                                         |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- 
   hello.Greeting("Hello ","World!");
   ExpertRemove();
  }
//+------------------------------------------------------------------+

Исходные файлы на MQL4 и MQL5 содержат одну строку кода — это директива #include, ссылающаяся на заголовочный файл:

(HelloWorld_SingleHeader.mq4 и HelloWorld_SingleHeader.mq5)

#include <HelloWorld_SingleHeader.mqh>

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

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

Но советники редко пишутся так же легко, как приведенный мною пример. Они намного сложнее. И чем сложнее становится эксперт, тем сложнее становится поддерживать две его отдельные версии.


Условная компиляция

MQL4 и MQL5 имеют много общего, но и различий между ними тоже немало. В числе этих различий — имплементация функции StringConcatenate. В MQL4 функция определена следующим образом:

string  StringConcatenate( 
   void argument1,        // первый параметр любого простого типа
   void argument2,        // второй параметр любого простого типа 
   ...                    // следующий параметр любого простого типа 
   );

В MQL5 ее имплементация немного другая:

int  StringConcatenate( 
   string&  string_var,   // строка, которая будет сформирована 
   void argument1,        // первый параметр любого простого типа
   void argument2,        // второй параметр любого простого типа 
   ...                    // следующий параметр любого простого типа 
   );

Мы можем использовать эту функцию в Hello World при перегрузке метода Greeting() в нашем классе. Новый метод будет принимать два строковых аргумента, и объединенный результат будет распечатан в терминале. Обновим наш заголовочный файл:

(HelloWorld_SingleHeader.mqh)

#include <Object.mqh>
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CHelloWorld : public CObject
  {
public:
                     CHelloWorld(void);
                    ~CHelloWorld(void);
   virtual void      Greeting(void);
   virtual void      Greeting(const string str1,const string str2);
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CHelloWorld::CHelloWorld(void)
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CHelloWorld::~CHelloWorld(void)
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CHelloWorld::Greeting(void)
  {
   Print("Hello World!");
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CHelloWorld::Greeting(const string str1,const string str2)
  {
   string str=NULL;
   Print(StringConcatenate(str,str1,str2));
  }
CHelloWorld hello;
//+------------------------------------------------------------------+
//| Функция инициализации советника                                  |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- 

//--- 
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Функция деинициализации советника                                |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- 

  }
//+------------------------------------------------------------------+
//| Тиковая функция эксперта                                         |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- 
   hello.Greeting("Hello ","World!");
   ExpertRemove();
  }
//+------------------------------------------------------------------+


Используя эту обновленную версию, в терминале MetaTrader 4 мы увидим следующий распечатанный результат:

Hello World!

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

12

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

CHelloWorld::Greeting(const string str1,const string str2)
  {
   #ifdef __MQL5__
      string str=NULL;
      StringConcatenate(str,str1,str2);
      Print(str);
   #else
      Print(StringConcatenate(str1,str2));
   #endif
  }

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

CHelloWorld::Greeting(const string str1,const string str2)
  {
      Print(StringConcatenate(str1,str2));
}

Компилятор MQL5 прочитает этот код так:

CHelloWorld::Greeting(const string str1,const string str2)
  {
      string str=NULL;
      StringConcatenate(str,str1,str2);
      Print(str);
}


Реализация разделения классов

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

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

В MQL4 и MQL5 есть множество функций, которые ведут себя одинаково в обоих языках. Один из примеров таких функций — Print(). Она работает одинаково, независимо от того, в какой версии платформы запущен эксперт. Пример совместимого исходного кода можно также наблюдать в виде обычных вычислений. Результат 1+1 будет одинаковым в MQL4 и в MQL5, равно как и в любом другом языке программирования из реального мира. В обоих этих случаях реализация разделения классов требуется крайне редко.

А вот в случаях, когда определенная часть исходного кода либо не компилируется, либо будет выполняться по-разному в разных версиях платформы, реализация разделения нам понадобится. Пример первого случая несовместимого кода — функция StringConcatenate. Несмотря на одинаковое имя, эта функция ведет себя по-разному в MQL4 и в MQL5. Есть также некоторые функции, которые не имеют прямого аналога в другом языке. К примеру, функция OrderCalcMargin function, которая (по крайней мере, на момент написания данной статьи) не имеет эквивалентов в MQL4. Третий случай — вероятно, самый сложный для кроссплатформенной разработки, поскольку именно здесь реализация может варьироваться у разных разработчиков. В этом случае для уменьшения объема кода может потребоваться нахождение некоего общего знаменателя между двумя версиями платформы, а затем — реализация разделения классов по мере необходимости.

В этом случае полагаться исключительно на условную компиляцию может быть плохой идеей. Поскольку со временем код становится длиннее, наличие большого количества подобных инструкций сильно усложнит отладку и дальнейшее обслуживание программы. В объектно-ориентированном программировании нам может потребоваться разделить имплементацию на три части: (1) базовая имплементация, (2) MQL4-специфичная имплементация и (3) MQL5-специфичная имплементация.

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

Для нашего советника Hello World мы объявляем базовый класс и даем ему имя CHelloWorldBase. В нем будет содержаться код, общий для MQL4 и MQL5. Он включает исходный метод Greeting(), который мы определили в начале этой статьи:

HelloWorld_SingleHeader.mqh

#include <Object.mqh>
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CHelloWorldBase : public CObject
  {
public:
                     CHelloWorldBase(void);
                    ~CHelloWorldBase(void);
   virtual void      Greeting(void);
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CHelloWorldBase::CHelloWorldBase(void)
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CHelloWorldBase::~CHelloWorldBase(void)
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CHelloWorldBase::Greeting(void)
  {
   Print("Hello World!");
  }
//+------------------------------------------------------------------+

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

(HelloWorld_SingleHeader_MQL4.mqh)

#include "HelloWorld_SingleHeader.mqh"
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CHelloWorld : public CHelloWorldBase
  {
public:
                     CHelloWorld(void);
                    ~CHelloWorld(void);
   virtual void      Greeting(const string str1,const string str2);
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CHelloWorld::CHelloWorld(void)
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CHelloWorld::~CHelloWorld(void)
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CHelloWorld::Greeting(const string str1,const string str2)
  {
   Print(StringConcatenate(str1,str2));
  }
//+------------------------------------------------------------------+

HelloWorld_SingleHeader_MQL5.mqh

#include "HelloWorld_SingleHeader.mqh"
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CHelloWorld : public CHelloWorldBase
  {
public:
                     CHelloWorld(void);
                    ~CHelloWorld(void);
   virtual void      Greeting(const string str1,const string str2);
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CHelloWorld::CHelloWorld(void)
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CHelloWorld::~CHelloWorld(void)
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CHelloWorld::Greeting(const string str1,const string str2)
  {
   string str=NULL;
   StringConcatenate(str,str1,str2);
   Print(str);
  }
//+------------------------------------------------------------------+

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

HelloWorld_SingleHeader.mq5

#include <HelloWorld_SingleHeader_MQL5.mqh>
CHelloWorld hello;
//+------------------------------------------------------------------+
//| Функция инициализации советника                                  |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- 

//--- 
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Функция деинициализации советника                                |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- 

  }
//+------------------------------------------------------------------+
//| Тиковая функция эксперта                                         |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- 
   hello.Greeting("Hello ","World!");
   ExpertRemove();
  }
//+------------------------------------------------------------------+

HelloWorld_SingleHeader.mq4

#include <HelloWorld_SingleHeader_MQL4.mqh>
CHelloWorld hello;
//+------------------------------------------------------------------+
//| Функция инициализации советника                                  |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- 

//--- 
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Функция деинициализации советника                                |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- 

  }
//+------------------------------------------------------------------+
//| Тиковая функция эксперта                                         |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- 
   hello.Greeting("Hello ","World!");
   ExpertRemove();
  }
//+------------------------------------------------------------------+

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


Подключение файлов

Естественное поведение для разработчиков — просто сослаться на заголовочный файл, содержащий определение классов, которые будут использоваться в программе. К примеру, в MQL5-реализации советника HelloWorld мы можем увидеть, что две версии (HelloWorld_SingleHeader.mq4 и HelloWorld_SingleHeader.mq5) практически одинаковы, за исключением специфичного заголовочного файла, который они содержат.

#include <HelloWorld_SingleHeader_MQL4.mqh>
#include <HelloWorld_SingleHeader_MQL5.mqh>
Другой подход — сослаться на заголовочный файл, содержащий базовую имплементацию. Тогда в конце этого заголовочного файла мы можем использовать директиву условной компиляции для ссылки на заголовочный файл, содержащий соответствующий класс-наследник, в зависимости от типа используемого компилятора:
#include <Object.mqh>
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CHelloWorldBase : public CObject
  {
public:
                     CHelloWorldBase(void);
                    ~CHelloWorldBase(void);
   virtual void      Greeting(void);
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CHelloWorldBase::CHelloWorldBase(void)
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CHelloWorldBase::~CHelloWorldBase(void)
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CHelloWorldBase::Greeting(void)
  {
   Print("Hello World!");
  }
//+------------------------------------------------------------------+
#ifdef __MQL5__
   #include "HelloWorld_SingleHeader_MQL5.mqh"
#else
   #include "HelloWorld_SingleHeader_MQL4.mqh"
#endif
//+------------------------------------------------------------------+

Затем, в основном исходном файле мы ссылаемся именно на этот заголовочный файл, а не на тот, который специфичен для определенного языка:

#include <HelloWorld_SingleHeader.mqh>
CHelloWorld hello;
//+------------------------------------------------------------------+
//| Функция инициализации советника                                  |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- 

//--- 
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Функция деинициализации советника                                |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- 

  }
//+------------------------------------------------------------------+
//| Тиковая функция эксперта                                         |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- 
   hello.Greeting("Hello ","World!");
   ExpertRemove();
  }
//+------------------------------------------------------------------+

После этого мы удаляем директивы #include для заголовочных файлов, специфичных для языков (зачеркнутые строки показывают удаленные фрагменты кода).

#include "HelloWorld_SingleHeader.mqh"
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CHelloWorld : public CHelloWorldBase
  {
public:
                     CHelloWorld(void);
                    ~CHelloWorld(void);
   virtual void      Greeting(const string str1,const string str2);
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CHelloWorld::CHelloWorld(void)
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CHelloWorld::~CHelloWorld(void)
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CHelloWorld::Greeting(const string str1,const string str2)
  {
   Print(StringConcatenate(str1,str2));
  }
//+------------------------------------------------------------------+

#include "HelloWorld_SingleHeader.mqh"
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CHelloWorld : public CHelloWorldBase
  {
public:
                     CHelloWorld(void);
                    ~CHelloWorld(void);
   virtual void      Greeting(const string str1,const string str2);
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CHelloWorld::CHelloWorld(void)
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CHelloWorld::~CHelloWorld(void)
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CHelloWorld::Greeting(const string str1,const string str2)
  {
   string str=NULL;
   StringConcatenate(str,str1,str2);
   Print(str);
  }
//+------------------------------------------------------------------+

Этот подход рекомендован и имеет некоторые преимущества. Во-первых, он сохраняет одинаковыми директивы #include для обоих основных исходных файлов (MQL4 и MQL5). Также это избавляет вас от утомительной необходимости выбора того, какой заголовочный файл и какой путь (например, MQL4/ или MQL5/) подключить в данной директиве процессора. Третий плюс — этот способ сохраняет базовые включения в базовые заголовочные файлы. Если использовать директивы включения в специфичных для языка заголовочных файлах, они будут использоваться исключительно для соответствующих версий языка (MQL4 или MQL5).

Разделенные файлы и папки

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

/Include

/Base

/MQL4

/MQL5

Три папки могут быть расположены прямо внутри директории Include, или в подпапке внутри нее.

Для нашего примера кода примем следующую структуру папок:

/Include

/MQLx-Intro

/Base

HelloWorldBase.mqh

/MQL4

HelloWorld.mqh

/MQL5

HelloWorld.mqh

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

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

#include <Object.mqh>
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CHelloWorldBase : public CObject
  {
public:
                     CHelloWorldBase(void);
                    ~CHelloWorldBase(void);
   virtual void      Greeting(void);
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CHelloWorldBase::CHelloWorldBase(void)
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CHelloWorldBase::~CHelloWorldBase(void)
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CHelloWorldBase::Greeting(void)
  {
   Print("Hello World!");
  }
//+------------------------------------------------------------------+
#ifdef __MQL5__
   #include "..\MQL5\HelloWorld.mqh"
#else
   #include "..\MQL4\HelloWorld.mqh"
#endif
//+------------------------------------------------------------------+

Также обновим основной исходный файл с измененным расположением для нашего базового класса. К этому моменту исходные файлы для обеих версий уже одинаковы:

HelloWorld_Sample.mq4 and HelloWorld_Sample.mq5

#include <MQLx-Intro\Base\HelloWorldBase.mqh>
CHelloWorld hello;
//+------------------------------------------------------------------+
//| Функция инициализации советника                                  |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- 

//--- 
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Функция деинициализации советника                                |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- 

  }
//+------------------------------------------------------------------+
//| Тиковая функция эксперта                                         |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- 
   hello.Greeting("Hello ","World!");
   ExpertRemove();
  }
//+------------------------------------------------------------------+


Наследование

Предположим, что мы хотели бы расширить класс CHelloWorld, который мы определили ранее, до класса под названием CGoodByeWorld. В этом классе для создания сообщения с текстом "Goodbye World!" будет использоваться метод Greeting() из CHelloWorld. Один (рекомендуемый мною) способ это сделать — сослаться на базовый родительский класс, в роли которого выступает CHelloWorldBase. Затем, аналогично CHelloWorldBase, в конец этого файла подключается директива препроцессора условной компиляции, ссылающаяся на правильный класс-наследник. Иерархия наследования будет выглядеть следующим образом:

Иерархия наследования

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

Структура подключения

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

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

/Include

/MQLx-Intro

/Base

HelloWorldBase.mqh

GoodByeWorldBase.mqh

/MQL4

HelloWorld.mqh

GoodByeWorld.mqh

/MQL5

HelloWorld.mqh

GoodByeWorld.mqh


Ниже продемонстрирована имплементация класса CGoodByeWorldBase:

#include "HelloWorldBase.mqh"
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CGoodByeWorldBase : public CHelloWorld
  {
public:
                     CGoodByeWorldBase(void);
                    ~CGoodByeWorldBase(void);
   virtual void      GoodBye(void);
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CGoodByeWorldBase::CGoodByeWorldBase(void)
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CGoodByeWorldBase::~CGoodByeWorldBase(void)
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CGoodByeWorldBase::GoodBye(void)
  {
   Greeting("Goodbye ","World!");
  }
//+------------------------------------------------------------------+
#ifdef __MQL5__
   #include "..\MQL5\GoodByeWorld.mqh"
#else
   #include "..\MQL4\GoodByeWorld.mqh"
#endif
//+------------------------------------------------------------------+

Обратите внимание, что даже если файл включает "HelloWorldBase.mqh", класс CGoodByeWorldBase наследуется от CHelloWorld, а не от CHelloWorldBase. То, какая версия CHelloWorld будет запущена, зависит в конечном итоге от используемой версии MQL-компилятора. В ином случае будет работать расширенная CHelloWorldBase. Тем не менее, в этом примере, поскольку в методе Goodbye() используется метод Greeting(), CGoodByeWorldBase должна будет наследоваться непосредственно от имплементации CHelloWorld, специфичной для платформы.

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

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CGoodByeWorld : public CGoodByeWorldBase
  {
  };
//+------------------------------------------------------------------+

Основной исходный файл так же необходимо обновить, на этот раз создавая объект, унаследованный от CGoodByeWorld и вызывающий GoodBye() внутри обработчика OnTick.

(HelloWorld_Sample.mq4 and HelloWorld_Sample.mq5)

#include <MQLx-Intro\Base\GoodByeWorldBase.mqh>
CGoodByeWorld hello;
//+------------------------------------------------------------------+
//| Функция инициализации советника                                  |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- 

//--- 
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Функция деинициализации советника                                |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- 

  }
//+------------------------------------------------------------------+
//| Тиковая функция эксперта                                         |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- 
   hello.Greeting("Hello ","World!");
   hello.GoodBye();
   ExpertRemove();
  }
//+------------------------------------------------------------------+

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

Hello World!
Goodbye World!
ExpertRemove() function called

Ограничения

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

1. Ограничения для MetaTrader 4.

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

2. Значительные различия в исполнении или условных обозначениях между двумя платформами. 

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

Заключение

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


Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/2569

Прикрепленные файлы |
MQLx-Intro.zip (69.5 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (6)
fxsaber
fxsaber | 17 авг. 2016 в 16:51
Идея (кроссплатформенность) правильная. Но здесь предлагается создать некий свой метаязык и на нем писать кроссплатформенные советники. Метаязык видится лишним в этом решении, т.к. можно писать все на MQL4. И чтобы советники запускались не только на MT4, но и на MT5. Такое решение есть в кодобазе. В частности, при таком решении старые MQ4-коды могут запускаться в MT5 при добавлении одного инклудника. В общем, создание метаязыка видится менее универсальным и даже лишним. Однако, посмотреть продолжение цикла статей на эту тему было бы интересно. Надеюсь, сизов труд не выйдет.

Что касается самого Введения. Резанул пример со StringConcatenate. Сложно понять людей, которые пользуются в MT4/5 этой функцией. Мало того, что это громоздко, так это еще и не наглядно. Объединение строк ВСЕГДА достигалось в обеих платформах через оператор +.  Поэтому использование для строк StringConcatenate сравнимо с использованием функции "NumberSummary" для получения суммы чисел. Абсурд, короче.

Если правильно понял, то это перевод статьи. Поэтому для связи с автором, видимо, нужно писать в оригинал. Английский вариант?
Rashid Umarov
Rashid Umarov | 17 авг. 2016 в 18:17
fxsaber:
Идея (кроссплатформенность) правильная. Но здесь предлагается создать некий свой метаязык и на нем писать кроссплатформенные советники. Метаязык видится лишним в этом решении, т.к. можно писать все на MQL4. И чтобы советники запускались не только на MT4, но и на MT5. 
Если правильно понял, то это перевод статьи. Поэтому для связи с автором, видимо, нужно писать в оригинал. Английский вариант?
Да
Yuriy Asaulenko
Yuriy Asaulenko | 17 авг. 2016 в 18:59
MetaQuotes Software Corp.:

Опубликована статья Кроссплатформенный торговый советник: Введение:

Автор: Enrico Lambino

Извините, но на фига это нужно?
TheXpert
TheXpert | 17 авг. 2016 в 22:40
Yuriy Asaulenko:
Извините, но на фига это нужно?
Чтобы портированием не заниматься
Vasiliy Sokolov
Vasiliy Sokolov | 18 авг. 2016 в 13:19
Кроссплатформенный советник возможно создать только в том случае, если он будет базироваться на кросплатформенном торговом движке, где торговое API и доступ к данным будут заменены ОО-версиями, внутренняя реализация которых будет определятся макросами #ifdef __MQL5__. Учитывая сказанное, статья автора как минимум наивна. Здорово конечно, что автор открыл для себя макрос #ifdef __MQL5__, но этого самого по себе недостаточно. Нужно как минимум написать движок с #ifdef на каждом шагу, а это на порядок сложнее.
Как в MetaTrader 5 быстро разработать и отладить торговую стратегию Как в MetaTrader 5 быстро разработать и отладить торговую стратегию
Скальперские автоматические системы по праву считаются вершиной алгоритмического трейдинга, но при этом они же являются и самыми сложными для написания кода. В этой статье мы покажем, как с помощью встроенных средств отладки и визуального тестирования строить стратегии, основанные на анализе поступающих тиков. Для выработки правил входа и выхода зачастую требуются годы ручной торговли. Но с помощью MetaTrader 5 вы можете быстро проверить любую подобную стратегию на реальной истории.
LifeHack для трейдера: один бэк-тест хорошо, а четыре – лучше LifeHack для трейдера: один бэк-тест хорошо, а четыре – лучше
Перед каждым трейдером при первом одиночном тестировании встает один и тот же вопрос — "Какой же из четырех режимов использовать?" Каждый из предлагаемых режимов имеет свои преимущества и особенности, поэтому сделаем проще - запустим сразу все режимы одной кнопкой! В статье показано, как с помощью Win API и небольшой магии увидеть одновременно все четыре графика тестирования.
Кроссплатформенный торговый советник: повторное использование компонентов из Стандартной библиотеки MQL5 Кроссплатформенный торговый советник: повторное использование компонентов из Стандартной библиотеки MQL5
В Стандартной библиотеке MQL5 есть некоторые компоненты, которые могут оказаться полезными в версиях кроссплатформенных торговых экспертов для MQL4. В этой статье рассматривается метод создания некоторых компонентов Стандартной библиотеки MQL5, совместимых с компилятором MQL4.
Графические интерфейсы X: Обновления для библиотеки Easy And Fast (build 2) Графические интерфейсы X: Обновления для библиотеки Easy And Fast (build 2)
С момента предыдущей публикации статьи этой серии, библиотека Easy And Fast пополнилась новыми возможностями. Проведена частичная оптимизация схемы и кода библиотеки, что немного сократило потребление ресурсов CPU. Некоторые повторяющиеся методы во многих классах элементов были перенесены в базовый класс CElement.