下载MetaTrader 5

MetaTrader 5 和 MATLAB 交互

4 十二月 2013, 07:18
Andrey Emelyanov
0
1 605

简介

我的第一篇文章《MetaTrader 4 与 MATLAB Engine(虚拟 MATLAB 机)交互》在 MQL 社区中引起了部分读者的关注。有些读者 (1Q2W3E4R5T) 甚至将此项目从 Borland 移植到 VS2008。然而时光荏苒(伤感但真实),MetaTrader 4 已不复存在并让位于使用 MQL5 的继任者 MetaTrader 5,MQL5 中引入了指针和动态内存。

由于这些创新,我们才有了编写与 MATLAB Engine 虚拟机交互的通用库以及将 MATLAB 生成的库直接与 MetaTrader 5 链接的机会。本文正是介绍这些功能。本文在逻辑上与前作接续,并更加彻底地阐述了 MetaTrader 5 和 MATLAB 间的交互问题。

为使本文涉及的内容对于尚未准备好的读者而言更容易理解,我们将文章分成了三部分:理论、参考和实践。“理论”部分将介绍 MQL5 和 MATLAB 中使用的数据类型,以及它们的相互转换。在“参考”部分,您将学习创建 DLL 所必需的函数的语言结构和语法。而在“实践”部分,我们将分析该交互的“陷阱”。

经验丰富的读者可跳过“理论”和“参考”部分,直接从“实践”部分开始。强烈要求其他读者在阅读“理论”和“参考”部分后再继续阅读“实践”部分。此外,“参考文献”部分提到的书籍也值得一读。

1. 理论

1.1 MATLAB 和 MQL5 中的数据类型

1.1.1 简单数据类型

我们开始吧。

首先,我们需要开始了解 MQL5 和 MATLAB 的内部世界。在大致检查变量类型后,我们得出结论,它们几乎是相同的:

MQL5
字节大小
最小值
最大值
 MATLAB
char
1
-128
127
Array int8/char
uchar
1
0
255
Array int8/char
bool
1
0(假)
1(真)
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. MATLAB 和 MQL5 中的数据类型

有一个主要的差异:MQL5 中的变量可以是简单变量也可以是复合(复杂)变量,而在 MATLAB 中,所有变量都是多维(复杂)的,也就是矩阵。您应始终牢记这一差异!

1.1.2 复杂数据类型

在 MQL5 中,有 4 种复杂数据类型:数组、字符串、结构和类。复杂数据类型是几个简单数据类型的集合,组合成一定长度的内存块。处理这种数据时,始终需要知道内存块的字节大小,或元素的数量(类除外)。我们只对数组和字符串感兴趣,因为提交类和 MQL5 结构至 MATLAB 是没有意义的。

当传递任意类型的数组时,您需要知道:类型(维度)和元素数量,这通过使用 ArraySize() 函数获得。应特别注意 MetaTrader 5 中的索引编排 - 通常它是向后的(即,第一个元素包含的数据比后面的元素包含的要新)。使用 ArrayIsSeries() 函数检查这一点。而 MATLAB 具有如下的索引编排:第一个元素包含的数据比后面的元素包含的要旧 - 因此,如果标志 AS_SERIES 为 TRUE,您必须在发送数组至 MATLAB 前将其“反转”。基于上述内容,我们得出以下观点:

  • “反转”数组对于 MQL5 程序“不可见”,字符类型的数组和二维数组除外 - 让它们保持不变。
  • 反转对于 MATLAB 的所有数组“不可见”,并将 TRUE 分配给 AS_SERIES 标志,字符类型的数组和二维数组除外 - 让它们保持不变。
  • 在 MQL5 程序中,在根据“向后”索引编排创建的每个数组中 AS_SERIES 标志必须为 TRUE,字符类型的数组和二维数组除外 - 让它们保持不变。  

但这不是使用数组的唯一限制。当您使用多维数组(更正确的说法是矩阵)时,尤其是从 MATLAB,我们引入不超过二维数组的限制。此处 AS_SERIES 标志不能为 TRUE,因此这种数组没有“反转”。

不要忘记 MQL5 中的字符串不是字符类型元素的数组。因此,在传递字符串时会有一个小问题:在 MQL5 中,字符串使用 Unicode 编码,而 MATLAB 使用 ANSI 编码。因此,在您传递字符串前,应使用 StringToCharArray() 函数将字符串转换为 ANSI 字符的数组。反之亦然,如果您从 MATLAB 获得字符数组,使用 CharArrayToString() 函数对其进行转换(请参见表 2)。为避免混淆,我们约定:在 MQL5 程序中使用 Unicode 存储所有字符串,没有字符类型的数组。

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 中使用的数据类型。您知道数据传递中有哪些“陷阱”以及如何有效地避开它们。您还需了解 MATLAB Engine API 和熟悉 MATLAB Compiler 4。

