从基础到进阶:图表对象(I)
引言
在前一篇文章《从入门到进阶:指标(V)》中,我们介绍了如何对指标进行简易改造,使其(哪怕仅部分)适配MetaTrader 5的图表显示模式。然而,当时并未讲解三种图表模式的用法,因为这需要一些尚未详细说明的前置知识。
在一定范围内,同一个指标可以同时使用K线模式或柱形模式,且基本不会出现问题。如果要使用折线模式,则必须单独新建一个指标,并手动加载到图表上。尽管存在这个小缺陷,后续文章我们会介绍规避该问题的方法。
相信各位已经理解,在这些文章中,在我们达到所需水平之前,可以先暂时应对现有的限制,从而学会如何开展工作,而不必现在就解释每一个实现细节。
即便如此,我们现在就可以开始讨论一些新内容。当然,部分读者会感兴趣,另一些读者可能觉得意义不大。之所以这么说,是因为很多读者暂时还无法领会本文将要探讨的主题的深层含义。但对于另一部分读者而言,本篇及后续文章所演示、讲解的内容会非常有意思,甚至具备很高的实用价值。
因此,我们先从一个例子切入,本文不会深入剖析它,但它将作为本次主题讨论的起点。
特别提醒
交易者希望在图表上查看的数据类型有很多。其中一部分数据在特定场景下具备不同程度的参考价值。K线或柱形计数器就是其中之一。对很多人来说,这类工具毫无用处;但另一部分交易者会用它快速交流对市场走势的判断。
以上是从使用者的视角出发,探讨我们放置在图表上的这些信息。但从程序员的角度出发,情况则有所不同。
我们可以用各种丰富美观的方式在图表上展示信息。然而,在这个简单的示例里,可选的展示方式并不多,核心目标只是在图表画布上输出文本,仅此而已。真正的难点不在于文本本身,如何将文本组织成表格化布局并定期刷新。
很多业余编程爱好者缺少专业开发者那种清晰的工程思维。请不要误解我的意思:我并不是说偶尔编写专用程序的人不懂自己写的代码。问题在于,这类开发者往往意识不到:在MetaTrader 5平台中,如果程序优化不佳,会引发一系列的问题。挂载在图表上的这类程序会持续拖慢平台性能,造成渐进式的性能衰减,严重时甚至会让平台因图表上运行的这些程序而几乎无法使用。
一个仅仅用来在图表上展示文字的简易K线计数器,如果优化不到位,资源消耗会远超实际所需。哪怕本系列文章以教学为首要目的,我们也会尽量写出不会对MetaTrader 5性能造成明显负面影响的代码。如果代码仍有可能引发性能问题,我在此提醒大家务必小心,避免在实操时踩坑。
各位读者,现在回到我们的主题:我们该如何实现一个K线计数器?有人可能会想到做一个在某个刻度上显示数值的应用程序。但问题依然存在:您会选择哪种实现方式?我们已经学习过脚本与指标的编写,大概率会在这两者之间做选择。然而,由于脚本存在一些局限性,最终我们只能选用指标来实现。
那么脚本到底有哪些局限性?如果您做过实验,一定会发现:每当切换图表时间周期,挂载在图表上的脚本就会被移除。即便在脚本内部写入循环,也无法避免此现象。这就意味着,每次切换周期,都需要重新把脚本加载到图表。当然有办法解决脚本运行带来的这类问题,然而,该方案需要用到服务(Services),而服务与图表的交互逻辑相当复杂,并不适合当前阶段的学习。
如果您想了解这套交互架构有多复杂,可以查阅我写的关于复刻/建模系统的文章。但那一系列文章面向已经掌握扎实MQL5编程基础的读者;而本篇属于入门阶段,因此暂不讲解通过服务操控图表的实现方式。
于是剩下的可选方案就是使用指标。没错,但大多数场景下,指标并不适合某些类型的任务。这是因为MetaTrader 5中的指标是以整块代码一次性执行的,如同在图表上独立运行的应用程序。这种设计是为了加速MetaTrader 5的计算。这些内容属于我们在把指标加载到图表时会接触到的一组输入参数范畴。这一点在之前的文章已经介绍过。
然而,柱形计数器只需要在图表生成新柱形时才运行,这时有人会想到用定时器来做同步。这里就会产生问题。在指标内使用定时器其实并不推荐。原因在于一旦定时器代码出现异常,会影响到图表上所有其他指标,甚至短暂阻塞MetaTrader 5,而这类阻塞在交易中可能是致命的。
不过我们依然可以搭建一套机制,用来检测图表上新柱形的生成。使用指标,本质上是最简单、最实用的方案:它可以捕捉新K线生成事件,完成K线计数,随后展示文本。想要实现K线计数,我们需要从基础代码开始。请参阅以下这段代码。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. int OnInit() 05. { 06. return INIT_SUCCEEDED; 07. }; 08. //+------------------------------------------------------------------+ 09. int OnCalculate(const int rates_total, const int prev_calculated, const datetime &Time[], const double &Open[], const double &High[], const double &Low[], const double &Close[], const long &TickVolume[], const long &Volume[], const int &Spread[]) 10. { 11. return rates_total; 12. }; 13. //+------------------------------------------------------------------+
代码01
请注意:每当图表上品种的价格发生变动时,MetaTrader 5就会向所有指标发送Calculate事件。在MetaTrader 5中,Calculate事件并不是按照固定时间周期触发的。假设您打开一张10分钟周期图表:大约每10分钟会生成一根新柱形,但只有价格实际发生变动时,才会触发Calculate事件。在此之前,不会产生任何Calculate事件。
考虑到高波动品种下,Calculate事件的触发间隔可能极短,达到毫秒级别,因此我们必须让Calculate事件内部代码尽可能快速执行。换句话说,不能在事件内部执行多余计算或无谓检索数据,否则会大幅拖慢事件处理流程。
这会对挂载在同一张图表上的所有其他程序产生负面影响。别忘记,我们还没有讨论EA。没错,优化不佳的指标会严重拖累MetaTrader 5的运行性能。不过后续我们会看到更好的实现方案。
现阶段您需要明白:代码执行速度取决于数据检索方式。在代码01中,我们用到了一个数组,用于存放每根柱形的时间戳。请注意,在Calculate事件中,我们无法获取报价变动的确切时刻。我们只能获取当前柱形的生成时间。并且可以根据图表当前的时间周期,预估此根柱形收线所需时长。该信息可用于开发另一类程序,我们后续再介绍。
但在本文中,我们的目标是开发K线计数器,至少要能够检测图表是否生成了新柱形。因此,只要第09行的Time数组的值发生变化,就代表图表出现了一根新柱形。
还有另一种方法,在我看来要简单得多,那就是通过prev_calculated和rates_total这两个变量判断。为什么?原因在于Time数组内的数据类型是日期时间型(datetime)。与之不同的是,prev_calculated和rates_total是MetaTrader 5平台原生提供的整型变量,检测判断更加简单、高效。并且这两个变量仅在图表产生新柱形时才会变化,因此很容易识别新柱形。
除了后面将要讲到的另一个问题之外,大家明白我想表达的核心思路吗?采用哪种实现方式并不是重点。关键是要搞清楚,为了达成目标,我们手上有哪些可用信息。为验证这一点,我们将代码01修改为以下所示的代码02。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. int OnInit() 05. { 06. return INIT_SUCCEEDED; 07. }; 08. //+------------------------------------------------------------------+ 09. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 10. { 11. return rates_total; 12. }; 13. //+------------------------------------------------------------------+
代码02
现在,我们有了一个切入点:所有问题的核心,在于正确理解当前场景,并学会利用手头可用的数据。实现功能不存在唯一正确(或错误)的方案;要达成相同的目标、得到相同的结果,方法不止一种,只是有的更简洁。好了,那我们该如何判断图表上是否生成了新柱形?各位读者,这反而是最简单的部分。我们只需要按照以下所示修改代码02,就能实现。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. int OnInit() 05. { 06. return INIT_SUCCEEDED; 07. }; 08. //+------------------------------------------------------------------+ 09. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 10. { 11. if ((prev_calculated > 0) && (prev_calculated != rates_total)) 12. Print("New Bar..."); 13. 14. return rates_total; 15. }; 16. //+------------------------------------------------------------------+
代码03
代码03完整地演示了我们想要说明的内容,也就是刚才所讲的原理。换句话说,这里提供了一种非常简洁的方法,用来判断当前柱形何时收盘、新柱形何时生成。我们可以通过以下动画演示来验证这一点。

