从基础到中级:对象(四)
概述
在上一篇文章“从基础到进阶:对象(三)”中,我们展示了如何实现一个目的非常简单的指标:无需打开 MetaTrader 5 菜单,即可轻松在图表上放置趋势线。
我知道很多人认为这部分内容完全没必要。然而,请记住,这里的目标并非实现某一具体功能,而是展示我们如何实现它。它是否得到实现以及是否带来实际效益,都只是细节问题。最重要的是,亲爱的读者,你要明白,任何人都可以通过学习和应用这些文章所展示的内容,将想法付诸实践。
在上一篇文章中,我们所看到和所做的只是我们真正能够实现的冰山一角,一旦我们开始为特定目的编写自己的应用程序,我们将能够取得更大的成就。在本文中,我们将尝试展示一些非常有趣的内容:一个隐藏在 MetaTrader 5 中的指标,几乎无人知晓其存在。
所以,让我们遵循我们通常的惯例:在学习这篇文章时,把任何可能让你分心的事物都放在一边,然后继续阅读下一节,看看这个指标是什么。
MetaTrader 5 中的一个隐藏指标
许多人声称 MetaTrader 5 的市场分析指标或工具很少。就我个人而言,我对此强烈反对。受限的不是平台或其内容,而是大多数人的狭隘观念,他们总是期望找到现成且完全符合他们想象的东西。
MetaTrader 5 包含了一个几乎无人察觉的元素,因为它隐藏在另一个指标之中。实际上,它并非严格意义上的指标,而是一个分析对象。我们需要把这个对象调出来,才能让隐藏的指标 — 或者更确切地说,这个分析对象 — 显现出来。默认情况下,它可以按照在 MetaTrader 5 中的实现形式使用,但如果我们对其进行正确修改,它所显示的信息将变得更加清晰,因为其用途将与原本大相径庭。
我特指斐波那契对象。当然,我们也可以使用趋势线来创建我们即将实现的功能,但使用斐波那契对象可以更容易地构建出我们需要的对象。
首先,让我们看一下上一篇文章中创建的指标代码。如下所示,该代码将作为构建另一种类型指标的起点:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_Prefix "Demo" 05. //+------------------------------------------------------------------+ 06. #define macro_NameObject def_Prefix + (string)(ObjectsTotal(0) + 1) 07. //+------------------------------------------------------------------+ 08. #include <Tutorial\File 01.mqh> 09. //+------------------------------------------------------------------+ 10. st_Cross gl_Cross; 11. //+------------------------------------------------------------------+ 12. int OnInit() 13. { 14. gl_Cross.Init(); 15. 16. IndicatorSetString(INDICATOR_SHORTNAME, def_Prefix); 17. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); 18. ChartSetInteger(0, CHART_CROSSHAIR_TOOL, false); 19. 20. return INIT_SUCCEEDED; 21. }; 22. //+------------------------------------------------------------------+ 23. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 24. { 25. return rates_total; 26. }; 27. //+------------------------------------------------------------------+ 28. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 29. { 30. st_TimePrice tp; 31. static string isPaint = ""; 32. 33. switch (id) 34. { 35. case CHARTEVENT_KEYDOWN: 36. if (TerminalInfoInteger(TERMINAL_KEYSTATE_ESCAPE)) gl_Cross.Hide(); 37. break; 38. case CHARTEVENT_MOUSE_MOVE: 39. if (((uchar)sparam & MOUSE_MIDDLE) != 0) 40. { 41. tp = gl_Cross.Move((ushort)lparam, (ushort)dparam); 42. if (isPaint == "") 43. { 44. ObjectCreate(0, isPaint = macro_NameObject, OBJ_TREND, 0, tp.Time, tp.Price); 45. ObjectSetInteger(0, isPaint, OBJPROP_SELECTABLE, true); 46. ObjectSetInteger(0, isPaint, OBJPROP_COLOR, clrMagenta); 47. ObjectSetInteger(0, isPaint, OBJPROP_WIDTH, 3); 48. ObjectSetInteger(0, isPaint, OBJPROP_RAY_RIGHT, true); 49. } 50. ObjectMove(0, isPaint, 1, tp.Time, tp.Price); 51. gl_Cross.Show(); 52. }else 53. { 54. isPaint = ""; 55. gl_Cross.Hide(); 56. } 57. break; 58. case CHARTEVENT_MOUSE_WHEEL: 59. break; 60. } 61. ChartRedraw(); 62. }; 63. //+------------------------------------------------------------------+ 64. void OnDeinit(const int reason) 65. { 66. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, false); 67. ChartSetInteger(0, CHART_CROSSHAIR_TOOL, true); 68. 69. gl_Cross.Hide(); 70. 71. if (reason == REASON_REMOVE) 72. ObjectsDeleteAll(0, def_Prefix); 73. }; 74. //+------------------------------------------------------------------+
代码 01
虽然这段代码能够运行,但对大多数用户来说,它有一个小缺点:它使用鼠标中键来创建我们想要添加到图表中的对象。这并不常见,因为通常我们会使用鼠标左键进行此类操作。不过,这只是一个小细节,目前我们并不关心。为了能够使用鼠标左键,我们必须改变代码工作方式的一些方面,但我们稍后再做处理。
目前,我们想用 OBJ_FIBO 替换 OBJ_TREND 对象。通过这种简单的替换,我们得到以下结果:

