Взаимодействие MetaTrader 5 и MATLAB

Andrey Emelyanov | 26 июля, 2010

Введение

После выхода в свет моей первой статьи "Взаимодействие между MetaTrader 4 и MATLAB Engine (виртуальная машина MATLAB)" она не осталась не замеченной MQL-сообществом, некоторые читатели даже смогли самостоятельно перенести проект с Borland на VS2008 (1Q2W3E4R5T). Но время неутомимо бежит вперед и MetaTrader 4, как это ни печально, уходит в прошлое, уступая место своему преемнику MetaTrader 5 с языком MQL5, в котором появились указатели и динамическая память. 

Благодаря этим нововведениям появилась возможность написания универсальной библиотеки связи с виртуальной машиной MATLAB Engine и напрямую связать сгенерированные MATLAB библиотеки с MetaTrader 5, о чем и пойдет речь в данной статье. Эта статья является логическим продолжением предыдущей, и более детально рассматривает проблему связки MetaTrader 5 с MATLAB.

Чтобы материал данной статьи был более понятен неподготовленным читателям, разобьем статью на три части: теоретическая, справочная, практическая части. В теоретической части ознакомимся с типами данных, принятых в MQL5 и MATLAB, а также способах их взаимного преобразования. В справочной части будем изучать языковые конструкции и синтаксис функций, необходимых для создания DLL. А в практической части разберем "подводные камни" данной связки.

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

1. Теория

1.1 Типы данных в MQL5 и MATLAB

1.1.1 Простые типы данных

Итак, приступим.

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

MQL5
Размер в байтах
Минимальное значение
Максимальное значение
 MATLAB
char
1
-128
127
Array int8/char
uchar
1
0
255
Array int8/char
bool
1
0(false)
1(true)
Array logical
short
2
-32768
32767
Array int16
ushort
2
0
65535
Array int16
int
4
-2147483648
2147483647
Array int32
uint
4
0
4294967295
Array int32
long 8
-9223372036854775808
9223372036854775807 Array int64
ulong 8
0
18446744073709551615
Array int64
float 4
1.175494351e-38
3.402823466e+38
Array single
double
8
2.225073858507201e-308
1.7976931348623158e+308
Array double

Таблица 1. Типы данных в MQL5 и MATLAB

Есть лишь одно существенное различие: переменные в MQL5 могут быть простыми и составными (сложными), а в MATLAB все переменные многомерные (сложные) – т.е. матрицы. Данное различие необходимо всегда помнить!

1.1.2 Сложные типы данных

В MQL5 существует 4 типа сложных данных: массивы, строки (string), структуры, классы. Сложные типы данных состоят из некоторого набора простых типов данных, объединенных в блок памяти определенной длины. При работе с такими данными всегда необходимо знать либо размер блока памяти в байтах, либо количество элементов (исключением, пожалуй, являются классы). Для нас представляет интерес только массивы и строки (string), т.к. передавать в MATLAB классы и структуры MQL5 смысла нет.  

При передаче массивов любого типа необходимо узнать: тип (размерность) и количество элементов, используя функцию ArraySize(). Особое внимание следует уделить индексации в MetaTrader 5 - обычно она задом наперед (т.е. первый элемент содержит более свежие данные, чем последующие), но этот факт необходимо проверять с помощью функции ArrayIsSeries(). А в MATLAB принята следующая индексация: первый элемент содержит более старые данные, чем последующие - поэтому необходимо "переворачивать" массивы перед посылкой в MATLAB, если флаг AS_SERIES = TRUE. На основании изложенного выше, договоримся:

Но это не единственное ограничение при работе с массивами. При работе с многомерными массивами, правильнее сказать с матрицами, особенно с теми, которые получаем из MATLAB, введем ограничение на не более чем 2-мерные массивы. При этом флаг направления AS_SERIES  не может быть TRUE, а значит такие массивы не "переворачиваются".  

Также не следует забывать, что строки в MQL5 не являются массивами символов типа char, поэтому при передаче строк возникает небольшая проблема: в MQL5 принято кодировать строки в Unicode, а в MATLAB принята кодировка ANSI, поэтому перед тем, как передать строку, следует её преобразовать в массив символов ANSI с помощью функции StringToCharArray(). И наоборот, когда получаем из MATLAB массив символов, преобразовываем его функцией CharArrayToString() (см. табл. 2). Для того, чтобы не путаться, договоримся: все строки в MQL5-программе храним в Unicode, никаких массивов типа char.

1.2 Сопоставление типов данных MQL5 и MATLAB

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

 MQL5 
Аналог в MatLab
char 
uchar
Array char
bool
Array logical
short
ushort
int
uint
Array int32
long
ulong
Array int64*
float
double
Array double
string
Array char, через функции StringToCharArray() <=> CharArrayToString()

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

Таблица 2. Сопоставление типов данных в MQL5 и MATLAB

Теперь, когда мы ознакомились с типами данных, принятых в MQL5 и MATLAB, выяснили какие "подводные камни" нас поджидают при передаче данных и как грамотно их обходить, осталось выяснить API интерфейс MATLAB Engine и ознакомиться с компилятором MATLAB версии 4.

2. Справочник по API MATLAB Engine, компилятору MATLAB версии 4 и библиотеке ввода/вывода C++

Данный раздел познакомит вас с наиболее важными функциями API MATLAB Engine, c особенностями компилятора MATLAB версии 4, а также рядом полезных функций из стандартной библиотеки ввода/вывода C++. Итак, начнем.

2.1 API MATLAB Engine и MCR функции

MATLAB Engine — внешний интерфейс, позволяющий сторонним программам использовать рабочий стол MATLAB, обеспечивает полную функциональную работу всех пакетов MATLAB без ограничений.