动画01
由于代码03本身非常简洁,此动画演示也很直观,因此我们可以省去额外讲解,直接进入本文最有意思的部分。不过在此之前,我们需要开启一个新主题。
图表对象
和很多人的固有认知不同,图表上出现的一切,本质上都可以视为对象。当然,这些对象根据开发目标,用途各不相同,但这并不会改变它们属于图表对象这一事实。还有一点需要注意:我们这里所说的对象,和面向对象编程(OOP)没有关系。接下来的内容会开始变得复杂。对象分为两大类,更准确地说,在行情图表上放置对象有两种方式。这就是MetaTrader 5中图表的组织方式。
第一类对象,其坐标体系绑定在图表坐标系;第二类对象,则依靠屏幕像素坐标定位。两类对象在屏幕渲染效果和程序交互方式上都存在差异。然而,部分对象类型之间存在一定的关联。选对合适的对象类型,能大幅简化开发工作,自然也能提升代码执行速度。
因为这是我们第一次接触此类知识,所以暂时放下K线计数器,先要搞清楚如何操作这些图表对象。
建议先查阅MQL5文档里的“对象类型”章节作为入门。您可以看到全部对象的列表。您可能会觉得可用对象种类不够、有所缺失,但事实并非如此。在我看来,MQL5提供的图表对象已经十分充足。老实说,我们并不会用到全部元素。但既然这些对象已经内置可用,完全可以善加利用。此外,很多对象还有非常巧妙的用法,能帮我们节省大量开发时间。
为演示相关用法,我们编写一小段代码,展示如何操作图表上的对象。相关代码如下所示。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_NameChannel "Demo" 05. //+------------------------------------------------------------------+ 06. int OnInit() 07. { 08. ObjectCreate(0, def_NameChannel, OBJ_REGRESSION, 0, 0, 0); 09. 10. return INIT_SUCCEEDED; 11. }; 12. //+------------------------------------------------------------------+ 13. int OnCalculate(const int rates_total, const int prev_calculated, const datetime &Time[], const double &Open[], const double &High[], const double &Low[], const double &Close[], const long &TickVolume[], const long &Volume[], const int &Spread[]) 14. { 15. const int n = 20, 16. a = 2; 17. 18. if ((prev_calculated > (n + a)) && (prev_calculated != rates_total)) 19. { 20. ObjectMove(0, def_NameChannel, 0, Time[rates_total - (n + a)], Close[rates_total - (n + a)]); 21. ObjectMove(0, def_NameChannel, 1, Time[rates_total - a], Close[rates_total - a]); 22. } 23. 24. return rates_total; 25. }; 26. //+------------------------------------------------------------------+ 27. void OnDeinit(const int reason) 28. { 29. ObjectDelete(0, def_NameChannel); 30. ChartRedraw(); 31. }; 32. //+------------------------------------------------------------------+
代码04
代码04展示了我们在指标中创建的第一个图表对象的效果。看到代码后,您可能会想:“这看起来不像指标,里面没有前几篇文章讲过的那些结构。”没错,读者朋友,代码04确实是一个指标。您可以查看OnCalculate函数来确认,该函数负责处理MetaTrader 5产生的计算事件。
但代码04中的这个指标并非传统意义上的指标。它的用途更加专一,用来完成一项特定任务。在分析代码04的工作原理之前,我们先看一下它在图表上运行的效果,参见下方动画。