动画 01
请注意,我们已从趋势线改为斐波那契对象。然而,我们并不打算以这种形式使用创建的对象,因为我们想要开发的对象是基于这个斐波那契对象,但并不完全相同。我们将对斐波那契对象进行修改。为了简化这项任务,我们将从下面的代码开始。
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. //+------------------------------------------------------------------+ 004. #define def_Prefix "Demo" 005. //+------------------------------------------------------------------+ 006. #define macro_NameObject def_Prefix + (string)(ObjectsTotal(0) + 1) 007. //+------------------------------------------------------------------+ 008. #include <Tutorial\File 01.mqh> 009. //+------------------------------------------------------------------+ 010. st_Cross gl_Cross; 011. //+------------------------------------------------------------------+ 012. int OnInit() 013. { 014. gl_Cross.Init(); 015. 016. IndicatorSetString(INDICATOR_SHORTNAME, def_Prefix); 017. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); 018. ChartSetInteger(0, CHART_CROSSHAIR_TOOL, false); 019. 020. return INIT_SUCCEEDED; 021. }; 022. //+------------------------------------------------------------------+ 023. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 024. { 025. return rates_total; 026. }; 027. //+------------------------------------------------------------------+ 028. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 029. { 030. st_TimePrice tp; 031. static string isPaint = ""; 032. 033. switch (id) 034. { 035. case CHARTEVENT_KEYDOWN: 036. if (TerminalInfoInteger(TERMINAL_KEYSTATE_ESCAPE)) gl_Cross.Hide(); 037. break; 038. case CHARTEVENT_MOUSE_MOVE: 039. if (((uchar)sparam & MOUSE_MIDDLE) != 0) 040. { 041. tp = gl_Cross.Move((ushort)lparam, (ushort)dparam); 042. if (isPaint == "") 043. { 044. ObjectCreate(0, isPaint = macro_NameObject, OBJ_FIBO, 0, tp.Time, tp.Price); 045. Modifier_OBJ_FIBO(isPaint); 046. } 047. ObjectMove(0, isPaint, 1, tp.Time, tp.Price); 048. gl_Cross.Show(); 049. }else 050. { 051. isPaint = ""; 052. gl_Cross.Hide(); 053. } 054. break; 055. case CHARTEVENT_MOUSE_WHEEL: 056. break; 057. } 058. ChartRedraw(); 059. }; 060. //+------------------------------------------------------------------+ 061. void OnDeinit(const int reason) 062. { 063. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, false); 064. ChartSetInteger(0, CHART_CROSSHAIR_TOOL, true); 065. 066. gl_Cross.Hide(); 067. 068. if (reason == REASON_REMOVE) 069. ObjectsDeleteAll(0, def_Prefix); 070. }; 071. //+------------------------------------------------------------------+ 072. void Modifier_OBJ_FIBO(const string szNameObj) 073. { 074. const double nLevels[] = 075. { 076. 0, 077. 1, 078. 2.5 079. }; 080. const string sLevels[] = 081. { 082. "Stop", 083. "Enter", 084. "Take" 085. }; 086. const color cLevels[] = 087. { 088. clrRed, 089. clrBlue, 090. clrGreen 091. }; 092. 093. ObjectSetInteger(0, szNameObj, OBJPROP_SELECTABLE, false); 094. ObjectSetInteger(0, szNameObj, OBJPROP_COLOR, clrNONE); 095. ObjectSetInteger(0, szNameObj, OBJPROP_WIDTH, 3); 096. ObjectSetInteger(0, szNameObj, OBJPROP_RAY_RIGHT, true); 097. 098. ObjectSetInteger(0, szNameObj, OBJPROP_LEVELS, nLevels.Size()); 099. for (uint c = 0; c < nLevels.Size(); c++) 100. { 101. ObjectSetDouble(0, szNameObj, OBJPROP_LEVELVALUE, c, nLevels[c]); 102. ObjectSetInteger(0, szNameObj, OBJPROP_LEVELCOLOR, c, cLevels[c]); 103. ObjectSetInteger(0, szNameObj, OBJPROP_LEVELWIDTH, c, 2); 104. ObjectSetString(0, szNameObj, OBJPROP_LEVELTEXT, c, sLevels[c]); 105. } 106. } 107. //+------------------------------------------------------------------+
代码 02
代码 02 非常有趣,尤其是因为它几乎完全实现了我想展示的对象。这怎么可能?亲爱的读者,如果您仔细观察,就会发现我们在第 45 行调用了位于第 72 行的函数。可以说,这个函数看起来有点混乱,因为它远没有我最初希望的那样井然有序。然而,我认为这样更容易理解它是如何工作的。
在解释该函数如何运作之前,让我们先看一下下面动画中显示的结果:

动画 02
如您所见,我们想要实现的目标非常简单。这个工具可以用来做什么?许多人在市场上使用某些类型的交易策略,而在许多这些策略中,目标位和止损位通常都非常清晰明确。理解这一点很重要,以便准确理解正在实施的内容,以及最重要的是,如何为特定目的修改这种实施方式。
这个修改后的 OBJ_FIBO 使我们能够准确检查这些条件:在进入交易之前,我们可以看到入场订单的位置、止损位置以及目标位置。通过这些信息,我们可以评估交易是否可行;如果止损位置设置错误,那么肯定会被触及,同样,如果目标计算错误,那么目标位很可能无法触及。
由于一些专业交易员在实际入市交易前几分钟就会进行分析,因此这个工具被证明非常有用且不可或缺,因为它能准更准确地判断交易过程中可能出现的情况。
好的,但是这个工具是如何创建的呢?我们如何对其进行改进或调整以适应我们的工作方式?为了理解这一点,我们现在重点关注 Modifier_OBJ_FIBO 过程,该过程从代码 02 的第 72 行开始。
在这里我们可以看到,已经创建了三个数组。每个数组都有其特定的用途,并且它们之间是相互关联的。在了解每个数组的用途之前,让我们先考虑一下这个过程的其他方面。在第 93 行,我们更改了 OBJ_FIBO 对象的一个属性,使用户无法通过鼠标单击选择它。这一点很重要,因为我们不希望对象在放置后被移动。
然而,这又带来了另一个问题:我们将无法通过点击对象然后按 DELETE 键来删除对象。不过,可以从显示图表对象列表的窗口中删除它,或者从图表中移除该指标。
第 94 行代码阻止了图表上虚线的显示。在动画 01 中,这条虚线原本以洋红色显示,但当它被指定为 clrNONE 颜色时,虽然对象线本身仍然存在,但虚线却不再可见。接下来的两行,即第 95 行和第 96 行,只更改 OBJ_FIBO 对象的属性;这里没有什么其他需要注意的。现在,我们继续讨论真正感兴趣的部分,即第 98 行代码。
OBJ_FIBO 对象由多个水平组成。无论我们使用哪种斐波那契对象变体,都是从这些水平中创建的。由于 MetaTrader 5 不知道应该创建多少个水平,我们使用第 98 行来告诉它。此时,平台将知道应该有多少个水平。我们可以指定更多或更少的水平;但是,由于我们在这里寻找的是动画 02 中所示的修改,因此我们将只使用几个水平,即三个水平。
现在要注意了,每个水平都有其独特的属性。由于这些水平可以用趋势线甚至曲线来表示,目的相同,我们可以指定如何在图表上绘制每条线,从而构建所需的形态。
这种形态是如何形成的?这就是我们正在修改的对象变得有趣的地方。请注意,在循环内部,我们遍历了之前不久定义的数组。这些数组中的每一个都为正在创建的线条的某个属性赋值,这很容易理解。然而,有一个细节可能稍微有些复杂:那就是在第 74 行定义的 nLevels 数组。
这个数组中最难的部分恰恰在于其值。“为什么要使用这些特定的值?我们能否使用其他选项?”要理解这一点,我们首先需要了解斐波那契数列的另一个方面。在斐波那契对象中,我们处理的是从 0 到 1 的值;这个范围内的所有内容都会显示在大家熟悉的斐波那契标准网格中。然而,我们可以赋予小于 0 和大于 1 的值,从而创建对象本身的扩展投影。请注意以下事项:当我们在图表上开始绘制 OBJ_FIBO 对象时,初始锚点对应于值 1,而当我们拖动对象以创建它时,我们得到的是 0 值的位置。
这或许看起来很奇怪,但事实就是如此。因此,当我们在第 78 行添加值 2.5 时,我们并不是告诉它创建一个投影,其距离是点 1 和点 0 之间距离的 2.5 倍,而是告诉它创建一个投影,其距离是该距离的 1.5 倍。
通过这种方式,我们计算出可能交易的目标位置投影。如果您希望使用 1:1 的目标比例,则应在第 78 行指定值 2;这样,入场点和止损点之间存在的相同距离将被投影为交易的目标距离。
你可能会觉得这很疯狂,确实如此,因为这非常容易实现,比如,可以包含部分水平。你只需要将相应的值添加到第 74 行声明的数组中即可。请记住,你还需要调整另外两个数组,以确保图表上的显示与在 MetaTrader 5 中使用自定义指标时预期的一致。
看到了吧?不需要太多东西。我们真正需要的只是创造力和创造的渴望。然后,通过应用本系列其他文章中介绍的基础知识,我们开发并实现了我们的想法。就这么简单。
我们在这里所做的事情非常有趣,它并不局限于上面所展示的内容:我们可以走得更远。为了证明这一点,我们来看一下:最初,代码 02 生成的代码与动画 02 中看到的内容类似;但是,如果我们只将 Modifier_OBJ_FIBO 过程的内容替换为其他内容,如下面的代码片段所示,会发生什么情况呢?
. . . 71. //+------------------------------------------------------------------+ 72. void Modifier_OBJ_FIBO(const string szNameObj) 73. { 74. #define macro_Mod_OBJ_FIBO(txt, pos, cor, width, style) { \ 75. ObjectSetDouble(0, szNameObj, OBJPROP_LEVELVALUE, levels, pos); \ 76. ObjectSetInteger(0, szNameObj, OBJPROP_LEVELCOLOR, levels, cor); \ 77. ObjectSetInteger(0, szNameObj, OBJPROP_LEVELWIDTH, levels, width); \ 78. ObjectSetInteger(0, szNameObj, OBJPROP_LEVELSTYLE, levels, style); \ 79. ObjectSetString(0, szNameObj, OBJPROP_LEVELTEXT, levels, txt); \ 80. levels++; \ 81. } 82. 83. int levels = 0; 84. 85. ObjectSetInteger(0, szNameObj, OBJPROP_SELECTABLE, false); 86. ObjectSetInteger(0, szNameObj, OBJPROP_COLOR, clrNONE); 87. ObjectSetInteger(0, szNameObj, OBJPROP_RAY_RIGHT, true); 88. 89. macro_Mod_OBJ_FIBO("Stop", 0, clrRed, 2, STYLE_SOLID); 90. macro_Mod_OBJ_FIBO("Enter", 1, clrBlue, 2, STYLE_DASH); 91. macro_Mod_OBJ_FIBO("Partial", 1.5, clrYellowGreen, 1, STYLE_DASHDOTDOT); 92. macro_Mod_OBJ_FIBO("Take", 2, clrGreen, 2, STYLE_SOLID); 93. ObjectSetInteger(0, szNameObj, OBJPROP_LEVELS, levels); 94. 95. #undef macro_Mod_OBJ_FIBO 96. } 97. //+------------------------------------------------------------------+
代码 03
由于其余代码保持不变,因此在代码 03 中,我们只显示我们真正感兴趣的部分。但是,如果我们按照代码 03 中的更改运行程序,结果将完全不同,如下图所示。