2. MATLAB Engine API 参考、MATLAB Compiler 4 参考和 C++ 输入/输出库参考

本节介绍 MATLAB Engine API 最重要的函数、MATLAB Compiler 4 的功能以及 C++ 标准输入/输出库有用函数的数量。那么,我们开始吧。

2.1 MATLAB Engine API 和 MCR 函数

MATLAB Engine - 是其他程序使用 MATLAB 桌面的外部接口。它提供所有 MATLAB 包的全功能工作,没有任何限制。

虽然未在文档中明言,但用系统编程人员的话来说,它就是一个类似 PHP、MySQL 等的虚拟机,在 MetaTrader 4/5 和 MATLAB 之间提供一种简单和相对快速的方法来交换数据。  

此连接外部程序和 MATLAB 包的方法是开发人员所推荐的。接口由六个函数组成:

Engine *pEng = engOpen(NULL) - 此函数调用 MATLAB 桌面,其参数始终为 NULL,并返回一个指向桌面描述符的指针。

int exitCode = engClose(Engine *pEng) - 此函数关闭桌面,返回 MATLAB 桌面的剩余用户数量,其中:

  • Engine *pEng - 指向桌面描述符的指针。  

mxArray *mxVector = mxCreateDoubleMatrix(int m, int n, int ComplexFlag) - 此函数创建 MATLAB 桌面的一个变量(矩阵),并返回指向该变量(矩阵)的指针,其中:

  • mxArray *mxVector - 指向矩阵变量的指针。  
  • int m - 行数。  
  • int n - 列数。  
  • ComplexFlag - MetaTrader 4/5 mxREAL 复数的类型。
void = mxDestroyArray(mxArray *mxVector) - 此函数用于销毁 MATLAB 矩阵,它是清除内存所必需的,其中:
  • mxArray *mxVector - 指向矩阵变量的指针。  
int = engPutVariable(Engine *pEng, char *Name, mxArray *mxVector) - 此函数发送变量至桌面。您不仅要创建 mxArray 类型的变量,还要把它们发送至 MATLAB,其中:
  • Engine *pEng - 指向桌面描述符的指针。  
  • char *Name - MATLAB 桌面字符类型的变量名。  
  • mxArray *mxVector - 指向矩阵变量的指针。  
mxArray *mxVector = engGetVariable(Engine *pEng, char *Name) - 此函数从桌面获取变量 - 是上一函数的逆函数。仅接受 mxArray 类型的变量,其中:
  • mxArray *mxVector - 指向矩阵变量的指针。  
  • Engine *pEng - 指向桌面描述符的指针。  
  • char *Name - MATLAB 桌面字符类型的变量名。  
double *p = mxGetPr(mxArray *mxVector) - 此函数获取指向值数组的指针,它用于复制数据和 memcpy()(请参见章节 2.3“C++ 标准输入/输出库”),其中:
  • double *p - 指向双精度类型数组的指针。  
  • mxArray *mxVector - 指向矩阵变量的指针。  
int = engEvalString(Engine *pEng, char *Command) - 此函数发送命令至 MATLAB 桌面,其中:
  • Engine *pEng - 指向桌面描述符的指针。  
  • char *Command - MATLAB 命令,字符类型字符串。  

您可能已注意到,MATLAB Engine API 仅允许您创建双精度类型的 mxArray 结构。但该限制不会影响您的可能性,而是将影响库的算法。

MCR (MCR instance) - 是 MATLAB 包的特殊库,可运行 MATLAB 环境在任何计算机上生成的独立应用程序/公用库。请注意,即使您有完成的 MATLAB 包,您仍需通过运行位于 <MATLAB>\Toolbox\compiler\deploy\win32 文件夹的 MCRInstaller.exe 文件安装 MCR 库。因此,在调用由 MATLAB 环境创建的任何公用库函数前,您需要调用 MCR 初始化函数:
 
bool = mclInitializeApplication(const char **option, int count) – 如果 MCR 成功启动,返回 TRUE,否则返回 FALSE,其中:

  • const char **option - 选项字符串,与 mcc - R 中的类似;通常为 NULL  
  • int count - 大小选项字符串,通常为 0。

在结束公用库工作时,您必须调用:
bool = mclTerminateApplication(void) - 如果 MCR 成功关闭,返回 TRUE。

2.2 MATLAB Compiler 4

MATLAB Compiler 允许您从 M 函数创建以下内容:  

  • 独立应用程序,即使未安装 MATLAB 亦可运行。
  • C/C++ 共享库,无需在终端用户系统上安装 MATLAB 即可使用。

