请 [注册] 或 [登录]  | 返回主站

量化交易吧 /  量化平台 帖子:3364677 新帖:0

MetaTrader 5 和 MATLAB 交互

我是游客发表于:2 月 25 日 00:00回复(1)

简介

我的第一篇文章《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 编程应用》

全部回复

0/140

达人推荐

量化课程

    移动端课程