图 01
请注意,如图 01 所示,我们这里实现的效果无法通过直接在图表上操作斐波那契对象来实现。在这种情况下,止损点与目标点的比例为 1:1,在距离的 50% 处有一个部分平仓。
最难的部分恰恰在于色彩和线条的结合。如果你对此表示怀疑,不妨尝试在 MetaTrader 5 菜单中插入一个斐波那契对象,然后重现这一过程,你会发现这是不可能实现的。
在代码 03 场景中,我认为 Modifier_OBJ_FIBO 过程更容易修改和配置,因为我们使用了一个宏,使得创建水平变得非常容易。任何新的水平都可以通过将其放在第 93 行之前来添加,以便 MetaTrader 5 知道图表上应该显示多少个水平以及哪些水平。
“你展示的内容很有趣,但我有个问题。我理解这些文章的目的不是教人如何创建一个完整的应用程序,但如果我们希望用户与最后一个指标进行交互,我们该如何实现呢?我这么问是因为编译后设置的值就无法再更改了。我们至少应当如何调整止损点与目标价位之间的关系?”这很容易解释;我们有时间来看看如何做到这一点。为了区分各个主题,让我们更详细地看一下。
调整止损-目标比率
有几种方法可以让用户指定止损点和目标点之间的比例。一种简单的方法是使用一个双精度值,如下所示:
. . . 09. //+------------------------------------------------------------------+ 10. input double user01 = 1.5; //Stop-Target Relationship 11. //+------------------------------------------------------------------+ . . . 73. //+------------------------------------------------------------------+ 74. void Modifier_OBJ_FIBO(const string szNameObj) 75. { 76. #define macro_Mod_OBJ_FIBO(txt, pos, cor, width, style) { \ 77. ObjectSetDouble(0, szNameObj, OBJPROP_LEVELVALUE, levels, pos); \ 78. ObjectSetInteger(0, szNameObj, OBJPROP_LEVELCOLOR, levels, cor); \ 79. ObjectSetInteger(0, szNameObj, OBJPROP_LEVELWIDTH, levels, width); \ 80. ObjectSetInteger(0, szNameObj, OBJPROP_LEVELSTYLE, levels, style); \ 81. ObjectSetString(0, szNameObj, OBJPROP_LEVELTEXT, levels, txt); \ 82. levels++; \ 83. } 84. 85. int levels = 0; 86. 87. ObjectSetInteger(0, szNameObj, OBJPROP_SELECTABLE, false); 88. ObjectSetInteger(0, szNameObj, OBJPROP_COLOR, clrNONE); 89. ObjectSetInteger(0, szNameObj, OBJPROP_RAY_RIGHT, true); 90. 91. macro_Mod_OBJ_FIBO("Stop", 0, clrRed, 2, STYLE_SOLID); 92. macro_Mod_OBJ_FIBO("Enter", 1, clrBlue, 2, STYLE_DASH); 93. macro_Mod_OBJ_FIBO("Partial", 1 + (user01 / 2), clrYellowGreen, 1, STYLE_DASHDOTDOT); 94. macro_Mod_OBJ_FIBO("Take", 1 + user01, clrGreen, 2, STYLE_SOLID); 95. ObjectSetInteger(0, szNameObj, OBJPROP_LEVELS, levels); 96. 97. #undef macro_Mod_OBJ_FIBO 98. } 99. //+------------------------------------------------------------------+
代码 04
代码 04 片段是建立止损点与其目标点之间直接关系的最简单方法。只需要在片段中添加第 10 行,并调整第 93 行和第 94 行即可生成关系。运行代码 04 后,我们得到如下图所示的结果:

图 02
调整图 02 所示的比例后,即可直接在图表中使用,结果将与下图所示类似:

图 03
考虑到这种方法如此简单,这确实令人惊叹。您可能会好奇,如果用户输入的比率值小于 1,即他们想在止损和目标之间的比率不利的情况下进行交易,会发生什么?会发生什么,以及应该为指标指定什么值才能使其正常工作?我并不想鼓励这种情况,但为了获得止损大于最终目标的比率,我们可以使用类似于下图所示的方法:

图 04
由于该值小于 1,因此比率不佳。将该指标应用于图表时,您将看到类似以下的结果:

图 05
在这个例子中,我们使用了相同的数据作为入场点;然而,通过对比图表可以看出,所得输出结果却大相径庭。因此,我们得到了一种实用且功能强大的工具:我们可以将应用程序放置在图表上,进行分析,更改值(如图 04 所示),然后重复分析,这使我们能够同时在图表上保持多个分析。
唯一的缺点可能是线条会相交,但通过允许用户绘制或不绘制向右延长射线,这一问题很容易解决。为此,只需按如下方式修改代码:
. . . 09. //+------------------------------------------------------------------+ 10. input double user01 = 1.5; //Stop-Target Relationship 11. input bool user02 = true; //Extend lines to the right 12. //+------------------------------------------------------------------+ . . . 73. //+------------------------------------------------------------------+ 74. void Modifier_OBJ_FIBO(const string szNameObj) 75. { 76. #define macro_Mod_OBJ_FIBO(txt, pos, cor, width, style) { \ 77. ObjectSetDouble(0, szNameObj, OBJPROP_LEVELVALUE, levels, pos); \ 78. ObjectSetInteger(0, szNameObj, OBJPROP_LEVELCOLOR, levels, cor); \ 79. ObjectSetInteger(0, szNameObj, OBJPROP_LEVELWIDTH, levels, width); \ 80. ObjectSetInteger(0, szNameObj, OBJPROP_LEVELSTYLE, levels, style); \ 81. ObjectSetString(0, szNameObj, OBJPROP_LEVELTEXT, levels, txt); \ 82. levels++; \ 83. } 84. 85. int levels = 0; 86. 87. ObjectSetInteger(0, szNameObj, OBJPROP_SELECTABLE, false); 88. ObjectSetInteger(0, szNameObj, OBJPROP_COLOR, clrNONE); 89. ObjectSetInteger(0, szNameObj, OBJPROP_RAY_RIGHT, user02); 90. 91. macro_Mod_OBJ_FIBO("Stop", 0, clrRed, 2, STYLE_SOLID); 92. macro_Mod_OBJ_FIBO("Enter", 1, clrBlue, 2, STYLE_DASH); 93. macro_Mod_OBJ_FIBO("Partial", 1 + (user01 / 2), clrYellowGreen, 1, STYLE_DASHDOTDOT); 94. macro_Mod_OBJ_FIBO("Take", 1 + user01, clrGreen, 2, STYLE_SOLID); 95. ObjectSetInteger(0, szNameObj, OBJPROP_LEVELS, levels); 96. 97. #undef macro_Mod_OBJ_FIBO 98. } 99. //+------------------------------------------------------------------+
代码 05
同样,这是代码 05 片段中的一个简单更改;在这种情况下,我们添加了第 11 行。它的值在第 89 行用于控制线条是否延伸到最右边缘。下图展示了一种线条不延伸的设置。