Compiler 支持 MATLAB 的大部分命令和包,但不是全部。有关限制的完整列表可在 MATLAB 网站上找到。该方法允许您创建 MetaTrader 5 和 MATLAB 的“软件独立包”,但相比 MATLAB Engine,它要求您是训练有素的编程人员并对编译有深刻的认识。

MATLAB Compiler 至少需要下列之一的 C/C++ 编译器:

  • Lcc C(通常由 MATLAB 自带)。它只是 C 编译器。  
  • Borland C++,版本 5.3、5.4、5.5、5.6。
  • Microsoft Visual C/C++,版本 6.0、7.0、7.1。

相比前任,MATLAB Compiler 4 仅生成接口代码(包装程序),即不会将 m 函数转换为二进制或 C/C++ 代码,但它创建基于组件技术文件 (CTF) 技术的特殊文件,包含了支持 m 函数所需的各种包的集成。MATLAB Compiler 还使用唯一的(不重复的)1024 位密钥对该文件进行了加密。

现在我们来讨论 MATLAB Compiler 4 的工作算法,忽略此问题将在编译时导致许多愚蠢的错误:

  1. 依赖性分析 - 在此阶段确定编译的 m 函数依赖的所有函数、MEX 文件和 P 文件。  
  2. 建立存档 - 创建并加密及压缩 CTF 文件。  
  3. 生成包装程序的对象代码 - 在此阶段创建组件所需的所有源代码:
    • 在命令行中指定的 m 函数的 C/C++ 接口代码 (NameFile_main.c)。
    • 组件文件 (NameFile_component.dat),包含执行 m 代码所需的所有信息(包含加密密钥和存储于 CTF 文件的路径)。  
  4. C/C++ 转换。在此阶段,C/C++ 源代码被编译为目标文件。
  5. 链接。项目构建的最终阶段。

当您熟悉了 MATLAB Compiler 算法行为,接下来您需要更多地了解键,以便在使用编译器 (mcc) 时有详细的操作计划:


用途
    a filename
 将 <filename> 文件添加至档案,确定要添加至 CTF 档案的文件
     l
 生成函数库的宏
    N
 清除所有路径,必要的最小目录集除外
    p <directory>
 根据过程添加转换路径。需要 -N 键。
    R -nojvm
 取消 MCR 选项(MATLAB 组件运行时,请参见 MATLAB 帮助)
    W
 管理函数包装程序的创建
    lib
 创建初始化和完成函数
    main
 创建 main() 函数的 POSIX 壳
    T
 指定输出阶段
    codegen
 为独立应用程序创建包装程序代码
    compile:exe
 与 codegen 等同
    compile:lib
 为公共 DLL 创建包装程序代码
    link:exe
 与 compile:exe 加上链接等同
    link:lib
 与 compile:exe 加上链接等同

表 3. Matlab mcc 编译器(版本 4)的键

表 3 包含可能会在解决典型问题时用到的基本键。要获得更多帮助,请使用 MATLAB 命令 help mccdoc mcc

我们必须熟悉 MATLAB 链接器,以下是主要的键 (mbuild):

 键
用途
 -setup
 在交互模式下,未来调用 mbuild 时默认使用的编译器选项文件的定义
 -g
 创建含调试信息的程序。将 DEBUGFLAGS 添加到文件末尾。
 -O
 优化对象代码

表 4. Matlab mbuild 链接器(版本 4)的键

表 4 列出了主要的键。要获得更多信息,请使用 help mbuilddoc mbuild 命令。

2.3 C++ 标准输入/输出库

使用标准输入/输出库提供正确的数据复制。它的使用将使您从程序设计阶段生成的“愚蠢”错误中解放出来(例如:很多新手编程人员仅复制指针到内存块而不是复制整个内存块)。就整个输入/输出库而言,我们只对其中的一个函数感兴趣:

void *pIn = memcpy(void *pIn, void *pOut, int nSizeByte) – 此函数从 pOut 复制(克隆)变量/数组至 pIn,复制大小为 nSizeByte 字节,其中:

  • void *pIn - 指向复制目的地数组的指针。  
  • void *pOut - 指向复制目标的数组的指针。  
  • int nSizeByte - 待复制数据的大小,该大小不能超出 pIn 数组的大小,否则将发生内存访问错误。  

3. 实践

至此我们已完成了理论部分,接下来我们继续实现 MetaTrader 5 和 MATLAB 的交互。

您可能已经猜到了,我们有两种方法来实现交互:使用 MATLAB Engine 虚拟机和使用 MATLAB Compiler 生成的库。首先,我们来讨论一种简单、快速和通用的交互方法 - 通过 MATLAB Engine。