Хотя и в документации не указано, но с точки зрения системного программиста — это просто виртуальная машина, такая как PHP, MySQL…, поддерживающая простой и относительно быстрый способ обмена данных между MetaTrader 4/5 и MATLAB.  

Данный способ связки внешних программ с пакетом MATLAB рекомендован разработчиками. Интерфейс состоит из шести функций:

Engine *pEng = engOpen(NULL) — функция вызова рабочего стола MATLAB, параметр всегда NULL, возвращает указатель на дескриптор рабочего стола.

int exitCode = engClose(Engine *pEng) — функция закрытия рабочего стола, возвращает число оставшихся пользователей стола MATLAB, где:

mxArray *mxVector = mxCreateDoubleMatrix(int m, int n, int ComplexFlag) — функция создает переменную (матрицу) рабочего стола MATLAB, возвращает указатель на переменную (матрицу), где:

void = mxDestroyArray(mxArray *mxVector) — функция уничтожает матрицу MATLAB, необходима для очистки памяти, где:
int = engPutVariable(Engine *pEng, char *Name, mxArray *mxVector) — функция отправки переменной в рабочий стол. Необходимо не только создавать переменные типа mxArray, но еще отправлять их в MATLAB, где:
mxArray *mxVector = engGetVariable(Engine *pEng, char *Name) — функция получения переменной из рабочего стола - функция, обратная предыдущей. Принимать можно только переменные типа mxArray, где:
double *p = mxGetPr(mxArray *mxVector) — функция получает указатель на массив значений, используется для копирования данных совместно с memcpy (см. 2.3 Стандартная  библиотека ввода/вывода С++), где:
int = engEvalString(Engine *pEng, char *Command) — функция отправки команды рабочему столу, которая будет выполнена рабочим столом MATLAB, где:

Наверное, вы заметили, что API MATLAB Engine позволяет создавать структуры mxArray, только для типа double, однако это ограничение никак не повлияет на Ваши возможности, но отразится на алгоритме работы Вашей библиотеки.

MCR (MCR instance) — особая библиотека пакета MATLAB, обеспечивающая работу автономных приложений/общедоступных библиотек, сгенерированных средой MATLAB на любом компьютере. Стоит отметить, что даже если у вас стоит полный пакет MATLAB, все равно требуется установить библиотеку MCR, запустив файл: MCRInstaller.exe (расположенный в каталоге: <MATLAB>\toolbox\compiler\deploy\win32). Итак, перед вызовом любой функции общедоступной библиотеки, созданной средой MATLAB, необходимо вызвать функцию инициализации MCR:
 
bool = mclInitializeApplication(const char **option, int count) – возвращает true в случае успешного запуска MCR, иначе false, где:

При завершении работы общедоступной библиотеки, необходимо вызвать:
bool = mclTerminateApplication(void) — возвращает true, в случае успешного закрытия MCR.

2.2 Компилятор MATLAB версии 4

Компилятор MATLAB позволяет из M-функций создавать:  

Компилятор поддерживает большинство команд и пакетов MATLAB, однако не весь. Полный список ограничений можно найти на сайте производителя MATLAB. Данный способ позволяет создать "программно-независимую связку" MetaTrader 5 и MATLAB, однако в отличие от MATLAB Engine, требует хорошей подготовки программиста и глубокого знания компиляции.

Компилятор MATLAB требует наличия хотя бы одного из следующих С/С++ - компиляторов:

Компилятор MATLAB версии 4, в отличие от своих предшественников, генерирует только код интерфейса (обертки), т.е. не занимается трансляцией m-функции в двоичный или С/С++ код, но при этом создает специальный файл по технологии Component Technology File (CTF), который содержит объединения различных пакетов, необходимых для обеспечения работы m-функции. Попутно компилятор MATLAB зашифровывает данный файл уникальным (неповторяющимся) 1024-битным ключом.

А теперь рассмотрим алгоритм работы компилятора MATLAB версии 4, т.к. незнание данного вопроса приведет к многочисленным глупым ошибкам на этапе компиляции:

  1. Анализ зависимостей — на данном этапе определяются все функции, MEX-файлы, P-файлы от, которых зависят компилированные m-функции;  
  2. Создание архива — создается файл CTF, он зашифрован и сжат;  
  3. Генерация объектного кода обертки – на данном этапе создаются все исходные тексты, необходимые для создания компоненты:
    • код интерфейса C/C++ для m-функций, указанных в командной строке (NameFile_main.c);
    • файл компоненты (NameFile_component.dat), который содержит всю информацию, необходимую для исполнения m-кода (включая ключи шифрования и пути, сохраненные в CTF-файле);  
  4. С/С++ трансляция. На этом шаге компилируется созданные С/С++ файлы из кода в объектные файлы;
  5. Соединение (линковка). Заключительный этап сборки проекта.

Теперь, когда мы ознакомились с алгоритмом поведения компилятора MATLAB, осталось ознакомиться с ключами, чтобы иметь перед собой подробный план действий при использовании компилятора (mcc):   

Ключ
Назначение
    a filename
 Добавление файла filename в архив, определяет какие файлы будут добавлены в архив CTF
     l
 Макрос, который генерирует библиотеку функций
    N
 Очистить все пути, кроме минимального необходимого набора каталогов
    p <directory>
 Добавление пути трансляции в соответствии с порядком. Требует наличия ключа –N
    R -nojvm
 Отмена опции MCR (MATLAB Component Runtime, компонент времени выполнения, см. справку по MATLAB)
    W
 Управляет созданием оберток функций
    lib
 Создание функции инициализации и завершения
    main
 Создание POSIX оболочки функции main()
    T
 Определяет стадию вывода
    codegen
 Создает код обертки для автономного приложения
    compile:exe
 То же, что codegen
    compile:lib
 Создать код обертки общедоступной библиотеки DLL
    link:exe
 То же, что и compile:exe, плюс линковка
    link:lib
 То же, что и compile:lib, плюс линковка