图 06
因此,在图表上我们得到类似这样的结果:

图 07
请注意:我们在图 05 中增加了一项新的分析,并且虽然我们更改了指标设置(图 06)但没有将其从图表中移除,这并不会删除或影响已经存在的内容。只需稍加努力,我们就创造出了真正有趣的东西。
尽管许多人声称在 MetaTrader 5 中有些事情是不可能的,但我们刚刚看到,只需具备最基本的知识,几乎任何事情都能完成,无需借助花哨技巧或复杂方法。
所以,这一切都很有趣,但还有一个问题我们将在下一个话题中进行分析。
使用鼠标左键创建图形
用鼠标中键在图表上绘图可能显得不太自然。大多数用户习惯于使用左键进行交互,因此对中键感到陌生。尽管我们测试的代码运行得非常完美,但它并不是我们最熟悉的选择,作为程序员,我们需要解决这个问题。然而,这里有一个小小的细节需要注意。
当按下鼠标中键时,所有鼠标事件都会被路由到第 38 行,如代码 02 所示。按住按钮时,十字准星仍然可见,从而可以创建绘图。当松开鼠标中键时,代码 02 的第 52 行会移除十字准星,表示我们不能再与绘图系统进行交互。
如果你理解了这一点,那么我们需要做什么就变得很清楚了:当按下鼠标中键时,会生成一个事件,该事件必须等待第二个事件 — 按下 ESC 键或点击鼠标左键 — 才能像以前一样创建图形并定位对象,但使用鼠标左键而不是鼠标中键。
这就是目标。实现这一目标的方法有很多,有些方法比其他方法更费时费力;由于教学清晰度是这里的主要优先事项,我们将选择最简单的途径,即使我们知道这并非在所有情况下都是理想的做法。这一点可以从代码 06 片段中看出:
. . . 029. //+------------------------------------------------------------------+ 030. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 031. { 032. #define macro_CLEAN_EVENT { \ 033. isPaint = ""; \ 034. gl_Cross.Hide(); \ 035. bMouseL = false; \ 036. } 037. 038. st_TimePrice tp; 039. static string isPaint = ""; 040. static bool bMouseL = false; 041. 042. switch (id) 043. { 044. case CHARTEVENT_KEYDOWN: 045. if (TerminalInfoInteger(TERMINAL_KEYSTATE_ESCAPE)) macro_CLEAN_EVENT; 046. break; 047. case CHARTEVENT_MOUSE_MOVE: 048. tp = gl_Cross.Move((ushort)lparam, (ushort)dparam); 049. if (((uchar)sparam & MOUSE_LEFT) != 0) 050. { 051. if (isPaint != "") 052. { 053. bMouseL = true; 054. if (!ObjectGetInteger(0, isPaint, OBJPROP_TIME)) ObjectMove(0, isPaint, 0, tp.Time, tp.Price); 055. ObjectMove(0, isPaint, 1, tp.Time, tp.Price); 056. } 057. }else if (bMouseL) macro_CLEAN_EVENT; 058. if ((((uchar)sparam & MOUSE_MIDDLE) != 0) && (isPaint == "")) 059. { 060. ObjectCreate(0, isPaint = macro_NameObject, OBJ_FIBO, 0, 0, 0); 061. Modifier_OBJ_FIBO(isPaint); 062. gl_Cross.Show(); 063. } 064. break; 065. case CHARTEVENT_MOUSE_WHEEL: 066. break; 067. } 068. ChartRedraw(); 069. 070. #undef macro_CLEAN_EVENT 071. }; 072. //+------------------------------------------------------------------+ . . .
代码 06
其中,我们用中间按钮激活绘图功能,但只有按下左按钮才能绘图。并非一切都完美:这个想法可行,但它存在一个问题,可以在动画 03 中看到:

动画 03
在这个动画中,由于图表不断移动,绘制的难度显而易见。这种移动有时是可取的,但在这种情况下,它弊大于利。在展示如何解决这个问题之前,让我们先看一下使用左键需要进行的更改,如动画 03 所示。
首先,我在第 32 行添加了一个宏,允许绘图创建事件在其当前状态下完成。然后,在鼠标事件处理中,我更改了执行顺序,以便正确处理事件:首先,按下鼠标中键;此时,第 60 行创建 OBJ_FIBO 对象,根据需要对其进行配置,并显示十字准星。
此时,如果我们按下鼠标左键,将执行第 51 行的检查,并在第 53 行标记一个新点;同时,第 54 行和第 55 行会将 OBJ_FIBO 对象放置在图表上。第 53 行创建的标记允许第 57 行在绘制 OBJ_FIBO 对象后(即释放左键时)立即从图表中移除十字准星。
为了修复动画 03 中显示的问题,我们需要修改代码 06 片段,添加两行新代码。以下代码片段反映了这些更改:
. . . 029. //+------------------------------------------------------------------+ 030. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 031. { 032. #define macro_CLEAN_EVENT { \ 033. isPaint = ""; \ 034. gl_Cross.Hide(); \ 035. bMouseL = false; \ 036. ChartSetInteger(0, CHART_MOUSE_SCROLL, true); \ 037. } 038. 039. st_TimePrice tp; 040. static string isPaint = ""; 041. static bool bMouseL = false; 042. 043. switch (id) 044. { 045. case CHARTEVENT_KEYDOWN: 046. if (TerminalInfoInteger(TERMINAL_KEYSTATE_ESCAPE)) macro_CLEAN_EVENT; 047. break; 048. case CHARTEVENT_MOUSE_MOVE: 049. tp = gl_Cross.Move((ushort)lparam, (ushort)dparam); 050. if (((uchar)sparam & MOUSE_LEFT) != 0) 051. { 052. if (isPaint != "") 053. { 054. bMouseL = true; 055. if (!ObjectGetInteger(0, isPaint, OBJPROP_TIME)) ObjectMove(0, isPaint, 0, tp.Time, tp.Price); 056. ObjectMove(0, isPaint, 1, tp.Time, tp.Price); 057. } 058. }else if (bMouseL) macro_CLEAN_EVENT; 059. if ((((uchar)sparam & MOUSE_MIDDLE) != 0) && (isPaint == "")) 060. { 061. ObjectCreate(0, isPaint = macro_NameObject, OBJ_FIBO, 0, 0, 0); 062. ChartSetInteger(0, CHART_MOUSE_SCROLL, false); 063. Modifier_OBJ_FIBO(isPaint); 064. gl_Cross.Show(); 065. } 066. break; 067. case CHARTEVENT_MOUSE_WHEEL: 068. break; 069. } 070. ChartRedraw(); 071. 072. #undef macro_CLEAN_EVENT 073. }; 074. //+------------------------------------------------------------------+ . . .
代码 07
使用代码 07 时,我们会得到如下动画中显示的结果:

动画 04
代码 07 片段中第 36 行和第 62 行的简单使用解决了动画 03 中的问题,这似乎很奇怪。如果你这么认为,可能是因为你可能没有尝试过之前文章中介绍的方法,而我在那些文章中已经展示过这些函数以及其他函数。然而,当时我们尚未启用和禁用标准 MetaTrader 5 配置来提供像现在这样的交互。
最后的探讨
或许这篇文章是目前为止最有趣的一篇。我们仅利用了所看到和解释过的一小部分内容,就成功实现了 MetaTrader 5 默认情况下不具备的功能,这使我们能够通过修改平台上用于其他目的的现有对象,来创建一个非常令人印象深刻的指标。
我知道,很多人认为,在编写如此有趣且令人愉快的东西之前,他们需要学习更多的知识。然而,这项工作并未就此结束:我们提出的指标仍然存在一个小缺陷。我们故意保留了这种状态,以便你能练习修复错误。我们会解释问题所在,但解决方案将由您来决定。
在代码 07 片段中,第 61 行创建了 OBJ_FIBO 对象,然后在第 64 行显示十字准星。问题是,如果用户(或您)按下 ESC 键,则会执行第 46 行,并将十字准星从图表中移除,但第 61 行创建的 OBJ_FIBO 对象仍保留在对象列表中,即使它不可见,因为它尚未定位。
你的任务是解决这个问题,使得只有在按下鼠标中键之后再按下鼠标左键,才能将 OBJ_FIBO 对象添加到对象列表中。还记得在生成十字准星后创建对象时出现的问题,以及我们是如何展示解决方法的吗?
这个问题的解决方法非常相似:只需改变代码 07 片段中某些操作的执行顺序即可;无需创建任何子程序或添加新的函数或过程。如果操作顺序正确,就可以移除十字准星,并防止不必要地创建 OBJ_FIBO 对象。因此,用户可以按下 ESC 键,对象列表中将不会包含图表上未显示的不可见对象。
本文所使用的代码见附件。练习解决这个任务,我们下一篇文章再见。
| MQ5 文件 | 描述 |
|---|---|
| 代码 01 | 对象示例 |
| 代码 02 | 对象示例 |
| 代码 03 | 对象示例 |
| 代码 04 | 对象示例 |
| 代码 05 | 对象示例 |
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/16026
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
交易机器人风险管理器(第一部分):EA风控头文件
MQL5交易工具(第八部分):具备可拖拽与可最小化功能的增强型信息仪表盘
基于Mamba选择性状态空间模型的神经网络交易机器人开发
基于协整股票的统计套利(第二部分):EA、回测与优化