动画02
之所以必须用动画来展示效果,是因为仅靠静态图片,不足以理解接下来要介绍的内容。不过,在我们开始“折腾”这段代码之前,我们先弄懂代码04的工作机制。第04行是一段定义语句,作用是为待创建的图表对象指定名称。没错,所有放置在图表上的对象都必须设置名称。大家从现在开始要习惯这一点。
因此,第08行只是尝试创建一个对象。本例中创建的是线性回归通道。由于此时还不确定通道的绘制位置,所以暂时不执行其他操作。接下来流程是这样的:指标会持续等待,直到MetaTrader 5触发满足特定条件的Calculate事件。我们等待的判断条件写在第18行。请注意,代码04的第18行和代码03的第11行非常相似,目标也完全一致:等待新柱形的生成。在新柱形出现之前,第08行所创建的对象不会显示在图表上。
一旦新柱形生成,程序就会执行第20行和第21行代码,完成线性回归通道的绘制。看一下这部分的实现方式,稍后我们再进行深入讲解。目前只需明白,这里并没有什么晦涩难懂的逻辑。第20行指定通道绘制的起始位置,第21行指定绘制的终止位置。这样一来,得到的通道会跟随价格,回归计算区间为20根柱形,并且不包含当前未收盘柱形。
当您通过程序创建并添加图表对象时,良好的编程规范要求:在程序结束时,必须将该对象从图表上删除。我们在Deinit事件处理器的第29行实现该清理逻辑。
现在,最有趣的部分开始了。如果我们修改线性回归的计算方式以及结果展示形式,指标会呈现什么样的表现?想要找到答案,我们将对代码04进行大幅改写,参见如下。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_NameChannel "Demo" 05. //+------------------------------------------------------------------+ 06. int OnInit() 07. { 08. ObjectCreate(0, def_NameChannel, OBJ_REGRESSION, 0, 0, 0); 09. 10. return INIT_SUCCEEDED; 11. }; 12. //+------------------------------------------------------------------+ 13. int OnCalculate(const int rates_total, const int prev_calculated, const datetime &Time[], const double &Open[], const double &High[], const double &Low[], const double &Close[], const long &TickVolume[], const long &Volume[], const int &Spread[]) 14. { 15. const int n = 20, 16. a = 1; 17. 18. if (prev_calculated > (n + a)) 19. { 20. ObjectMove(0, def_NameChannel, 0, Time[rates_total - (n + a)], Close[rates_total - (n + a)]); 21. ObjectMove(0, def_NameChannel, 1, Time[rates_total - a], Close[rates_total - a]); 22. } 23. 24. return rates_total; 25. }; 26. //+------------------------------------------------------------------+ 27. void OnDeinit(const int reason) 28. { 29. ObjectDelete(0, def_NameChannel); 30. ChartRedraw(); 31. }; 32. //+------------------------------------------------------------------+
代码05
请注意,相较于代码04,代码05仅修改了OnCalculate事件处理器。但得到的结果,至少可以说是相当有趣的。您可以参考以下动画直观感受。