Таблица 3. Ключи компилятора Matlab mcc (версия 4)

В таблице 3 приведены основные ключи, которые могут понадобится при решении типовых задач. За более подробной справкой обращайтесь к MATLAB через команды help mcc или doc mcc.

Осталось ознакомиться только с линковщиком MATLAB, ниже перечислены основные ключи (mbuild):

 Ключ
Назначение
 -setup
 В интерактивном режиме определение файла опций компилятора для использования по умолчанию в будущих вызовах mbuild
 -g
 Создание программы с отладочной информацией. Добавляет в конец файла DEBUGFLAGS
 -O
 Оптимизация объектного кода

Таблица 4. Ключи линковщика Matlab mbuild (версия 4)

В таблице 4 перечислены основные ключи, за более подробной информацией обращайтесь через команды help mbuild или doc mbuild.

2.3 Стандартная  библиотека ввода/вывода С++

Использование стандартной библиотеки ввода/вывода обеспечивает правильное копирование данных. Её использование убережет вас от "нелепых" ошибок, возникающих на стадии проектирования программы (например: "многие начинающие программисты копируют только указатель на блок памяти, вместо копирования всего блока памяти"). Из всей библиотеки ввода/вывода нас интересует только одна функция:

void *pIn = memcpy (void *pIn, void *pOut, int nSizeByte) – функция копирования (клонирования) переменной/массива из pOut в pIn размером nSizeByte байт, где:

3. Практическая часть

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

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

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

3.1 Разработка универсальной библиотеки связи MetaTrader 5 & MATLAB Engine

Данный способ связки нельзя назвать элегантным и быстрым, но зато это самый надежный и охватывающий весь пакет MATLAB. Конечно, нельзя не упомянуть о быстроте разработки конечной модели. Суть разработки сводится к написанию универсальной библиотеки-обертки для связи MetaTrader 4/5 & MATLAB Engine, после чего скрипт/индикатор/эксперт MetaTrader 4/5 может управлять виртуальным рабочим столом MATLAB. Причем весь программно-математический алгоритм может находиться в MQL-программе и храниться в виде строк, чем можно воспользоваться при защите интеллектуальной собственности (подробности см. в статье "Защищайтесь, господа разработчики!") и также может располагаться в самостоятельных файлах m-функций или P-функций, хранящихся в каталоге < MetaTrader 5>\MQL5\Libraries.  

Возможные области применения такой связки:

Приступим, я надеюсь что вы прочитали для начала 1.1 Типы данных в MQL 5 и MatLab, 1.2 Сопоставление типов данных MQL 5 и MATLAB, 2.1 API MATLAB Engine и 2.3 Стандартная  библиотека ввода/вывода C/С++, т.к. останавливаться и разбирать это уже не будем. Внимательно ознакомьтесь со следующей блок-схемой, которая отображает алгоритм будущей библиотеки:  

Рис. 1. Блок-схема алгоритма библиотеки

Рис. 1. Блок-схема алгоритма библиотеки

Как видно на рис. 1, библиотека состоит из трех основных блоков, рассмотрим их назначение:

Теперь разберемся с алгоритмами, начнем по порядку, с БЛОКА-MQL5, как внимательный читатель уже понял, речь пойдет о реализации того, о чем писалось в разделе 1.1 Типы данных в MQL 5 и MatLab. Если вы его пропустили, то не поймете, зачем все это нужно.

Алгоритм функций mlInput <тип_переменной> практически одинаков, и разберем его работу на функции mlInputDouble(), которая обеспечивает ввод переменных типа double в виртуальную машину MATLAB.

Вот её прототип:

bool mlInputDouble(double &array[],int sizeArray, string NameArray), где:

Алгоритм:

  1. Перевод строки NameArray в массив char с помощью функции StringToCharArray();
  2. Проверяем с помощью функции ArrayIsSeries() тип индексации. Если тип индексации обычный — передаем значение функции mlxInputDouble();
    ИНАЧЕ индексация массива тайм-серия:
    "переворачиваем" массив и передаем значение функции mlxInputDouble();
  3. Завершаем работу функции, передаем возвращаемое значение функции mlxInputDouble();

Алгоритм функций mlGet <тип_переменной> тоже практически одинаков. Разберем его на примере функции mlGetDouble(), которая возвращает переменную типа double из виртуальной машины MATLAB.

Её прототип: 

int mlGetDouble(double &array[],int sizeArray, string NameArray), где:

Алгоритм:

  1. Перевод строки NameArray в массив char, с помощью функции StringToCharArray();   
  2. Выясняем размер массива с помощью функции mlxGetSizeOfName();
    • ЕСЛИ размер БОЛЬШЕ НУЛЯ, выделить массив-приемник необходимого размера с помощью функции ArrayResize(), получить данные mlxGetDouble(), вернув размер массива;
    • ЕСЛИ размер НУЛЬ вернуть ошибку, т.е. значение нуль.  

Вот и все! Отличие mlGetInt() и mlGetLogical() в том, что производят "теневое" преобразование типов из double -> int/bool, для чего указанные функции создают временный буфер памяти в своем теле. Данная мера вынужденная, т.к. к сожалению API интерфейс MATLAB не позволяет создавать структуры mxArray для типов данных, отличных от double. Однако это не означает, что MATLAB оперирует исключительно double.

