1. 问题定义 例如,下文引用自 MQL4 社区论坛的一个主题(译自俄语): ...有两个指标(我们称之为 A 和 B)。像往常一样,指标 A 使用直接来自价格图表的数据,B 使用指标 A 的数据。问题如下:如何使用指标 A 的数据(已经附加)而不是 iCustom(“指标 A”,...)动态执行 B 的计算,即,如果我更改了指标 A 的某些设置,更改应该在指标 B 的参数更改上体现出来。
或换言之: ...假设我们已将“移动平均线”附加至图表。现在要如何才能直接访问其数据缓冲区? 又如:
... 如果我使用 iCustom 调用某项指标,即使该指标在调用前已经加载也仍将重新加载。是否有什么方法防止指标重复加载?
诸如此类的问题还有很多,并且被反复问及 - 提问者不仅仅是初学者。概括而言,问题在于赫兹量化 没有办法访问自定义指标数据而不使用 iCustom (MQL4) 或 iCustom + CopyBuffer 绑定 (MQL5)。但是用 MQL 代码编写一些函数以使用来自指定图表的数据获取数据或进行计算,对于开发下一部大作而言是十分诱人的。 使用上文中提到的标准函数并不方便:例如,在 MQL5中,当调用 iCustom 时,将为每个调用方创建指标的副本;而对于 MQL5,通过使用句柄使问题得到了部分解决,现在仅为原本执行一次计算。但句柄也不是万灵药:如果涉及的指标占用大量的计算资源,则终端互斥等待几乎是通过每次重新初始化的十几秒钟保证。 我记得有几个提供的方法可访问原本,如下所示:
-
在物理磁盘或内存上映射文件;
-
经由 DLL 共享内存使用以用于数据交换;
-
使用客户端的全局变量以用于数据交换及其存储。
此外还有属于上述方法的变体的其他一些方法,以及一些不常用的方法,如套接字、邮件槽等(有一种基本方法在“EA 交易”中经常使用,即直接将指标计算转移至“EA 交易”代码,但这远远超出了本文论述的范围)。 在笔者看来,所有这些方法都有可取之处,但它们有一个共同的劣势:数据首先是复制到某个位置,然后才分发给其他方。首先,这需要占用部分 CPU 资源;其次,产生了传输数据相关性的新问题(在此我们不做论述)。
所以,让我们尝试定义问题: 我们希望创建这样一个环境,即能够提供对附加于图表的指标的数据访问,并具有以下属性:
-
没有数据复制(也没有数据相关性的问题);
-
只需稍加修改我们需要使用的可用方法的代码;
-
MQL 代码优先(当然,我们必须使用 DLL,但我们将只使用一些 C++ 代码字符串)。
笔者将 C++ Builder 用于 DLL 创建和赫兹量化 客户端。下面的源代码以 编写,MQL5 代码已附于本文;我们将在下文中讨论两种代码的主要区别。
2. 数组 首先,我们需要一些理论,因为我们将要使用指标缓冲区,并且我们必须知道缓冲区的数据在内存中是如何分布的。这一信息没有适当地文档化。 MQL 中的动态数组是具有可变大小的结构,如果数组大小增加而在数组后没有空闲内存,则 MQL 如何解决数据重新分配的问题将变得十分有趣。我们有两种方法解决问题:
-
在可用内存的其他部分重新分配新数据(并储存该数组所有部分的地址,例如使用引用列表),或
-
将整个数组作为整体移动至内存新的部分,该部分有足够的空间用于分配数组。
第一种方法带来了其他一些问题,因为在这种情况下我们必须调查 MQL 编译程序创建的数据结构。以下考虑仍然证明了第二个变体(仍然较慢):当动态数组传递至外部函数时(传递至 DLL),后者获得数据第一个元素的指针,其他数组元素按照顺序排列在第一个数组元素后。 当通过引用传递时,数组数据可以更改,因此这意味着对于第一种方法,复制整个数组至单独的内存区域以传递给外部函数,然后将结果添加至源数组存在一个问题,即,执行第二种方法隐含的相同的操作。
虽然这一结论在逻辑上不是 100% 精确,我们仍可将它视为是相当可靠的(这也被我们基于此理念的产品的正确运行所证明)。
因此,我们可以假设下面的说法是正确的:
-
在每一时刻,动态数组的数据在内存中一个接一个地连续排列;并且数组可重新分配至计算机内存的其他区域,但只能作为一个整体操作。
-
当动态数组作为参数通过引用传递至外部函数时,第一个数组元素的地址被传递至 DLL 库。
然而我们还需要其他一些假设:所谓时刻,我们指的是调用对应指标的 OnCalculate()(MQL4 则为 start () 函数)函数的时刻;此外,为了简便,我们假设我们的数据缓冲区具有相同的维度,且类型均为 double []。 3. 必要条件 MQL 不支持指针(除了所谓的对象指针,对象指针并不是普遍意义上的指针),因为这已被 MetaQuotes Software 公司的代表反复声明和确认。因此,我们来看看事实是否如此。
什么是指针?指针并不只是带星号的的标识符,它是计算机内存单元的地址。那么什么是单元地址?单元地址是起始于某个起点的序列号。最后,什么是序列号?序列号是一个整数,对应于计算机内存的某个单元。为什么我们不能像使用整数那样使用指针?不,我们可以,因为 MQL 程序可完美处理整数! 那么如何将指针转换为整数?动态链接库可帮助我们达成目的,我们将利用 C++ 类型转换的机会。由于 C++ 指针为四字节数据类型,对我们而言,可以很方便地将 int 用作此类四字节数据类型。 符号位并不重要,我们对其不作考虑(如果它等于 1,意味着整数为负),重要的是我们可以保持所有指针位不变。当然,我们可以使用无符号整数,但 MQL5 和 MQL4 的代码相似则更为理想,因为 MQL4 未提供无符号整数。
因此, extern "C" __declspec(dllexport) int __stdcall GetPtr(double *a) { return((int)a); } 这样,我们就有了值为数组起始地址的长型变量!现在,我们需要了解如何读取 i-th 数组元素的值:
extern "C" __declspec(dllexport) double __stdcall GetValue(int pointer,int i) { return(((double*) pointer)[i]); }
... 并写入值(在我们的示例中它仍然不是必须的...)
extern "C" __declspec(dllexport) void __stdcall SetValue(int pointer,int i,double value) { ((double*) pointer)[i]=value; } 以上便是全部内容。现在我们可以在 MQL 中使用指针。
4. 包装
我们已创建系统的内核,现在必须为其在 MQL 程序中使用方便做准备。然而,美学粉也不要感到不安,我们会向大家展现一些不同之处。
有很多种方法可用,我们将选择以下方式。让我们回想一下,客户端具有一个特别功能用于独立 MQL 程序间的数据交换 - 全局变量。最自然的方式是用它们存储指向我们将要访问的指标缓冲区的指针。我们会将这些变量作为描述符表考虑。每个描述符将具有如下所示的以字符串表示的名称:
string_identifier#buffer_number#symbol#period#buffer_length#indexing_direction#random_number, 并且其值将等于计算机内存中相应缓冲区指针的整数显示。 有关描述符字段的一些细节。
-
string_identifier – 任意字符串(例如,指标的名称 - 可使用 short_name 变量等);它将被用于搜索必要的指针,我们指的是一些指标将使用相同的标识符注册描述符,并在相互间使用字段进行区分:
-
buffer_number – 将用于区分缓冲区;
-
buffer_length – 我们需要用它来控制界限,否则可能导致客户端和 Windows 崩溃Blue Screen of Death :);
-
symbol, period – 交易品种和周期用于指定图表窗口;
-
ordering_direction – 它指定了数组元素的排序方向:0 – 正常排序,1 – 逆向排序(AS_SERIES 标志为 true);
-
random_number – 如果指标有多个副本通过不同的窗口或不同的参数集附加至客户端时使用(它们的第一个和第二个字段可设置相同的值,这也是我们需要通过某些方式区分它们的原因)- 也许这不是最佳的解决方案,但它确实管用。
首先,我们需要用到一些函数来注册和删除描述符。看一看函数的第一个字符串 - 调用 UnregisterBuffer() 函数对于从全局变量列表中删除旧的描述符来说是必要的。 对于每个新柱,缓冲区大小将增加 1,因此我们必须调用 RegisterBuffer()。如果缓冲区大小改变,新的描述符将在表中创建(其大小信息包含在其名称内),旧的描述符将留在表中。这就是我们使用它的原因。 void RegisterBuffer(double &Buffer[], string name, int mode) export