English Русский Español Deutsch 日本語 Português
preview
从基础到中级:事件(二)

从基础到中级:事件(二)

MetaTrader 5示例 |
21 0
CODE X
CODE X

概述

在上一篇文章,“从基础到中级:事件(一)中,我们开始讨论事件驱动编程。是的,许多程序员都很熟悉这种方法,它的名字就是:事件驱动编程。许多人认为这既复杂又困难。这是因为他们不了解它的机制和概念。

然而,事件驱动编程使我们摆脱了与图形用户界面(GUI)相关的各种问题和任务。由于 MetaTrader 5 是一个 GUI 平台,使用事件驱动编程大大简化了事件驱动应用程序的开发。

在上一篇文章中,我们采取了第一步来了解事情将如何进行。我们看到了如何使用一个相当简单的指标来跟踪发生的事件。然而,对于那些想测试他们在实践中理解和应用某些编程概念的能力的人来说,有一个小任务需要解决。

由于拟议的任务不是很复杂,我相信那些花更多时间和精力的人本可以想出一种方法来解决它,因为它看起来确实相对简单。

接下来,我将展示解决同一任务的两种方法,并讨论每种方法的优缺点。所以,是时候专注于今天的学习内容了。而且,像往常一样,我们将开始一个新的主题,以便更好地组织一切。


可能的第一种解决方案

为了正确地开始解释,让我们从指标代码的样子开始,如前一篇文章所示:

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. uint gl_Counter;
05. //+------------------------------------------------------------------+
06. int OnInit()
07. {
08.    gl_Counter = 0;
09. 
10.    Print(__FUNCTION__);
11. 
12.    return INIT_SUCCEEDED;
13. };
14. //+------------------------------------------------------------------+
15. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[])
16. {
17.    if (!gl_Counter)
18.    {
19.       uint arr[];
20. 
21.       if (FileLoad(_Symbol, arr) > 0)
22.          gl_Counter = arr[0];
23.    }
24.    
25.    Print(__FUNCTION__, " :: ", ++gl_Counter);
26. 
27.    return rates_total;
28. };
29. //+------------------------------------------------------------------+
30. void OnDeinit(const int reason)
31. {
32.    uint arr[1];
33. 
34.    arr[0] = gl_Counter;
35.    FileSave(_Symbol, arr);
36. 
37.    Print(__FUNCTION__);
38. };
39. //+------------------------------------------------------------------+

代码 01

建议的任务是创建一种方法,使本代码第 04 行声明的变量仅在时间周期发生变化时才保留其当前的计数器值。在其他任何情况下,该变量的值都应该是零。也就是说,计数器会重置。

尽管这项任务相对简单,但它可以显示读者对更复杂的任务的准备程度。但是,如果你在解决问题时遇到困难,不要担心。重要的是,你找到了解决这个问题的方法。这才是真正重要的。解决这项任务对你来说是一个“额外的收获”,但这并不能证明你已经知道如何编程。你只会找到一个解决方案。如果是这样的话,请接受我最热烈的祝贺。

好吧,我们来思考一下。每次我们更改图表的时间周期时,MetaTrader 5 都会触发 Deinit 。这反过来又会生成一个针对特定图表的调用。我们将在未来继续探索这一机制。现在,您只需要了解 MetaTrader 5 对您有多大的帮助,以及我们可以和应该如何使用它。当此事件到达图表窗口时,它会被发送到该图表上显示的所有应用程序。注意,是全部。每个应用程序都必须捕获此 Deinit 事件并以最佳方式处理它。

好的,那么谁会收到这个事件?嗯,这个事件将完全由 OnDeinit 拦截,而有趣的事情也由此开始。如果我们查看代码 01 的第 30 行,可以看到该过程接收一个值。MetaTrader 5 会填充此值,以说明应用程序从图表中移除的原因。“但是等等,‘从图表中移除’是什么意思?乍一看,这似乎没什么道理,因为我们只是要求更改图表周期,而不是移除指标。”从某种意义上说,你们是对的,亲爱的读者。