С Блоком-C/C++ все значительно проще  – он должен обеспечивать трансляцию данных с типа double в структуру mxArray, что осуществляется с помощью функций mxCreateDoubleMatrix(), mxGetPr() и memcpy(). После чего с помощью функции engPutVariable()  он передает данные в виртуальную машину MATLAB, а при извлечении данных использует функцию  engGetVariable(). И опять же, обратите внимание на функции с приставками Int и Logical — как видно на БЛОК-СХЕМЕ, напрямую не общаются с MATLAB, а используют функции: mlxInputDouble/mlxGetDouble и mlxInputChar(). Алгоритм поведения их прост: вызов функции mlxInputDouble/mlxGetDouble — ввод/вывод значений как double(!), и отправка "теневой" команды MATLAB на преобразование типа данных через функцию mlxInputChar().

С Блоком MATLAB Engine все еще проще, он обеспечивает выполнение лишь математических функций. Его поведение напрямую зависит от Ваших команд и от Ваших m/p-функций.  

Теперь, когда все "тонкости" реализации проекта ясны, пора разобраться со сборкой проекта.

Любая подобная сборка начинается с создания главной библиотеки — в нашем случае это БЛОК-С/С++. Для этого в любом ANSI-текстовом редакторе (Блокнот, Bred, и т.п.) создадим файл с расширением DEF. Желательно, чтобы имя данного файла состояло из латинских символов, без пробелов и знаков, иначе вы"услышите" от своего компилятора много лестных "слов" в свой адрес…  Данный файл обеспечивает неизменность ваших функций, в случае его отсутствия компилятор С/С++ придумает свои "экзотические имена" экспортным функциям.

Данный файл содержит: LIBRARY — управляющие слово "имя библиотек", LibMlEngine —  имя библиотеки, и второе управляющее слово EXPORTS — дословно "экспорт", после этого слова идут имена функций. Как вы, наверное, поняли, имена экспортных функций не могут быть написаны русским шрифтом, иметь пробелы и знаки. Вот текст файла DllUnit.def из архива MATLABEngine.zip:  

LIBRARY LibMlEngine
EXPORTS
mlxClose
mlxInputChar
mlxInputDouble
mlxInputInt
mlxInputLogical
mlxGetDouble
mlxGetInt
mlxGetLogical
mlxGetSizeOfName
mlxOpen

Итак, первый файл проекта есть, теперь открываем проводник и заходим в папку <MATLAB>\extern\include. Копируем файл engine.h (заголовочный файл виртуальной машины MATLAB) в каталог, где собираем проект (не сделаете этого, будете указывать путь к этому файлу вручную на этапе компиляции).

Теперь пора перейти к созданию БЛОКА-С/С++. Весь текст программы в статье писать не будем, т.к. данный файл есть в архиве MATLABEngine.zip под именем DllUnit.cpp и хорошо комментирован. Но стоить отметить, что функции все таки лучше создавать __stdcall — т.е. передача параметров через стек, функция чистит стек. Данный стандарт "родной" для API Win32/64.

Рассмотрим, как объявить функцию:

  extern "C" __declspec(dllexport) <тип_переменной> __stdcall Function(<тип> <имя>);

  1. extern "C" __declspec(dllexport) — говорит компилятору С++, что функция внешняя.  
  2. <тип_переменной> — тип возвращаемой переменной, может быть: void, bool, int, double, составные типы (известные не только Dll, но и вызывающей программе) и указатели;
  3.  __stdcall — соглашение о передаче параметров в функцию и обратно, стандарт для API Win32/64;  
  4. Function — имя Вашей функции;  
  5. <тип> <имя> — тип и имя входной переменной, максимальное количество переменных - 64.

Более подробно данная тема раскрывается в статье "Как за 10 минут написать DLL библиотеку для MQL5 и обмениваться данными?".

Сборка БЛОКА-С/С++: для этого необходимо подключить стандартную библиотеку ввода/вывода и добавить в проект следующие файлы (команда в Вашем компиляторе: Project->Add Project):

  1. DllUnit.def
  2. В каталоге <MATLAB>\extern\lib\<win32/64>\<компилятор>\, где:
    <MATLAB> — главный каталог MATLAB;
    <win32/64> — либо папка win32 для 32 битных ОС, либо win64 для 64 битных ОС;
    <компилятор> — папка "borland" – Borland C/C++ ver. 5-6, папка "microsoft" - Microsoft Visual C++:  
    • libeng.lib
    • libmx.lib

У многих возник наверно вопрос "У меня компилятор другой версии или нет такого компилятора в списке! (Очень редко данных файлов вообще нет)". Давайте рассмотрим процесс ручного создания общедоступных библиотек. Будет рассмотрено, как это делается в Visual C++ и Borland C++:

  1. открываем FAR’ом папку <MATLAB>\bin\<win32/64>, где:
    <MATLAB> — главный каталог MATLAB;
    <win32/64> — либо папка win32 для 32 битных ОС, либо win64 для 64 битных ОС;  
  2. Если Borland C++: команда implib libeng.lib libeng.dll ; так же для libmx.dll
  3. Если Visual C++: команда lib libeng.dll ; так же для libmx.dll
  4. Если другой компилятор: любой компилятор любого языка программирования должен иметь в своем составе данную утилиту, обычно данная консольная программа располагается <каталог_компилятора>\bin\*lib*.exe — по-английски звучит название так: "Library Manager".

Кстати, забыл предупредить, не вздумайте делать 64-битные LIB для 32-битного компилятора, сначала узнайте в справке на компилятор о поддержки 64-битной адресации, если таковой нет, либо ищите 32-битные DLL MATLAB, либо меняйте С/С++ компилятор. Приступаем к компиляции, после которой у нас получается библиотека, которую необходимо разместить в каталоге: Каталог_терминала\MQL5\Libraries.

