简介
我的第一篇文章《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 包的方法是开发人员所推荐的。接口由六个函数组成:
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 复数的类型。
- mxArray *mxVector - 指向矩阵变量的指针。
- Engine *pEng - 指向桌面描述符的指针。
- char *Name - MATLAB 桌面字符类型的变量名。
- mxArray *mxVector - 指向矩阵变量的指针。
- mxArray *mxVector - 指向矩阵变量的指针。
- Engine *pEng - 指向桌面描述符的指针。
- char *Name - MATLAB 桌面字符类型的变量名。
- double *p - 指向双精度类型数组的指针。
- mxArray *mxVector - 指向矩阵变量的指针。
- 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 的工作算法,忽略此问题将在编译时导致许多愚蠢的错误:
- 依赖性分析 - 在此阶段确定编译的 m 函数依赖的所有函数、MEX 文件和 P 文件。
- 建立存档 - 创建并加密及压缩 CTF 文件。
- 生成包装程序的对象代码 - 在此阶段创建组件所需的所有源代码:
- 在命令行中指定的 m 函数的 C/C++ 接口代码 (NameFile_main.c)。
- 组件文件 (NameFile_component.dat),包含执行 m 代码所需的所有信息(包含加密密钥和存储于 CTF 文件的路径)。
- C/C++ 转换。在此阶段,C/C++ 源代码被编译为目标文件。
- 链接。项目构建的最终阶段。
当您熟悉了 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 mcc 或 doc mcc。
我们必须熟悉 MATLAB 链接器,以下是主要的键 (mbuild):
键 |
用途 |
---|---|
-setup |
在交互模式下,未来调用 mbuild 时默认使用的编译器选项文件的定义 |
-g |
创建含调试信息的程序。将 DEBUGFLAGS 添加到文件末尾。 |
-O |
优化对象代码 |
表 4. Matlab mbuild 链接器(版本 4)的键
表 4 列出了主要的键。要获得更多信息,请使用 help mbuild 或 doc 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 所示,库主要由三大功能块组成。考虑一下它们各自的用途:
- 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 的要求)。
算法:
- 使用 StringToCharArray() 函数将 NameArray 字符串转换为 char 数组。
-
使用 ArrayIsSeries() 函数检查索引类型。如果索引类型正确 - 将值传递到 mlxInputDouble() 函数。
时序数组的其他索引:“反转”数组并将值传递到 mlxInputDouble() 函数。
- 结束函数,将返回的值传递到 mlxInputDouble() 函数。
mlGet <variable_type> 函数的算法几乎是相同的。我们使用 mlGetDouble() 函数来讨论它的工作情况,前者从 MATLAB 虚拟机返回 double 类型的变量。
原型请见此处:
int mlGetDouble(double &array[],int sizeArray, string NameArray),其中:
-
array - double 类型变量或数组的引用。
-
sizeArray - 数组大小(元素数量,而非字节数!)。
-
NameArray - 包含 MATLAB 虚拟机的唯一变量名的字符串。
算法:
- 使用 StringToCharArray() 函数将 NameArray 字符串转换为 char 数组。
- 使用 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() 函数。再次注意具有前缀 Int 和 Logical 的函数 - 如功能块示意图中所示,它们没有直接与 MATLAB 交互,而是通过 mlxInputDouble/mlxGetDouble 和 mlxInputChar() 函数进行。它们行为的算法十分简单:调用 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>)
- extern "C" __declspec(dllexport) - 告诉 C++ 编译器函数为外部函数。
- <variable_type> - 返回变量的类型,可能是:void、bool、int、double、复合类型(不仅为 Dll 所熟知,也为调用程序所熟知)和指针。
- __stdcall - 声明传递参数至函数和返回,它是 Win32/64 API 的一个标准。
- Funcion - 您的函数名称。
-
<type> <name> - 输入变量的类型和名称,变量的最大数量为 64。
该主题在《如何交换数据:10 分钟为 MQL5 创建 DLL》一文中已详细论述。
C/C++ 功能块构建:为此您需要包含标准输入/输出库并将下述文件添加至项目(在您的编译器中:Project->Add Project):
- DllUnit.def
- 在 <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++ 中它是如何实现的:
- 在 FAR 中打开 <MATLAB>\Bin\<win32/64> 文件夹,其中:
<MATLAB> - MATLAB 主文件夹。
<win32/64> - 用于 32 位操作系统的 win32 文件夹,或用于 64 位操作系统的 win64 文件夹。 - 对于 Borland C++,输入:implib libeng.lib libeng.dll。对于 libmx.dll 也是一样。
- 对于 Visual C++,输入:lib libeng.dll。对于 libmx.dll 也是一样。
-
其他编译器:任何编程语言的任何编译器必须具有该实用程序 - 库管理器,通常这是一个控制台程序 <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 向导:创建库
图 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”)。
在您编译项目前,您必须配置编译器。为此,请遵循下述步骤:
- 在 MATLAB 命令行中输入:mbuild -setup
- 按 "y" 以确认在您的系统中找到安装的 C/C++ 兼容编译器。
- 选择标准 Lcc-win32 C 编译器。
- 按 "y" 以确认选择的编译器。
图 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。它包含(仅基本函数):
- 任意 DLL 的主函数 - BOOL WINAPI DllMain(),处理(根据 Microsoft 规范)DLL 中出现的事件:DLL 加载到进程的地址空间,创建新的流,删除流并从内存卸载 Dll。
- DLL 初始化/取消初始化服务函数:BOOL <NameLib>Initialize(void)/void <NameLib>Terminate(void) - 需要在使用库函数前以及在使用结束时用于启动/卸载数学工作环境。
-
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 适配器功能块示意图
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):
- nSMA.def
- 在 <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
- 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 包是您的科学计算器。
参考文献
- MATLAB 内置帮助。
- MQL5 内置帮助。
- Jeffrey Richter《Microsoft Windows 编程应用》
。