然而,由于各种原因,主要是与 MetaTrader 5 平台本身的实现有关,更简单的方法是先从图表中移除所有元素,请求重新绘制图表,然后再将移除的元素放回图表上。当然,正因为如此,脚本不会重新定位,因为它们无法处理事件。但是,能够处理事件的指标和 EA 交易会自动重新附加到图表上。这与平台启动时的情况相同。如果打开的图表上有指标或 EA 交易,MetaTrader 5 会自动将其放置在图表上。在将所有这些元素放置在图表上之后,MetaTrader 5 会立即为该图表发出一个新事件。相关事件为 Init ,而 Init 事件又将被 OnInit 函数拦截。该函数可以在代码 01 的第 06 行看到。这就是 MetaTrader 5 将为我们做的全部工作了。从那里,我们决定我们的应用程序将如何对此类事件做出反应。

这非常重要,所以请确保你很好地理解这一点。这是MQL5中事件驱动编程的基础,用于创建在 MetaTrader 5 上运行的应用程序。由于我们的应用程序只能在运行时知道 gl_Counter 中存在的计数器值,因此我们无法知道在 Init 事件发生之前它的值是多少。这是因为在从平台中删除图表并随后重新创建图表的过程中,MetaTrader 5 会清除我们的应用程序在图表上使用的整个内存区域。唯一可以保留的(视情况而定)是静态变量。但是,在设计用作指标的应用程序中,静态变量会带来一些问题。它们可以使用,但需采取一定的预防措施。我们以后会讨论这个问题。现在,让我们暂时忘记指标代码中的静态变量。对于 EA 交易而言,情况有所不同,因为其中涉及不同的机制,需要理解这些机制才能正确使用。所以,我们先来关注指标的问题 —— 正是因为它们比较简单。

确实,在这个阶段,我们不能使用静态变量,gl_Counter 的值只有在指标显示在图表上时才有效。此外,当我们要求 MetaTrader 5 更改图表周期时,会触发一个事件,该事件将被第 30 行的过程捕获,其 reason 参数值指示 Deinit 事件的原因。现在,我们需要查阅文档,检查哪个值报告了我们需要保留 gl_Counter 的事件类型。这就是我们找到解决方案的方法。

好的,查阅文档后,我们发现常量 REASON_CHARTCHANGE 符合我们的标准,即时间周期的变化。但是,如果交易品种也发生变化,则会报告相同的常量。但我们首先来关注一下图表周期问题。因此,通过在 OnDeinit 过程中添加一个小测试,我们就得到了新的代码:

                   .
                   .
                   .
29. //+------------------------------------------------------------------+
30. void OnDeinit(const int reason)
31. {
32.    uint arr[1];
33. 
34.    arr[0] = (reason == REASON_CHARTCHANGE ? gl_Counter : 0);
35.    FileSave(_Symbol, arr);
36. 
37.    Print(__FUNCTION__);
38. };
39. //+------------------------------------------------------------------+

代码 02

由于其余代码与代码 01 中看到的内容完全相同,我们将只关注发生变化的部分。改变非常简单:我只需要修改第 34 行,检查该常量是否是我们想要使用的常量。如果是,则保留计数器值。否则,该值将重置为零,这将导致当再次对进行计数的同一交易品种使用指标时,计数器重新开始计数。

现在我们将探讨以这种方式实现该解决方案的优点和缺点。是的,这些并不是与实施相关的所有优点和缺点。因此,理解概念和练习是学习过程的一部分。

为简洁起见,我们将只介绍一个优点和一个缺点。剩下的取决于你,随着你练习和研究特定实现形式的每个具体用例,知识就会出现。

优点是该过程相对“干净”,易于在多个参数下控制。例如,您可能想知道在给定的时间周期内触发特定事件的次数。

另一方面,使用文件存储值迫使我们分析它们,这在许多情况下可能会成为问题。例如,如果同一个交易品种和同一个指标在同一个 MetaTrader 5 交易时段内打开,不断改变时间周期可能会导致数值不匹配。在编程中,这类问题被称为竞争条件,非常复杂;事实上,整个博士论文和书籍都是关于这个主题的。相信我,作为一个初学者,你真的不想在代码中遇到竞争条件。