Теперь можно приступить БЛОКУ-MQL. Для этого запустим MetaEditor, выберем команду "Создать" и выполним действия, как показано на рисунке:  

Рис. 2. Мастер MQL5: Создание библиотеки

Рис. 2. Мастер MQL5: Создание библиотеки

Рис. 3. Мастер MQL5: Общие параметры библиотеки

Рис. 3. Мастер MQL5: Общие параметры библиотеки

Теперь, когда Мастер MQL5 создал шаблон, приступим к его редактированию:

1. Необходимо описать импорт функций:

//+------------------------------------------------------------------+
//| ОБЪЯВЛЕНИЕ ИМПОРТИРУЕМЫХ ФУНКЦИЙ                                 |
//+------------------------------------------------------------------+
#import "LibMlEngine.dll"
void   mlxClose(void);                        //void – означает: не передаем никаких параметров!
bool   mlxOpen(void);                         //void – означает: не передаем и не получаем никаких параметров!
bool   mlxInputChar(char &CharArray[]);       //char& CharArray[] – означает: передаем ссылку!
bool   mlxInputDouble(double &dArray[],
                      int sizeArray,
                      char &CharNameArray[]);
bool   mlxInputInt(double &dArray[],
                   int sizeArray,
                   char &CharNameArray[]);
bool   mlxInputLogical(double &dArray[],
                       int sizeArray,
                       char &CharNameArray[]);
int    mlxGetDouble(double &dArray[],
                    int sizeArray,
                    char &CharNameArray[]);
int    mlxGetInt(double &dArray[],
                 int sizeArray,
                 char &CharNameArray[]);
int    mlxGetLogical(double &dArray[],
                     int sizeArray,
                     char &CharNameArray[]);
int    mlxGetSizeOfName(char &CharNameArray[]);
#import    

Обратите внимание, что передавать "указатели" в MQL 5 можно двумя способами:

  • void NameArray[] ;   // При таком способе передачи из массива можно только читать данные. Однако, если такую ссылку попытаться использовать для "редактирования содержимого", возникнет ошибка доступа к памяти (в лучшем для вас случае, MetaTrader 5 "терпеливо промолчит", обработав ошибку незаметно в SEH-фрейме, а вот мы свой SEH-фрейм НЕ ПИСАЛИ, поэтому можем даже причину ошибки не узнать);
  • void& NameArray[] ; // При таком способе передачи над массивом можно осуществлять не только операцию чтения, но и даже операцию редактирования содержимого, но при этом размеры массива должны быть сохранены.

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

2. Описывать все функции БЛОКА-MQL нет смысла, т.к. исходный текст есть в архиве MATLABEngine.zip под именем MatlabEngine.mq5.

Поэтому рассмотрим особенности объявления и определения внешних функций в MQL5:

bool mlInputChar(string array)export
{
//… тело функции
}

Как видно в примере, объявление и определение функции объединены. В данном случае мы объявляем функцию с именем mlInputChar()  внешней (export), возвращающей значение типа bool  и принимающей строку array в качестве параметра. Осталась только компиляция…

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

Для этого напишем простой проверочный скрипт (либо возьмем его из архива MATLABEngine.zip, файл: TestMLEngine.mq5).

Код скрипта простой и хорошо комментирован:

#property copyright "2010, MetaQuotes Software Corp."
#property link      "https://www.mql5.com/ru"
#property version   "1.00"
#import "MatlabEngine.ex5"
bool mlOpen(void);
void mlClose(void);
bool mlInputChar(string array);
bool mlInputDouble(double &array[],
                   int sizeArray,
                   string NameArray);
bool mlInputInt(int &array[],
                int sizeArray,
                string NameArray);
int mlGetDouble(double &array[],
                string NameArray);
int mlGetInt(int &array[],
             string NameArray);
bool mlInputLogical(bool &array[],
                    int sizeArray,
                    string NameArray);
int mlGetLogical(bool &array[],
                 string NameArray);