这部分文章必须从头到尾阅读,虽然两种交互方法存在表面上的差异,但它们的语言结构具有相同的原理和相似的语法,并且从简单的示例中我们更容易学到新的知识。

3.1 开发 MetaTrader 5 和 MATLAB Engine 交互的通用库

此交互方法算不上简洁和快速,但它是最可靠的,并且涵盖了整个 MATLAB 包。当然,我们应该指出最终模型开发的速度。开发的实质是为 MetaTrader 4/5 和 MATLAB Engine 交互编写通用库包装程序。之后,MetaTrader 4/5 脚本/指标/EA 交易可管理 MATLAB 虚拟桌面。而整个数学算法可作为字符串存储在 MQL 程序中,因此您可以用它来保护您的知识产权(更多详情,请参见《开发人员如何保护自己》一文)。它也可以存储在 <MetaTrader 5>\MQL5\Libraries 文件夹的 m 函数或 P 函数单独文件中。  

这种交互可能的应用领域:

  • 测试或展示“数学模型/理念”而无需编写复杂程序(知识产权的保护可在 MQL 程序中通过 MATLAB 包进行 - 使用 P 函数)。  
  • 使用 MATLAB 的所有功能编写复杂数学模型。
  • 适用于所有不打算发布他们的脚本/指标/EA 交易的人员。

我们继续。我希望您已阅读章节“1.1 MATLAB 和 MQL5 中的数据类型”“1.2 MQL5 和 MATLAB 数据类型的比较”“2.1 MATLAB Engine API 和 MCR 函数”以及“2.3 C++ 标准输入/输出库”,因为我们不会再停下来对前面讲述的内容进行分析。请仔细查看下面的功能块示意图,该示意图说明了未来库的算法:  

图 1. 库算法的功能块示意图

图 1. 库算法的功能块示意图

如图 1 所示,库主要由三大功能块组成。考虑一下它们各自的用途:

  • MQL5 功能块,发送/接收数据的初步准备:  
    • 反转数组。
    • 类型转换。
    • 字符串编码转换。
  • C/C++ 功能块:
    • 将数组转换为 mxArray 结构。
    • 传递 MATLAB Engine 命令。
  • MATLAB Engine 功能块 - 计算系统。  

我们现在来讨论算法。我们将从 MQL5 功能块开始。细心的读者已注意到,它的重点是实施章节“MATLAB 和 MQL5 中的数据类型”中编写的内容。如果您跳过了该部分内容,您将很难理解为什么所有这些是必要的。

mlInput <variable_type> 函数的算法几乎是相同的。我们使用 mlInputDouble() 函数来讨论它的工作情况,前者为 MATLAB 虚拟机提供 double 类型的变量的输入。

原型请见此处:

bool mlInputDouble(double &array[],int sizeArray, string NameArray),其中:

  • array - double 类型变量或数组的引用。
  • sizeArray - 数组大小(元素数量,而非字节数!)。
  • NameArray - 字符串,包含 MATLAB 虚拟机的唯一变量名(名称必须符合 MATLAB 的要求)。

算法:

  1. 使用 StringToCharArray() 函数将 NameArray 字符串转换为 char 数组。
  2. 使用 ArrayIsSeries() 函数检查索引类型。如果索引类型正确 - 将值传递到 mlxInputDouble() 函数。
    时序数组的其他索引:
    “反转”数组并将值传递到 mlxInputDouble() 函数。
  3. 结束函数,将返回的值传递到 mlxInputDouble() 函数。

mlGet <variable_type> 函数的算法几乎是相同的。我们使用 mlGetDouble() 函数来讨论它的工作情况,前者从 MATLAB 虚拟机返回 double 类型的变量。

原型请见此处:

int mlGetDouble(double &array[],int sizeArray, string NameArray),其中:

  • array - double 类型变量或数组的引用。
  • sizeArray - 数组大小(元素数量,而非字节数!)。
  • NameArray - 包含 MATLAB 虚拟机的唯一变量名的字符串。

算法:

  1. 使用 StringToCharArray() 函数将 NameArray 字符串转换为 char 数组。
  2. 使用 mlxGetSizeOfName() 函数获得数组的大小。
    • 如果大小大于零,使用 ArrayResize() 函数分配接收数组所需的大小,获取 mlxGetDouble() 的数据,返回数组大小。
    • 如果大小为,返回错误,即 NULL 值。  

就是这样!mlGetInt()mlGetLogical() 函数生成类型 double ->; int/bool 的“影子”转换。为此,这些函数在其主体内创建一个临时内存缓冲区。这是一种强制措施,因为遗憾的是,MATLAB API 不允许为 double 以外的数据类型创建 mxArray 结构。然而,这并不意味着,MATLAB 仅仅只操作 double 类型。