好吧,亲爱的读者,这将是第一个解决方案,正是因为它不需要对代码进行重大更改:只需在前面的文章中已经显示的文档和借用元素中快速搜索即可。

现在,我们将看看一个稍微不同的解决方案,它将跟踪计数器值的一些工作委托给 MetaTrader 5。但是,如你所知,我们将在一个新的主题中这样做。


可能的第二种解决方案

第二个解决方案更有趣,无论从实际角度来看,还是因为维护计数器值的责任仍然掌握在 MetaTrader 5 手中。然而,在你想知道为什么我们没有早点考虑这个解决方案之前,我们会做相反的事情。首先,我们将讨论使用第二种解决方案的优缺点,然后我们将看看它是如何实现的。与前面的解决方案一样,我们将列出一个优点和一个缺点,以防止解释变得太长。

第二种解决方案的优点是,它减轻了我们将变量值存储在某个地方的一些责任,很可能是使用文件。另一方面,应该指出的是,根据我们试图实现的目标,委派维护变量值的责任会使任务稍微复杂化。这是因为这项责任在于 MetaTrader 5,而不是我们。

"但这怎么可能呢?同一件事怎么能既是优势又是劣势呢?"为了理解为什么我把它列为两者,我们需要研究这个解决方案由什么组成,它的优点和缺点实际上是一样的,但它给人的印象是不可思议的。这是 MetaTrader 5 中一个很少用到的功能。过去,我曾将其用于其他目的,MetaTrader 5 开发人员当然没有预料到有人会将其用于此目的。

你可以在文章“从零开始开发 EA 交易(第 17 部分):访问 Web 数据(三)”中看到这一点。当时,我认为 MetaTrader 5 的资源被大量程序员更广泛地研究。然而,我最终注意到,许多对 MQL5 了解非常肤浅的人出现了,试图强迫 MQL5 去做它设计之初没有做的事情。这就是为什么我停顿了一下,反思并决定创建这些文章,您现在可以用这些文章以愉快的方式学习 MQL5。我想鼓励这些渴望知识的观众,但我们很少有针对初学者的参考资料。

希望这些文章能为你提供一个良好的起点。欢迎提出任何问题和建议以改进此内容。

好吧,让我们回到我们的讨论点。其思路是利用终端全局变量。这是一个非常有趣的资源,可以通过 MQL5 库中定义的函数和过程访问。它简单、实用,在各种情况下都非常有用。现在让我们看看利用此资源的指标代码是什么样子的。就在这里:

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. union u_01
05. {
06.     double  d_value;
07.     uint    u_Counter;
08. }gl_Union;
09. //+------------------------------------------------------------------+
10. int OnInit()
11. {
12.     ZeroMemory(gl_Union);
13. 
14.     if (!GlobalVariableGet(_Symbol, gl_Union.d_value))
15.         if (!GlobalVariableTemp(_Symbol))
16.             return INIT_FAILED;
17. 
18.     GlobalVariableSet(_Symbol, gl_Union.d_value);
19. 
20.     Print(__FUNCTION__);
21. 
22.     return INIT_SUCCEEDED;
23. };
24. //+------------------------------------------------------------------+
25. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[])
26. {
27.     Print(__FUNCTION__, " :: ", ++gl_Union.u_Counter);
28. 
29.     return rates_total;
30. };
31. //+------------------------------------------------------------------+
32. void OnDeinit(const int reason)
33. {
34.     switch(reason)
35.     {
36.         case REASON_CHARTCHANGE:
37.             GlobalVariableSet(_Symbol, gl_Union.d_value);
38.             break;
39.         default:
40.             GlobalVariableDel(_Symbol);
41.     }
42. 
43.     Print(__FUNCTION__);
44. };
45. //+------------------------------------------------------------------+

代码 03

运行代码 03 时,我们会看到类似下图所示的动画:


动画 01