动画03
动画03展示出的效果相当夸张,尤其要注意这个回归通道是实时生成的。也就是说,每一次新报价到来时,线性回归通道就会发生变动。根据参数配置的不同,您可以得到灵敏度极高的趋势指标,其灵敏度甚至超过指数移动平均线(EMA)。至于如何利用这一特性,就留给当下那些热衷于开发自调节线性回归通道的交易者去探索吧。
或许您会心生疑惑:“那这样一个指标到底有什么实际用处?”我只能回答:我也不知道。不可否认的是,它的效果确实新奇有趣。然而,思路灵活的开发者已经能够想到它的其他用途:只需要更换创建的对象类型,并且直接设置锚定点即可。这些图表对象会自动更新调整,无需手动干预。对象一旦挂载到图表上,我们预先定义的规则就会自动生效。
我们再来看另外一个示例。这一次我们将使用另一种类型的图表对象。请记住,接下来演示的所有内容仅用于教学学习。参见以下代码。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_NameChannel "Demo" 05. //+------------------------------------------------------------------+ 06. int OnInit() 07. { 08. ObjectCreate(0, def_NameChannel, OBJ_TREND, 0, 0, 0); 09. ObjectSetInteger(0, def_NameChannel, OBJPROP_RAY_RIGHT, true); 10. 11. return INIT_SUCCEEDED; 12. }; 13. //+------------------------------------------------------------------+ 14. int OnCalculate(const int rates_total, const int prev_calculated, const datetime &Time[], const double &Open[], const double &High[], const double &Low[], const double &Close[], const long &TickVolume[], const long &Volume[], const int &Spread[]) 15. { 16. const int n = 60; 17. double p = DBL_MIN; 18. int t = 0; 19. 20. for (int c = rates_total - (n + 1); c < rates_total; c++) 21. p = (High[c] > p ? High[t = c] : p); 22. ObjectMove(0, def_NameChannel, 0, Time[t], p); 23. p = DBL_MIN; 24. for (int c = t + 1; c < rates_total; c++) 25. p = ((High[c] > p) && (Open[c] < Close[c]) ? High[t = c] : p); 26. ObjectMove(0, def_NameChannel, 1, Time[t], p); 27. 28. return rates_total; 29. }; 30. //+------------------------------------------------------------------+ 31. void OnDeinit(const int reason) 32. { 33. ObjectDelete(0, def_NameChannel); 34. ChartRedraw(); 35. }; 36. //+------------------------------------------------------------------+
代码 06
运行代码06后,生成效果如下所示。