C/C++ 功能块要简单得多 - 它应提供从 double 类型到 mxArray 结构的数据转换。这通过 mxCreateDoubleMatrix()mxGetPr()memcpy() 函数完成。然后,它使用 engPutVariable() 函数将数据传递至 MATLAB 虚拟机,而提取数据则是使用 engGetVariable() 函数。再次注意具有前缀 IntLogical 的函数 - 如功能块示意图中所示,它们没有直接与 MATLAB 交互,而是通过 mlxInputDouble/mlxGetDoublemlxInputChar() 函数进行。它们行为的算法十分简单:调用 mlxInputDouble/mlxGetDouble 函数 - 将值作为 double(!) 输入/输出并发送“影子”MATLAB 命令以通过 mlxInputChar() 函数转换数据类型。

MATLAB Engine 功能块就更加简单了。它仅提供数学函数。它的行为取决于您的命令和 m/p 函数。  

至此,项目的所有“细节”均一览无余,是时候处理项目构建了。

而这种构建从创建主库开始 - 在我们的示例中就是 C/C++ 功能块。为此,在任意 ANSI 文本编辑器(Notepad、Bred 等)中,创建扩展名为 DEF 的文件。此文件的名称最好由拉丁字符组成并不含空格和标点,否则您将“听到”编译器的许多“谀辞”...该文件提供您函数的性能。如果此文件缺失,C/C++ 编译器将创建自己的“外来名称”以导出函数。

该文件包含:LIBRARY - 控制字,LibMlEngine - 库名称,EXPORTS - 第二控制字,然后是函数的名称。您可能已经知道,导出函数的名称不能包含空格和标点。下面是来自 MATLABEngine.zip 档案的 DllUnit.def 文件的文本:  

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

至此,我们有了项目的第一个文件。现在,我们打开 Windows Explorer 并转到 "<MATLAB>\Extern\include" 文件夹。将 engine.h 文件(MATLAB 虚拟机的头文件)复制到您建立项目的文件夹(如果不这样做,在编译阶段您需要手动指定文件路径)。

现在是创建 C/C++ 功能块的时候了。我们不会在文章中给出程序的完整源代码,读者可在 MATLABEngine.zip 中找到该文件(DllUnit.cpp,已详细注释)。请注意,最好使用 __stdcall 转换创建函数 - 即,参数通过堆栈传递,函数清除堆栈。此标准对于 Win32/64 API 而言是“原生”的。

考虑如何声明函数:

extern "C" __declspec(dllexport) <variable_type> __stdcall Function(<type> <name>)

  1. extern "C" __declspec(dllexport) - 告诉 C++ 编译器函数为外部函数。  
  2. <variable_type> - 返回变量的类型,可能是:void、bool、int、double、复合类型(不仅为 Dll 所熟知,也为调用程序所熟知)和指针。
  3.  __stdcall - 声明传递参数至函数和返回,它是 Win32/64 API 的一个标准。  
  4. Funcion - 您的函数名称。  
  5. <type> <name> - 输入变量的类型和名称,变量的最大数量为 64。

该主题在《如何交换数据:10 分钟为 MQL5 创建 DLL》一文中已详细论述。

C/C++ 功能块构建:为此您需要包含标准输入/输出库并将下述文件添加至项目(在您的编译器中:Project->Add Project):

  1. DllUnit.def
  2. 在 <MATLAB>\Extern\lib\<win32/64>\<compiler>\ 文件夹中,其中:
    <MATLAB> - MATLAB 主文件夹。
    <win32/64> - 用于 32 位操作系统的 win32 文件夹,或用于 64 位操作系统的 win64 文件夹。
    <compiler> - 用于 Borland C/C++ 版本5-6 的 "borland" 文件夹,用于 Microsoft Visual C++ 的 "microsoft" 文件夹:  
    • libeng.lib
    • libmx.lib

一个如下所述的常见问题可能会出现:“我有不同版本的编译器,或列表中没有这种编译器!(没有这些文件的情形十分少见)”。我们来看一下如何手动创建公用库。我们将考虑在 Visual C++ 和 Borland C++ 中它是如何实现的:

  1. 在 FAR 中打开 <MATLAB>\Bin\<win32/64> 文件夹,其中:
    <MATLAB> - MATLAB 主文件夹。
    <win32/64> - 用于 32 位操作系统的 win32 文件夹,或用于 64 位操作系统的 win64 文件夹。  
  2. 对于 Borland C++,输入:implib libeng.lib libeng.dll。对于 libmx.dll 也是一样。
  3. 对于 Visual C++,输入:lib libeng.dll。对于 libmx.dll 也是一样。
  4. 其他编译器:任何编程语言的任何编译器必须具有该实用程序 - 库管理器,通常这是一个控制台程序 <compiler _folder>\bin\*lib*.exe。