请注意,我们在某个时候更改了图表的时间周期,这样做产生了与我们在前一个主题中讨论的相同的事件序列。这一点可以从终端打印的信息中推断出来。但这并非这里发生的全部事情。请注意,与之前的代码 02 不同,我们没有使用文件来临时存储信息(在本例中是计数器值),而是要求 MetaTrader 5 以非常具体的方式存储此信息。

为了验证这一点,我们可以在解释代码 03 之前,检查它是否存在,如果存在,MetaTrader 5 中有哪些终端全局变量。为此,只需使用热键 F3 或按照下图所示的路径操作:


图 01

如果代码 03 指标正在运行,那么在打开的窗口中我们将看到这样一个变量:


图 02

如果代码 03 中的指标没有运行,则图 02 中显示的同一窗口很可能不包含任何信息,因为据我所知,很少有人出于任何原因使用全局终端变量。但是,如果一种资源存在并且可用,为什么不创造性地使用它呢?

由于我们在图像和动画中看到的信息,我们有足够的元素来理解代码 03 的工作原理。从某种意义上说,我觉得没有必要解释它是如何工作的,因为它只是对代码 02 的简单修改。但是,我不会这样做,因为全局终端变量是一种使用非常具体的资源。借此机会,我将解释如何在其他情况下使用它们。

首先要说明的是,如果您真的想使用全局终端变量,您必须知道它们只能保存 double 类型的值。也就是说,我们不能将文本或整数值放入这样的变量中。一般来说是这样的。然而,由于 MQL5 允许创建联合,正如我们已经看到的,这在日常工作中非常有用,因此我们在第 04 行创建了一个联合,将非浮点值放入全局终端变量中。

因此,我们为使用这些变量开辟了多种可能性,因为在这种情况下,我们可以在其中包含少量文本,前提是文本可以合并到 double 类型中。如果你不太明白我在说什么,我建议你看看之前的文章,了解更多相关信息。

为了避免不必要地访问全局终端变量,我们将联合声明为全局变量。从第 08 行可以明显看出这一点。请注意:我们所做的并非强制性的。我们可以实现这段代码,这样我们就没有全局变量,但这将迫使我们将代码中声明的全局变量移动到代码之外,将其转换为全局终端变量。从某种意义上说,这可能会降低整体性能。然而,在一些非常具体的场景中,使用这种类型的实现可能是有趣和必要的。每个案例都是独一无二的。因此,我们不应该一概而论,或者认为我们必须总是用一种方法而不是另一种方法做事。情况并非总是如此。

出于实际目的,我们使用第 12 行清除计数器将驻留的存储区域。我们也可以通过其他方式做到这一点,所以想想其他方法并使用它们来查看结果。紧接着,在第 14 行,我们尝试读取图 02 中可见的变量。如果这样的变量不存在,那么在第 15 行,我们将尝试创建它。请注意,变量名将是指标所使用的交易品种名称。如果尝试失败,指标将返回 MetaTrader 5 错误值,触发一个新事件 —— 在本例中,由于初始化失败,触发 Deinit 事件。这将导致第 32 行的程序执行,并且该指标将从图表中消失。

有趣的是,如果一个人知道该做什么以及如何生成一切,那么一切都是如何工作的,并且可以非常简单地实现。但是,我希望您明白,第 15 行的这个函数会尝试创建一个临时的全局终端变量,而且重要的是,这必须在第 18 行的调用之前完成。否则,该变量将不是临时的。但即便如此,根据其他标准,该变量仍然是全局的,只是不是我们想要使用的。

当我们创建一个临时全局终端变量时,它只会在 MetaTrader 5 平台打开期间存在。如果它因任何原因关闭,则第 15 行函数创建的所有全局终端变量都将被删除。如果我们想在 MetaTrader 5 打开时(无论运行什么)存储数值,而当平台关闭时这些信息就会丢失,那么这种情况就非常有趣了。但是,只有当全局终端变量是由 GlobalVariableTemp 函数创建时,才会发生这种情况。