int mlGetSizeOfName(string strName);
#import
void OnStart()
  {
// Динамичские буфера, для вывода из MATLAB
   double dTestOut[];
   int    nTestOut[];
   bool   bTestOut[];
// Переменные для ввода в среду MATLAB
   double dTestIn[] = {   1,     2,    3,     4};
   int    nTestIn[] = {   9,    10,   11,    12};
   bool   bTestIn[] = {true, false, true, false};
   int nSize=0;
// Имена переменных и командная строка
   string strComm="clc; clear all;"; // командная строка - очистить экран и переменные
   string strA     = "A";            // переменная A
   string strB     = "B";            // переменная B
   string strC     = "C";            // переменная C
/*
   ** 1. ЗАПУСК DLL
   */
   if(mlOpen()==true)
     {
      printf("Matlab загружен");
     }
   else
     {
      printf("Matlab ERROR! Ошибка загрузки.");
      mlClose();
      return;
     }
/*
   ** 2. ПЕРЕДАЧА КОМАНДНОЙ СТРОКИ
   */
   if(mlInputChar(strComm)==true)
     {
      printf("Комадная строка переданна в Matlab");
     }
   else printf("ERROR! Ошибка при передачи строки");
/*
   ** 3. ПЕРЕДАЧА ПЕРЕМЕННОЙ ТИПА DOUBLE
   */
   if(mlInputDouble(dTestIn,ArraySize(dTestIn),strA)==true)
     {
      printf("Переменная типа double передана в MATLAB");
     }
   else printf("ERROR! При передачи строки типа double");
/*
   ** 4. ПОЛУЧЕНИЕ ПЕРЕМЕННОЙ ТИПА DOUBLE
   */
   if((nSize=mlGetDouble(dTestOut,strA))>0)
     {
      int ind=0;
      printf("Переменная A, тип: double получена в MATLAB, размером = %i",nSize);
      for(ind=0; ind<nSize; ind++)
        {
         printf("A = %g",dTestOut[ind]);
        }
     }
   else printf("ERROR! Переменная типа double не получена");
/*
   ** 5. ПЕРЕДАЧА ПЕРЕМЕННОЙ ТИПА INT
   */
   if(mlInputInt(nTestIn,ArraySize(nTestIn),strB)==true)
     {
      printf("Переменная типа int передана в MATLAB");
     }
   else printf("ERROR! При передачи строки типа int");
/*
   ** 6. ПОЛУЧЕНИЕ ПЕРЕМЕННОЙ ТИПА INT
   */
   if((nSize=mlGetInt(nTestOut,strB))>0)
     {
      int ind=0;
      printf("Переменная B, тип: int получена в MATLAB, размером = %i",nSize);
      for(ind=0; ind<nSize; ind++)
        {
         printf("B = %i",nTestOut[ind]);
        }
     }
   else printf("ERROR! Переменная типа int не получена");
/*
   ** 7. ПЕРЕДАЧА ПЕРЕМЕННОЙ ТИПА BOOL
   */
   if(mlInputLogical(bTestIn,ArraySize(bTestIn),strC)==true)
     {
      printf("Переменная типа bool передана в MATLAB");
     }
   else printf("ERROR! При передачи строки типа bool");
/*
   ** 8. ПОЛУЧЕНИЕ ПЕРЕМЕННОЙ ТИПА BOOL
   */
   if((nSize=mlGetLogical(bTestOut,strC))>0)
     {
      int ind=0;
      printf("Переменная C, тип: bool получена в MATLAB, размером = %i",nSize);
      for(ind=0; ind<nSize; ind++)
        {
         printf("C = %i",bTestOut[ind]);
        }
     }
   else printf("ERROR! Переменная типа bool не получена");
/*
   ** 9. ЗАВЕРШЕНИЕ РАБОТЫ
   */
   mlClose();
  }

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

Теперь, когда мы окончательно разобрались с виртуальной машиной MATLAB, можно приступить к использованию DLL-библиотек, созданных средой MATLAB.

3.2 Технические рекомендации по сборке/использовании DLL-библиотек, сгенерированных компилятором MATLAB версии 4

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

Для примера возьмем простой индикатор — скользящую среднюю (SMA) — и "немного" модернизируем его, добавив Нейронно-Сетевой Фильтр (тип НС: GRNN), позволяющий сглаживать помехи "белого шума" (случайные всплески). Новый индикатор назовем NeoSMA, а НС-фильтр GRNNFilter.  

Итак, мы получили две m-функции, из которых мы хотим создать одну DLL-библиотеку, причем которую можно вызывать из MetaTrader 5.

Теперь вспомним, что MetaTrader 5 ищет DLL-библиотеки в следующих каталогах:

Поэтому перемещаем в один из указанных каталогов две m-функции (NeoSMA.m и GRNNFilter.m), где и будем производить сборку DLL-библиотеки. Обращаю ваше внимание на данный факт перемещения, т.к. это сделано не случайно. Внимательный читатель уже знает особенность компилятора MATLAB, которая заключается в том, что он сохраняет значения путей при компиляции (см.  раздел "2.2 Компилятор MATLAB версии 4").

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

  1. Введем в командной строке MATLAB команду: mbuild –setup;
  2. Введем ‘y’ для подтверждения поиска совместимых С/С++ - компиляторов установленных в системе;
  3. Выберем стандартный С-компилятор Lcc-win32 C;
  4. Подтвердим правильность выбора компилятора, введем ‘y’.

Рис. 4. Компиляция проекта

Рис. 4. Компиляция проекта


Теперь мы готовы перейти к процессу компиляции m-функций.

Для этого введем команду:

mcc -N -W lib:NeoSMA -T link:lib  NeoSMA.m GRNNFilter.m

Поясним ключи:

-N                                     —  ввели для того, чтобы компилятор откинул все лишние пути
-W lib:NeoSMA                   —  ключ говорит компилятору, что имя библиотеки NeoSMA
-T link:lib                           —  ключ сообщает компилятору, что создается общедоступная библиотека, с линкованием
NeoSMA.m и GRNNFilter.m  —  имена m-функций

Итак, давайте разберемся, что же создал компилятор:

Итак, давайте разберемся с DLL библиотекой, а именно с её внутренним строением. Она состоит (только основные функции) из:

  1. Главной функции любой DLL-библиотеки BOOL WINAPI DllMain(), которая, согласно спецификации Microsoft, обрабатывает события происходящие с DLL: загрузка DLL в адресное пространство процесса, создание нового потока, уничтожение потока и выгрузка из памяти Dll;  
  2. Служебных функций инициализации/деинициализации DLL: BOOL <NameLib>Initialize(void)/void <NameLib>Terminate(void) — необходимы для запуска/выгрузки среды окружения Math Work перед началом использования библиотечных функций и в конце их использования;
  3. Экспортных m-функций – void mlf<NameMfile>(int <number_of_return_values>, mxArray **<return_values>, mxArray *<input_values>,…), где:
    • <number_of_return_values> — число возвращаемых переменных (не путать с размером массива и т.п.);
    • mxArray **<return_values> — адрес структуры mxArray куда возвратят результат работы m-функции;
    • mxArray *<input_values> — указатель на структуру mxArray входной переменной m-функции.
     