顺便一提,我忘了提醒读者 - 切勿尝试将 64 位 LIB 用于 32 位编译器。 首先,在编译器帮助中查看是否支持 64 位寻址。如果不支持,查找 32 位 MATLAB DLL 或选择其他 C/C++ 编译器。编译后,我们得到一个库,应位于 terminal_folder\MQL5\Libraries 文件夹中。

我们现在讨论 MQL 功能块。运行 MetaEditor,单击 "New"(新建),按图 2 和图 3 中所示步骤操作:  

图 2. MQL5 向导:创建库

图 2. MQL5 向导:创建库

图 3. MQL5 向导:库的一般属性

图 3. MQL5 向导:库的一般属性

现在,Wizard 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 类型的值并接收数组字符串作为参数。现在编译...

至此我们已完成并编译了库的最后一个功能块,是时候在真实条件下对其进行测试了。

为此,编写一个简单的测试脚本(或从 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 has been loaded");
     
}
   else
     {
      printf("Matlab ERROR! Load error.");
      mlClose();
      return;
     
}
/*
   ** 2. 传输命令行
   */
   if(mlInputChar(strComm)==true)
     {
      printf("Command line has been passed into MATLAB");
     
}
   else printf("ERROR! Passing the command line error");
/*
   ** 3. 传输 double 类型的变量
   */
   if(mlInputDouble(dTestIn,ArraySize(dTestIn),strA)==true)
     {
      printf("Variable of the double type has been passed into MATLAB");
     
}
   else printf("ERROR! When passing string of the double type");
/*
   ** 4. 获取 double 类型的变量
   */
   if((nSize=mlGetDouble(dTestOut,strA))>0)
     {
      int ind=0;
      printf("Variable A of the double type has been got into MATLAB, with size = %i",nSize);
      for(ind=0; ind<nSize; ind++)
        {
         printf("A = %g",dTestOut[ind]);
        
}
     
}
   else printf("ERROR! Variable of the double type double hasn't ben got");
/*
   ** 5. 传输 int 类型的变量
   */
   if(mlInputInt(nTestIn,ArraySize(nTestIn),strB)==true)
     {
      printf("Variable of the int type has been passed into MATLAB");
     
}
   else printf("ERROR! When passing string of the int type");
/*
   ** 6. 获取 int 类型的变量
   */
   if((nSize=mlGetInt(nTestOut,strB))>0)
     {
      int ind=0;
      printf("Variable B of the int type has been got into MATLAB, with size = %i",nSize);
      for(ind=0; ind<nSize; ind++)
        {
         printf("B = %i",nTestOut[ind]);
        
}
     
}
   else printf("ERROR! Variable of the int type double hasn't ben got");
/*
   ** 7. 传输 bool 类型的变量
   */
   if(mlInputLogical(bTestIn,ArraySize(bTestIn),strC)==true)
     {
      printf("Variable of the bool type has been passed into MATLAB");
     
}
   else printf("ERROR! When passing string of the bool type");
/*
   ** 8. 获取 bool 类型的变量 E
   */ 
   if((nSize=mlGetLogical(bTestOut,strC))>0)
     {
      int ind=0;
      printf("Variable C of the bool type has been got into MATLAB, with size = %i",nSize);
      for(ind=0; ind<nSize; ind++)
        {
         printf("C = %i",bTestOut[ind]);
        
}
     
}
   else printf("ERROR! Variable of the bool type double hasn't ben got");
/*
   ** 9. 结束
   */
   mlClose();
  
}

如脚本所示,我们输入值然后获取值。然而,相比在 MetaTrader 4 中我们需要在设计阶段知道缓冲区的大小,MetaTrader 5 则无此必要,因为我们使用动态缓冲区

现在,您已完全理解了 MATLAB 虚拟机,可以开始使用 MATLAB 环境中内置的 DLL。

3.2 构建/使用 MATLAB Compiler 4 生成 DLL 的技术指南

在上一节中,我们讨论了如何创建与 MATLAB 包通用交互的库。然而,这种方法有一个缺点 - 它需要来自终端用户的 MATLAB 包。这一限制在软件成品的分发中造成了很多困难。这是 MATLAB 数学包具有内置编辑器的原因之所在,它允许您创建独立于 MATLAB 包的“独立应用程序”。我们来看看该编译器。

例如,考虑一个简单的指标 - 移动平均线 (SMA)。通过添加神经网络滤波器 (GRNN) 使其稍稍升级,该滤波器可用于平滑“白噪点”(随机猝发)。将新的指标命名为 NeoSMA,将滤波器命名为 GRNNFilter。  