动画04
各位读者,不要对眼前的效果过度兴奋,这仅仅只是演示示例。无论如何都不能把它当成可以直接投入使用的成品程序。在动画04中,可以看到程序依据代码06内置的判定条件,自动生成了一条下降趋势线。“但这是怎么做到的?” 简单来说:我只是把自己定义的下降趋势线判定规则写进了代码。
现在我们来拆解背后的实现逻辑。第08行和代码04中第08行的写法类似,指定我们想要在图表上创建的对象类型。之后在第09行修改该对象的其中一项属性。假如代码06没有设置该属性,运行效果就会和以下图片一致。

图片01
观察图片01,我们可以清晰地看到这条趋势线的绘制起点与终点。本例中我们要识别的是下降趋势线,而上升趋势线则需要采用另一套判定规则。由于本文的目的并不是讲解如何全自动识别并绘制趋势线,但我们这里使用的判定逻辑和方法确实可以生效。那么,这个指标是如何找到绘制趋势线所需点位的?这看起来像是魔法。
不,读者朋友,这完全不是什么魔法,而只是一套基础判定规则的应用。请重点关注OnCalculate事件处理器。这里包含两个循环:第一个循环用来查找趋势线的第一个定位点;第二个循环用来查找绘制趋势线所需的第二个定位点。找到第一个点位之后,通过第22行代码为对象设置第一个锚定点。紧接着找到第二个点位,通过第26行设置第二个锚定点。剩下的渲染绘制工作全部由MetaTrader 5平台完成。
即便如此,我需要着重强调:绝不应该把此指标看成什么值得称道的作品,它也不是可直接复用的成熟方案。开发它仅仅是为了复现图片01与动画04中的演示效果。现实场景下您几乎很难遇到和演示效果一样完美的行情样本。
“好的,我明白了。那我们是否可以修改其他属性,做出更具个性化的图表对象?”当然可以,各位读者。还有一点需要注意:和直接在图表上手动修改属性不一样,部分图表对象的一些属性只能通过代码修改,无法在图形界面调整。
我们来看一个修改对象属性的简单示例,参见如下。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_NameChannel "Demo" 05. //+------------------------------------------------------------------+ 06. int OnInit() 07. { 08. ObjectCreate(0, def_NameChannel, OBJ_TREND, 0, 0, 0); 09. ObjectSetInteger(0, def_NameChannel, OBJPROP_COLOR, clrLawnGreen); 10. ObjectSetInteger(0, def_NameChannel, OBJPROP_WIDTH, 4); 11. 12. return INIT_SUCCEEDED; 13. }; 14. //+------------------------------------------------------------------+ . . .
代码 07
在代码07中,我们只展示了相较于代码06需要修改的代码段。但由此可见,第09行指定图表对象所要使用的颜色,第10行设置线条宽度。执行这段代码后,得到的效果参见以下图片。

图片02
各位读者,请务必留意。在当前版本代码中,无法通过快捷键调出指标设置窗口来修改线条颜色与宽度,参见以下图片。

图片03
想要修改这类对象的属性,必须打开另一个对话框。也就是说,本例需要使用下图所示的对象属性窗口。

图片04
我明白这套操作看起来十分繁琐,但这就是在MetaTrader 5中处理这类问题的经典方式。不过,我们也有办法规避这类问题:让用户直接在指标参数配置界面修改图表对象属性。但我们不必急于实现,现在我们才刚刚接触图表对象。相信大家已经有足够多的内容可以动手试验,为下一篇文章做好准备。
总结
在本文中,我们借助专门编写的演示代码,开始学习如何直接操作图表上的绘图对象。虽然本次入门介绍相对浅显,但已经可以看出,只要冷静理清目标、妥善规划任务,我们就可以实现很多有意思的功能。想要做到这一点,您需要多加练习,学习MQL5提供的每一类对象的工作原理,掌握它们在MetaTrader 5当中的使用方法。如果不能真正理解平台提供的各类能力,您就无法将自己的想法落地实现。
和本系列之前的文章一样,完整的代码参见附件。但针对本篇内容,我必须特别提醒:本文提供的全部代码仅用于学习,用来练习文中讲解的知识点。请勿将文中代码直接当作成品程序使用,也不要用于学习以外的其他用途,否则有可能会给您造成实实在在的资金损失。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/15975
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
在 MQL5 中实现来自其他语言的实用模块(第 03 部分):移植 Python 的 Schedule 模块,打造增强版 OnTimer
科学家群体优化(CoSO):理论
交易中的神经网络:摆脱特定数据依赖的时间序列泛化(结论)
通过图表同步简化技术分析