Как вы уже сами увидели, экспортные m-функции содержат адреса и указатели на структуру mxArray, и напрямую вызвать такие функции из MetaTrader 5 нельзя, т.к. MetaTrader 5 не поймет такой тип данных. Описывать в MetaTrader 5 структуру mxArray не имеет никакого здравого смысла, т.к. разработчики MATLAB  не гарантируют её неизменность во времени, даже в пределах одной версии продукта, поэтому необходимо написать простенькую DLL-переходник.

Её блок-схема показана ниже:

Рис. 5. Блок-схема DLL-переходника

Рис. 5. Блок-схема DLL-переходника

Она очень похожа на правую часть DLL-библиотеки для MATLAB Engine, поэтому её алгоритм разбирать не будем, чтобы не повторяться, а перейдем сразу к коду. Для этого в вашем компиляторе С/С++ создаем два маленьких файла:  

nSMA.cpp (из архива DllMatlab.zip):  

#include <stdio.h>
#include <windows.h>
/* Включение заголовочного файла MCR и заголовочного файла библиотеки*/
#include "mclmcr.h"
#include "NEOSMA.h"
/*---------------------------------------------------------------------------
** Глобальные функции DLL(внешние)
*/
extern "C" __declspec(dllexport) bool __stdcall IsStartSMA(void);
extern "C" __declspec(dllexport) bool __stdcall nSMA(double *pY,  int  nSizeY,
                                                     double *pIn, int nSizeIn,
                                                     double   dN, double dAd);
/*---------------------------------------------------------------------------
** Глобальные переменные
*/
mxArray *TempY;
mxArray *TempIn;
mxArray *TempN;
mxArray *TempAd;
bool bIsNeoStart;
//---------------------------------------------------------------------------
int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void* lpReserved)
{
    switch(reason)
    {
        case DLL_PROCESS_ATTACH:
         bIsNeoStart = false;
         TempY  = 0;   //Обнулили указатели на буфера
         TempN  = 0;
         TempIn = 0;
         TempAd = 0;
         break;
        case DLL_PROCESS_DETACH:
         NEOSMATerminate();
         //Удаляем старые данные, перед выходом из DLL
         if(TempY  != NULL) mxDestroyArray(TempY);
         if(TempN  != NULL) mxDestroyArray(TempN);
         if(TempIn != NULL) mxDestroyArray(TempIn);
         if(TempAd != NULL) mxDestroyArray(TempAd);
         mclTerminateApplication();
    }
    return 1;
}
//---------------------------------------------------------------------------
bool __stdcall IsStartSMA(void)
{
 if(bIsNeoStart == false)
 {
  if(!mclInitializeApplication(NULL,0) )
  {
   MessageBoxA(NULL, (LPSTR)"Can't start MATLAB MCR!",
               (LPSTR) "MATLAB DLL: ERROR!", MB_OK|MB_ICONSTOP);
   return false;
  }else
   {
    bIsNeoStart = NEOSMAInitialize();
   };
 };
 return bIsNeoStart;
}
//---------------------------------------------------------------------------
bool __stdcall nSMA(double *pY, int nSizeY, double *pIn, int nSizeIn, double dN, double dAd)
{
   /*
   ** Создаем буферы
   */
   if(TempN == NULL){ TempN = mxCreateDoubleMatrix(1, 1, mxREAL);}
   else
   {
     mxDestroyArray(TempN);
     TempN= mxCreateDoubleMatrix(1, 1, mxREAL);
   };
   if(TempIn == NULL){ TempIn = mxCreateDoubleMatrix(1, nSizeIn, mxREAL);}
   else
   {
     mxDestroyArray(TempIn);
     TempIn= mxCreateDoubleMatrix(1, nSizeIn, mxREAL);
   };
   if(TempAd == NULL){ TempAd = mxCreateDoubleMatrix(1, 1, mxREAL);}
   else
   {
     mxDestroyArray(TempAd);
     TempAd= mxCreateDoubleMatrix(1, 1, mxREAL);
   };
   /*
   ** Создание данных для обработки
   */
   memcpy((char *)mxGetPr(TempIn), (char *) pIn, (nSizeIn)*8);
   memcpy((char *)mxGetPr(TempN), (char *) &dN, 8);
   memcpy((char *)mxGetPr(TempAd), (char *) &dAd, 8);
   /*
   ** Передача и получение ответа от m-функции
   */
   if(mlfNeoSMA(1, (mxArray **)TempY, (mxArray *)TempIn, (mxArray *)TempN
      , (mxArray *)TempAd) == false) return false;
   /*
   ** Возвращаем вычисленный вектор из m-функции, очищаем буфера
   */
   memcpy((char *) pY, (char *)mxGetPr(TempY), (nSizeY)*8);
   mxDestroyArray((mxArray *)TempY);  TempY  = 0;
   mxDestroyArray((mxArray *)TempN);  TempN  = 0;
   mxDestroyArray((mxArray *)TempIn); TempIn = 0;
   mxDestroyArray((mxArray *)TempAd); TempAd = 0;
   return true;
}

nSMA.def (из архива DllMatlab.zip):

LIBRARY nnSMA
EXPORTS
IsStartSMA
nSMA


Собираем проект в вашем компиляторе С/С++: для этого необходимо подключить стандартную библиотеку ввода/вывода и добавить в проект следующие файлы (команда в вашем компиляторе: Project->Add Project):

  1. nSMA.def
  2. В каталоге <MATLAB>\extern\lib\<win32/64>\<компилятор>\, где:
    <MATLAB> — главный каталог MATLAB; 
    <win32/64> — либо папка win32 для 32 битных ОС, либо win64 для 64 битных ОС;
    <компилятор> — папка "borland" – Borland C/C++ ver. 5-6, папка "microsoft" - Microsoft Visual C++ (у меня файлы для 6 версии):  
    • libmx.lib
    • mclmcr.lib
  3. NeoSMA.lib — создаем самостоятельно (см. раздел 3.1 Разработка универсальной библиотеки связи MetaTrader 5 & MATLAB Engine).  

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