因此我们具有两个 m 函数,我们希望创建这两个函数的可从 MetaTrader 5 调用的 DLL。

请记住,MetaTrader 5 在下面的文件夹中搜索 DLL。

  • <terminal_dir>\MQL5\Libraries  
  • <terminal_dir>  
  • 当前文件夹
  • 系统文件夹 <windows_dir>\SYSTEM32  
  • <windows_dir>  
  • 在系统环境变量中列示的目录。

因此,将两个 m 函数(NeoSMA.m 和 GRNNFilter.m)放入这些目录的其中之一,我们将在此建立 DLL。我要提请您注意放置的事实,因为这不是偶然的。细心的读者已经知道 MATLAB 编译器的特性 - 在编译时保存路径(请参见“2.2 MATLAB Compiler 4”)。

  在您编译项目前,您必须配置编译器。为此,请遵循下述步骤:

  1. 在 MATLAB 命令行中输入:mbuild -setup
  2. 按 "y" 以确认在您的系统中找到安装的 C/C++ 兼容编译器。
  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 and GRNNFilter.m  - m 函数名称

现在我们来看看编译器创建的内容:

  • mccExcludedFiles.log - 包含编译器操作的日志文件
  • NeoSMA.c - C 版本的库(包含 C 代码的包装程序)
  • NeoSMA.ctf - CTF 文件(请参见章节“2.2 MATLAB Compiler 4”) 
  • NeoSMA.h - 头文件(包含库、函数、常量的声明)
  • NeoSMA.obj - 目标文件(源文件包含机器代码和伪代码)
  • NeoSMA.exports - 导出的函数名称
  • NeoSMA.dll - 用于进一步链接的 Dll
  • NeoSMA.lib - 在 C/C++ 项目中使用的 Dll 
  • NeoSMA_mcc_component_data.c - C 版组件(用于和 CTF 文件兼容,包含路径等)
  • NeoSMA_mcc_component_data.obj - 目标版组件(包含机器代码和伪代码的源文件);

那么,我们严格按照其内部结构来处理 DLL。它包含(仅基本函数):

  1. 任意 DLL 的函数 - BOOL WINAPI DllMain(),处理(根据 Microsoft 规范)DLL 中出现的事件:DLL 加载到进程的地址空间,创建新的流,删除流并从内存卸载 Dll。  
  2. DLL 初始化/取消初始化服务函数:BOOL <NameLib>Initialize(void)/void <NameLib>Terminate(void) - 需要在使用库函数前以及在使用结束时用于启动/卸载数学工作环境。
  3. Exported m-functions – 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> - 指向 m 函数输入变量的 mxArray 结构的指针。
     

如您所见,导出的 m 函数包含地址和指向 mxArray 结构的指针,且您无法从 MetaTrader 5 直接调用这些函数,因为 MetaTrader 5 不认识此类型的数据。我们没有对 MetaTrader 5 中的 mxArray 结构进行说明,因为 MATLAB 开发人员不保证它不会随时间改变,甚至在产品的同一版本内也是如此,因此您需要编写一个简单的 DLL 适配器。

它的功能块示意图如下所示:

图 5. DLL 适配器功能块示意图

图 5. DLL 适配器功能块示意图

DLL 右侧的内容与 MATLAB Engine 十分相似,因此我们不再分析其算法,而是直接考虑代码。为此,在您的 C/C++ 编译器中创建两个小文件:  

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


在您的 C/C++ 编译器中构建项目:为此您需要包含标准输入/输出库并将下述文件添加至项目(在您的编译器中:Project->Add Project):

  1. nSMA.def
  2. 在 <MATLAB>\Extern\lib\<win32/64>\<compiler>\ 文件夹中,其中:
    <MATLAB> - MATLAB 主文件夹。
    <win32/64> - 用于 32 位操作系统的 win32 文件夹,或用于 64 位操作系统的 win64 文件夹。
    <compiler> - 用于 Borland C/C++ 版本5-6 的 "borland" 文件夹,用于 Microsoft Visual C++(我有版本 6 的文件)的 "microsoft" 文件夹:  
    • libmx.lib
    • mclmcr.lib
  3. NeoSMA.lib - 手动创建(请参见章节“3.1 开发 MetaTrader 5 和 MATLAB Engine 交互的通用库”)。  

在本节的最后,我想和大家讨论在移动项目至未安装 MATLAB 的其他计算机时所需的文件。

下面是文件列表以及目标计算机上的路径:

  • MCRInstaller.exe                    任意文件夹(MCR 安装程序)
  • extractCTF.exe                      任意文件夹(用于 MCR 安装程序)
  • MCRRegCOMComponent.exe  任意文件夹(用于 MCR 安装程序)
  • unzip.exe                              任意文件夹(用于 MCR 安装程序)
  • NeoSMA.dll                           <terminal_dir>\MQL5\Libraries
  • NeoSMA.ctf                           <terminal_dir>\MQL5\Libraries
  • nnSMA.dll                             <terminal_dir>\MQL5\Libraries