我们还可以使用第 18 行的函数创建相同的变量,但不添加删除条件。在这种情况下,MetaTrader 5 会在一段时间后将其删除。请参阅文档以了解更多信息,因为在某些情况下,一方面我们需要信息长时间存在,但另一方面我们不想将其存储在文件中。在这种情况下,全局终端变量会变得非常有趣。

好的,关于 OnCalculate 函数,我们没有什么要补充的了。不过,我们再简单介绍一下 OnDeinit 过程。请注意以下事项:在第 34 行,我们检查是什么情况导致了 Deinit 事件。如果图表时间周期发生变化,正如预期的那样,我们将使用第 37 行来存储计数器的当前值。在其他任何情况下,我们将使用第 40 行删除创建的全局终端变量。但是,如果创建的变量是临时的,为什么要这样做呢?原因纯粹出于教育目的。您可以尝试使用代码 03,并创建您自己的情况类型。没有什么比直接通过代码展示如何创建和删除全局终端变量更公平的了。这就是第 40 行存在的原因。

这个想法是为了表明我们可以通过不同的方式实现相同的结果。手段的选择取决于我们面临的情景或情况的类型。


总结性思考

好了,又一篇文章到此结束。它与前一篇相辅相成,所以你应该将它们结合起来考虑。学习上一篇文章中展示的内容,并运用从本系列其他文章中获得的知识,将有助于你更深入地理解本文中解释和演示的内容。虽然我没有详细解释每一行代码的工作原理,但这并不是我的目的。我想让你学会“如何捕鱼”。我为你提供知识和材料,这样你就可以以最好的方式练习和学习,以实现你的目标。你必须练习和研究所展示的每一个细节。

因此,在下一篇文章中,我们将探讨如何开发一个真正的指标来应用我们迄今为止展示的所有内容。然而,要正确理解下一篇文章的本质,有必要了解事件是如何生成的,以及 MetaTrader 5 可以为我们提供多少帮助;否则,许多门将对你关闭。

在附录中,您可以找到本文讨论的代码。祝你好运,我们下篇文章见!


本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/15733

附加的文件 |
Anexo.zip (1.66 KB)
您应当知道的 MQL5 向导技术(第 64 部分):运用 DeMarker 和包络通道形态,搭配白噪内核 您应当知道的 MQL5 向导技术(第 64 部分):运用 DeMarker 和包络通道形态,搭配白噪内核
DeMarker 振荡器和包络指标是动量和支撑/阻力工具,能够在开发智能系统时配对。我们延续上一篇文章,概述在机器学习中加入把这对指标。我们正在使用一个循环神经网络,利用白噪内核来处理来自这两个指标的向量化信号。这是在一个自定义信号类文件中完成,其与 MQL5 向导汇编的智能系统搭配工作。
MQL5交易工具(第四部分):为多周期扫描仪表盘添加动态定位与切换功能 MQL5交易工具(第四部分):为多周期扫描仪表盘添加动态定位与切换功能
本文将升级MQL5多周期扫描仪表盘,新增拖动与切换功能。通过实现仪表盘的拖拽及最小化/最大化选项,优化屏幕空间的利用率。我们实现并测试这些优化功能,以提升交易的灵活性。
外汇掉期套利:构建合成投资组合,创造持续稳定的掉期收益流 外汇掉期套利:构建合成投资组合,创造持续稳定的掉期收益流
您想利用利率差异获利吗?本文将探讨如何通过外汇掉期套利实现每晚稳定盈利,并构建抗市场波动的投资组合。
MQL5交易策略自动化(第二十二部分):构建基于包络线趋势交易的区间补仓系统 MQL5交易策略自动化(第二十二部分):构建基于包络线趋势交易的区间补仓系统
本文中,我们在MQL5中开发了一个与包络线(Envelopes)趋势交易策略集成的区间补仓系统。我们概述了利用相对强弱指标(RSI)和包络线指标触发交易,并通过管理补仓区域来减轻亏损的架构。通过实现和回测,我们展示了如何为动态市场构建一套有效的自动化交易系统。