Вот список файлов и пути размещения на целевой машине:

Как уже догадались многие продвинутые программисты, целесообразно использовать программы-установщики (SETUP), их достаточное количество в Internet, в том числе есть и бесплатные продукты.

Осталось протестировать данную DLL в MetaTrader 5. Для этого этого напишем простой скрипт (TestDllMatlab.mq5 из архива DllMatlab.zip):

#property copyright "2010, MetaQuotes Software Corp."
#property link      "nav_soft@mail.ru"
#property version   "1.00"
#import "nnSMA.dll"
bool  IsStartSMA(void);
bool  nSMA(double  &pY[],
           int    nSizeY,
           double &pIn[],
           int   nSizeIn,
           double     dN,
           double   dAd);
#import
datetime    Time[];    // дин. массив координат времени
double      Price[];   // дин. массив цены
double      dNeoSma[]; // дин. массив цены
void OnStart()
  {
   int ind=0;
// запуск Dll
   if(IsStartSMA()==true)
     {
      //--- Создание, заполнение массивов
      CopyTime(Symbol(),0,0,301,Time);   // временной массив + 1
      ArraySetAsSeries(Time,true);       // получение времени графика
      CopyOpen(Symbol(),0,0,300,Price);  // ценовой массив
      ArraySetAsSeries(Price,true);      // получение цен открытий
      ArrayResize(dNeoSma,300,0);        // резервирование места под ответ функции
                                         // получение данных
      if(nSMA(dNeoSma,300,Price,300,1,2)==false) return;
      // уточняем ориентацию массива
      ArraySetAsSeries(dNeoSma,true);
      // отрисовываем данные на графике
      for(ind=0; ind<ArraySize(dNeoSma);ind++)
        {
         DrawPoint(IntegerToString(ind,5,'-'),Time[ind],dNeoSma[ind]);
        }
     }
  }
//+------------------------------------------------------------------+
void DrawPoint(string NamePoint,datetime x,double y)
  {  // Готовность: 100%. Функция отрисовки данных на графике. Рисование стрелками.
// Основные свойства объекта графика
   ObjectCreate(0,NamePoint,OBJ_ARROW,0,0,0);
   ObjectSetInteger(0, NamePoint, OBJPROP_TIME, x);        // координата по времени х
   ObjectSetDouble(0, NamePoint, OBJPROP_PRICE, y);        // координата по цене y
// Дополнительные свойства объекта графика
   ObjectSetInteger(0, NamePoint, OBJPROP_WIDTH, 0);       // толщина линии
   ObjectSetInteger(0, NamePoint, OBJPROP_ARROWCODE, 173); // тип стрелки
   ObjectSetInteger(0, NamePoint, OBJPROP_COLOR, Red);     // цвет стрелки
  }
//+------------------------------------------------------------------+

Заключение

Итак, мы ознакомились с тем как создать универсальную библиотеку для связи MetaTrader 5 & MATLAB, и как подключить DLL-библиотеку созданную средой MATLAB. Но остались ещё неосвещенные интерфейсы взаимодействия MetaTrader 5 & MATLAB, их освещать не имеет смысла, т.к. в данная статья "не резиновая". Рассматриваемый вопрос освещен в полном объеме, т.к. выбраны самые эффективные способы взаимодействия, не требующие создания особых видов "переходников". Хотя вы можете пойти и "другим путем", например по технологии .NET — МetaTrader 5. Экспорт котировок в .NET приложение, используя WCF сервисы.

У многих возник вопрос: какой же способ связки выбрать из описываемых в статье? Ответ прост – оба, т.к. на этапе проектирования/отладки математической модели скорость не нужна, однако нужно, чтобы вся мощь MATLAB была задействована без "особых производственных затрат" на программирование. Тут, естественно, поможет MATLAB Engine. Однако, когда математическая модель будет отлажена и готова к использованию, потребуется скорость, многозадачность (работа индикатора и/или MTC на нескольких ценовых графиках) – тут без сомнения поможет DLL-библиотека, созданная средой MATLAB.

Но все вышесказанное не обязывает вас этому следовать, ответ на этот вопрос должен каждый дать самостоятельно, опираясь в первую очередь на отношение "затраты на программирование" к масштабности проекта (количество пользователей индикатора и/или MTC). Нет никакого смысла создавать Dll в среде MATLAB для одного, двух пользователей (легче на двух компьютерах установить MATLAB).  

У многих читателей, кто мало знаком с MATLAB, наверно есть вопрос: зачем все это? Ведь MQL5 и так имеет математические функции! Применение MATLAB позволит вам без особых усилий реализовать свои математические идеи, вот лишь неполный список возможностей:  

Так что все в ваших руках, и не забывайте: "МАТЕМАТИКА БЫЛА И БУДЕТ ЦАРИЦЕЙ НАУК", а пакет MATLAB — ваш научный калькулятор.

Литература

   1. Встроенная справка по MATLAB.

   2. Встроенная справка по MQL5.
   3. Н. Н. Мартынов, А. П. Иванов. MATLAB 5.x. Вычисления. Визуализация. Программирование.
   4. М. Л. Подкур, П. Н. Подкур, Н. К. Смоленцев. Программирование в среде Borland C++ Builder с математическими библиотеками MATLAB C/C++.
   5. А.Я. Архангельский. C++ Builder 6. Справочное пособие.
   6. Дж. Рихтер. Windows для профессионалов. Создание эффективных Win32-пpилoжeний с учетом специфики 64-разрядной версии Windows.