许多高级编程人员已经猜到,明智的做法是使用安装程序 (SETUP)。网上有很多这样的程序,包括免费版本。

现在我们需要在 MetaTrader 5 中测试该 DLL。为此我们将编写一个简单的脚本(来自 DllMatlab.zip 的 TestDllMatlab.mq5):

#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);        // 时间坐标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 交互的通用库,以及如何连接 MATLAB 环境内置的 DLL。还需要说明的是 MetaTrader 5 和 MATLAB 交互的接口,只是这已超出了本文的范围。我们已详细讨论了本文的主题。我选择的是最有效的交互方式,无需特殊类型的“适配器”。当然,您也可以选择其他的方式,例如 .NET 技术 - 《如何使用 WCF 服务从 MetaTrader 5 导出报价至 .NET 应用程序》

很多读者可能会问:到底选择哪种方法?答案很简单 - 两种,因为在数学模型的设计/调试阶段,对速度没有什么要求。但您编程时会需要 MATLAB 的全部能力而无需“特殊生产成本”。当然,MATLAB Engine 在此可提供大量帮助。然而,当数学模型经过调试并使用就绪时,系统将对速度和多任务处理(指标和/或交易系统在多个价格图表上工作)提出要求 - 毫无疑问,此时您需要内置于 MATLAB 环境的 DLL。

但这一切并不是强迫您按部就班。对于这个问题,每个人都会给出自己的答案,这主要取决于“编程成本”相对于项目规模(指标和/或交易系统用户的数量)的比例。为一两个用户在 MATLAB 环境中创建 Dll 是没有意义的(在两台计算机上安装 MATLAB 要更简单)。  

许多熟悉 MATLAB 的读者可能会问:为什么需要这些?MQL5 已经有了数学函数!答案是,使用 MATLAB 可轻松地实施您的数学理念,下面列出的是众多可能性中的一小部分:  

  • 指标和/或手工操作交易系统中的模糊逻辑的动态算法
  • 手工操作交易系统中的动态遗传算法(动态策略测试程序)
  • 指标和/或手工操作的交易系统中的动态神经网络算法
  • 三维指标
  • 非线性管理系统仿真

因此,一切尽在您的掌握之中,并且不要忘了:“数学始终是科学的皇后”,而 MATLAB 包是您的科学计算器。

参考文献

  1. MATLAB 内置帮助。
  2. MQL5 内置帮助。
  3. Jeffrey Richter《Microsoft Windows 编程应用》

本文译自 MetaQuotes Software Corp. 撰写的俄文原文
原文地址: https://www.mql5.com/ru/articles/44

附加的文件 |
dllmatlab_en.zip (955.7 KB)
matlabengine_en.zip (670.57 KB)
创建具有图形控制选项的指标 创建具有图形控制选项的指标

熟悉市场情绪的人都知道 MACD 指标(其全称为平滑异同移动平均线)- 自计算机分析方法面世以来即已被交易人员用于分析价格变动的强大工具。在本文中,我们将讨论 MACD 可能的变型,并在可图形切换变型的指标中实施这些变型。

MQL5 中的事件处理:快速更改 MA 周期 MQL5 中的事件处理:快速更改 MA 周期

假设有周期为 13 的简单 MA(移动平均线)指标应用至图表。而我们希望将周期更改为 20,但我们不想转到指标属性对话框去把将数字 13 更改为 20:我们就是对这些使用鼠标和键盘执行的乏味操作感到厌倦。尤其是,我们不希望打开指标代码进行修改。我们只想按一下按钮 - 与数字小键盘相邻的“向上箭头”- 就完成这一切。在本文中,我将给出实现方法。

面向初学者的创建具有多个指标缓冲区的指标 面向初学者的创建具有多个指标缓冲区的指标

复杂代码由一组简单代码组成。如果您熟悉简单代码,复杂代码看上去就不那么复杂了。在本文中,我们将讨论如何创建具有多个指标缓冲区的指标。我们将 Aroon 指标作为示例进行详细分析,并给出两个不同的代码版本。

“傻瓜式”MQL:如何设计和构建对象类 “傻瓜式”MQL:如何设计和构建对象类

我们将通过创建视觉设计的样本程序,介绍如何在 MQL5 中设计和构建类。本文为使用 MT5 应用程序的初学者编程人员所编写。我们提出一种简易明了的抓取技术用于创建类,无需深刻理解面向对象编程的理论。