English Русский Español Deutsch 日本語 Português
preview
开发回放系统(第 77 部分):新 Chart Trade(四)

开发回放系统(第 77 部分):新 Chart Trade(四)

MetaTrader 5示例 |
260 0
Daniel Jose
Daniel Jose

概述

在上一篇文章“开发回放系统(第 76 部分):新 Chart Trade(三)”中,我讲解了 DispatchMessage 代码中最关键的部分,并开始讨论应该如何设计通信过程 —— 或者更准确地说,通信协议。

在深入本文的主要主题之前,我们需要对前面介绍的代码进行一点调整。之前解释的一切仍然有效。但为了稳定系统,这种修改是必要的。之后,我们就可以进入本文的真正重点了。


进一步稳定 DispatchMessage 代码

由于鼠标指标与 Chart Trade 交互存在一定缺陷,需要进行小范围修改。我无法完全解释为什么交互有时会失败。但经过如下所示的代码调整,问题就消失了。

259. //+------------------------------------------------------------------+
260.       void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
261.          {
262. #define macro_AdjustMinX(A, B)    {                          \
263.             B = (A + m_Info.Regions[MSG_TITLE_IDE].w) > x;   \
264.             mx = x - m_Info.Regions[MSG_TITLE_IDE].w;        \
265.             A = (B ? (mx > 0 ? mx : 0) : A);                 \
266.                                  }
267. #define macro_AdjustMinY(A, B)   {                           \
268.             B = (A + m_Info.Regions[MSG_TITLE_IDE].h) > y;   \
269.             my = y - m_Info.Regions[MSG_TITLE_IDE].h;        \
270.             A = (B ? (my > 0 ? my : 0) : A);                 \
271.                                  }
272.                               
273.             static short sx = -1, sy = -1, sz = -1;
274.             static eObjectsIDE obj = MSG_NULL;
275.             short   x, y, mx, my;
276.             double dvalue;
277.             bool b1, b2, b3, b4;
278.             ushort ev = evChartTradeCloseAll;
279.    
280.             switch (id)
281.             {
282.                case CHARTEVENT_CHART_CHANGE:
283.                   x = (short)ChartGetInteger(GetInfoTerminal().ID, CHART_WIDTH_IN_PIXELS);
284.                   y = (short)ChartGetInteger(GetInfoTerminal().ID, CHART_HEIGHT_IN_PIXELS);
285.                   macro_AdjustMinX(m_Info.x, b1);
286.                   macro_AdjustMinY(m_Info.y, b2);
287.                   macro_AdjustMinX(m_Info.minx, b3);
288.                   macro_AdjustMinY(m_Info.miny, b4);
289.                   if (b1 || b2 || b3 || b4) AdjustTemplate();
290.                   break;
291.                case CHARTEVENT_MOUSE_MOVE:
292.                   if ((*m_Mouse).CheckClick(C_Mouse::eClickLeft))
293.                   {                  
294.                      switch (CheckMousePosition(x = (short)lparam, y = (short)dparam))
295.                      {
296.                         case MSG_MAX_MIN:
297.                            if (sz < 0) m_Info.IsMaximized = (m_Info.IsMaximized ? false : true);
298.                            break;
299.                         case MSG_DAY_TRADE:
300.                            if ((m_Info.IsMaximized) && (sz < 0)) m_Info.IsDayTrade = (m_Info.IsDayTrade ? false : true);
301.                            break;
302.                         case MSG_LEVERAGE_VALUE:
303.                            if ((m_Info.IsMaximized) && (sz < 0)) CreateObjectEditable(obj = MSG_LEVERAGE_VALUE, m_Info.Leverage);
304.                            break;
305.                         case MSG_TAKE_VALUE:
306.                            if ((m_Info.IsMaximized) && (sz < 0)) CreateObjectEditable(obj = MSG_TAKE_VALUE, m_Info.FinanceTake);
307.                            break;
308.                         case MSG_STOP_VALUE:
309.                            if ((m_Info.IsMaximized) && (sz < 0)) CreateObjectEditable(obj = MSG_STOP_VALUE, m_Info.FinanceStop);
310.                            break;
311.                         case MSG_TITLE_IDE:
312.                            if (sx < 0)
313.                            {
314.                               DeleteObjectEdit();
315.                               ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, false);
316.                               sx = x - (m_Info.IsMaximized ? m_Info.x : m_Info.minx);
317.                               sy = y - (m_Info.IsMaximized ? m_Info.y : m_Info.miny);
318.                            }
319.                            if ((mx = x - sx) > 0) ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_XDISTANCE, mx);
320.                            if ((my = y - sy) > 0) ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_YDISTANCE, my);
321.                            if (m_Info.IsMaximized)
322.                            {
323.                               m_Info.x = (mx > 0 ? mx : m_Info.x);
324.                               m_Info.y = (my > 0 ? my : m_Info.y);
325.                            }else
326.                            {
327.                               m_Info.minx = (mx > 0 ? mx : m_Info.minx);
328.                               m_Info.miny = (my > 0 ? my : m_Info.miny);
329.                            }
330.                            break;
331.                         case MSG_BUY_MARKET:
332.                            ev = evChartTradeBuy;
333.                         case MSG_SELL_MARKET:
334.                            ev = (ev != evChartTradeBuy ? evChartTradeSell : ev);
335.                         case MSG_CLOSE_POSITION:
336.                            if ((m_Info.IsMaximized) && (sz < 0))
337.                            {
338.                               string szTmp = StringFormat("%d?%s?%c?%d?%.2f?%.2f", ev, _Symbol, (m_Info.IsDayTrade ? 'D' : 'S'), m_Info.Leverage, 
339.                                                          FinanceToPoints(m_Info.FinanceTake, m_Info.Leverage), FinanceToPoints(m_Info.FinanceStop, m_Info.Leverage));                           
340.                               PrintFormat("Send %s - Args ( %s )", EnumToString((EnumEvents) ev), szTmp);
341.                               sz = x;
342.                               EventChartCustom(GetInfoTerminal().ID, ev, 0, 0, szTmp);
343.                            }
344.                            break;
345.                      }
346.                      if (sz < 0)
347.                      {
348.                         sz = x;
349.                         AdjustTemplate();
350.                         if (obj == MSG_NULL) DeleteObjectEdit();
351.                      }
352.                   }else
353.                   {
354.                      sz = -1;
355.                      if (sx > 0)
356.                      {
357.                         ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, true);                  
358.                         sx = sy = -1;
359.                      }
360.                   }
361.                   break;
362.                case CHARTEVENT_OBJECT_ENDEDIT:
363.                   switch (obj)
364.                   {
365.                      case MSG_LEVERAGE_VALUE:
366.                      case MSG_TAKE_VALUE:
367.                      case MSG_STOP_VALUE:
368.                         dvalue = StringToDouble(ObjectGetString(GetInfoTerminal().ID, m_Info.szObj_Editable, OBJPROP_TEXT));
369.                         if (obj == MSG_TAKE_VALUE)
370.                            m_Info.FinanceTake = (dvalue <= 0 ? m_Info.FinanceTake : dvalue);
371.                         else if (obj == MSG_STOP_VALUE)
372.                            m_Info.FinanceStop = (dvalue <= 0 ? m_Info.FinanceStop : dvalue);
373.                         else
374.                            m_Info.Leverage = (dvalue <= 0 ? m_Info.Leverage : (short)MathFloor(dvalue));
375.                         AdjustTemplate();
376.                         obj = MSG_NULL;
377.                         ObjectDelete(GetInfoTerminal().ID, m_Info.szObj_Editable);
378.                         break;
379.                   }
380.                   break;
381.                case CHARTEVENT_OBJECT_CLICK:
382.                   if (sparam == m_Info.szObj_Chart) if ((*m_Mouse).CheckClick(C_Mouse::eClickLeft)) switch (obj = CheckMousePosition(x = (short)lparam, y = (short)dparam))
383.                   {
384.                      case MSG_DAY_TRADE:
385.                         m_Info.IsDayTrade = (m_Info.IsDayTrade ? false : true);
386.                         DeleteObjectEdit();
387.                         break;
388.                      case MSG_MAX_MIN:
389.                         m_Info.IsMaximized = (m_Info.IsMaximized ? false : true);
390.                         DeleteObjectEdit();
391.                         break;
392.                      case MSG_LEVERAGE_VALUE:
393.                         CreateObjectEditable(obj, m_Info.Leverage);
394.                         break;
395.                      case MSG_TAKE_VALUE:
396.                         CreateObjectEditable(obj, m_Info.FinanceTake);
397.                         break;
398.                      case MSG_STOP_VALUE:
399.                         CreateObjectEditable(obj, m_Info.FinanceStop);
400.                         break;
401.                   }
402.                   if (obj != MSG_NULL) AdjustTemplate();
403.                   break;
404.                case CHARTEVENT_OBJECT_DELETE:
405.                   if (sparam == m_Info.szObj_Chart) macro_CloseIndicator(C_Terminal::ERR_Unknown);
406.                   break;
407.             }
408.             ChartRedraw();
409.          }
410. //+------------------------------------------------------------------+

C_ChartFloatingRAD.mqh 文件片段

请注意,有些行已被删除,例如第 314 行和第 341 行。两者都被移至位于第 346 行的测试中。此调整解决了点击某些控件时出现的稳定性问题。每个对象中都使用变量 sz 。这可以在第 297、300、303、306 和 309 行以及第 312 和 336 行的条件测试中看到。

与之前的版本相比,本次修改特别稳定了鼠标指标与 Chart Trade 之间的交互。以前,如果首先加载鼠标指标,某些 Chart Trade 控件将无法正确响应。唯一的解决方法是从图表中删除指标并重新插入。只有这样,控件才能正常运行。至少可以说很奇怪。

因此,应该从处理代码中删除 CHARTEVENT_OBJECT_CLICK 事件。因此,必须从上一篇文章中展示的代码中删除 381 到 403 之间的所有行。由于这些变化不会改变之前给出的任何解释,我们现在可以继续讨论本文的主要主题。


理解消息协议背后的规划

亲爱的读者,我不能假设你对计算机通信系统已经了解多少。为了避免让任何人掉队,我将从头开始解释。如果您已经有过此类协议的经验,其中大部分可能是显而易见的,您可以跳到下一节。

在上一篇文章中,我解释了为什么数值必须转换为文字(字符串)等效物。例如需要传输二进制值 0001 0011,则必须将其转换为字符串“19”。这意味着传输两个字节而不是一个字节。虽然这可能看起来效率低下,但目标不是效率,而是清晰度:确保接收方正确理解信息。当然,效率是可取的,但准确性是优先考虑的。

如前所述,我们将使用 “sparam” 字段。现在的挑战是:如果信息必须在单个字符串中传输,我们如何确定一条数据的结束和另一条数据开始的位置?毕竟,这是关键问题。

为此设计一个策略至关重要。您需要一种格式,以便以后提取每条数据。有几种可行的方法,每种方法都有其优点和缺点。一种选择是为每个字段使用固定长度的数组,它们连接在一起,形成传输的字符串。这种方法有其优点和缺点。

这使得索引变得简单,因为每个块的大小总是相同的。然而,如果字段没有完全使用其分配的空间,则会浪费内存和带宽。事实上,空位会占用带宽,在我们的例子中,就是占用内存。

为了更清楚地说明,请看下面的图片:

图 1

蓝色块表示标记字符串结束的 NULL 字符。请注意,其中一个数组的位置为空,浪费了空间。正是这种情况使得使用固定大小的数组变得困难。

另一种方法是使用与数据大小完全匹配的可变长度数组。这避免了浪费空间,使提取更容易。缺点是增加了复杂性,我们需要编写更多的代码。随着我们创建更多的代码,我们需要对其进行测试。此外,在许多情况下,如果信息超过预期大小,则存在数据丢失的风险。图 2 显示了此方法的理想化版本:

图 2

正如您在第一张图片中看到的,蓝色块表示标记行结束的字符所在的位置。请注意,这里我们有一个理想化的状态,其中每种颜色代表将被压缩成一行的信息。尽管在某些情况下,这可能会使字符串的提取复杂化,但对于完整正确地恢复原始信息来说,这种情况似乎已经足够了。然而,在这种情况下,每组都有一个固定的大小。然后,我们得到了一种比第一幅图所示更先进的方法。但是,例如,如果一个预期为两个字节的字段突然需要三个字节,就会出现问题。

这将是最糟糕的情况。在组装绿色组的过程中,其中一个字节将丢失。如果绿色组可以增长以容纳这个额外的字节会怎样?可以这样做吗?不可以。如果我们这样做,绿色组之后的所有信息都会受到损害,并且接收方将无法理解绿色组中有三个字节,而不是它期望的两个字节。

这表明协议设计并非易事。如果接收方期望固定大小,而发送方更改了它们,则通信失败。我们需要采取不同的方法。

一种可能是可变长度块,其中每个字段可以根据需要尽可能大。那样会好得多,不是吗?但随后出现了一个新问题:接收方如何知道一个字段在哪里结束,下一个字段从哪里开始?这需要分隔符或标记。但在这里你需要仔细考虑,否则你最终可能会发送一条接收方无法理解的信息,即使这条信息适合你。但对于接收方来说,一切怎么可能不那么清楚呢?

想想看:消息的每一部分都可以是任何大小,使我们能够传达几乎任何类型的信息。无论信息是什么,都必须按照一定的顺序排列,但必须始终遵守顺序。到现在为止,一直都还不错。同样,我们如何表明一个部分已经完成,另一个部分正在开始?

这是最难的部分。这完全取决于我们放入字符串的数据类型。如果我们在一个字节中使用所有 255 个可能的值 —— 同样,我们必须避免使用空字符,因此 255 而不是 256 —— 我们有一个大问题:如何指示我们在字符串块中提供不同的信息。如果我们将值减少到 32 到 127 之间的字符,我们将不得不做更多的组装工作。但这允许我们使用 128 到 255 之间的任何值作为标记符号。

然而,我们可以进一步限制。我们只能使用字母数字字符来传达所需的信息。因此,我们可以保留标点符号作为分隔符。为什么我们可以在这里这样做?这是有效的,因为我们需要传输的数据相对简单:资产名称和数值,如杠杆、止盈和止损水平。这些是在 Chart Trade 中配置的,但必须传输给 EA 交易。

但除了这些非常简单的值之外,我们还需要传递一个值。尽管 MetaTrader 5 已经处理了这一点,但我们将包括操作类型,以确保通信处于控制之下。这不是必需的。

请记住:通信不一定只发生在一个终端内。通过适当的网络协议,一台计算机可以运行终端,而另一台计算机则可以管理订单和头寸。即使是一台普通的机器也可以处理交易,就像它更强大一样。

我们不会进一步讨论细节。这个想法是为了展示连接将如何实际工作。


结合两个世界的优点

我们选择的解决方案结合了固定长度和可变长度方法的优点。我们将以字母数字字符串的形式传输数据,为了清晰起见使用分隔符,并且仍然允许索引。每个字段可以根据需要占用尽可能多的字符,确保不会丢失任何内容。

请参阅前面代码片段的第 338 行。为了更清楚地说明这一点,让我们看一个实际的发送示例。

图 3

图 3 显示了 Chart Trade 传输的真实示例。乍一看,这条信息似乎令人困惑。为了理解它,我们需要看看片段中的第 338 行发生了什么。该消息仍然遵循定义良好的协议。从本质上讲,我们正在使用两全其美的方法。它允许一行中的块具有任何大小,同时以特定方式对行中的信息进行索引。

你可能不了解索引是如何工作的,也不太明显,但它确实存在。注意消息中的 D 符号。请注意,它的前后都是同一个字符。此时,我们只有一个块,由字符 D 填充。这种情况不会发生在消息的其他任何地方。这表明这个字符可以以某种方式被索引。然而,这将在以后变得更加清晰。现在,让我们专注于了解这里发生了什么。

在这个特定消息中,第一个块包含一个字符,指示要执行的操作类型。MetaTrader 5 将再次向我们的 EA 交易提供此信息,稍后将看到。然而,在这里,我考虑到 Chart Trade 确实会与 EA 交易进行通信 - 无论是通过网络、电子邮件还是通过其他方式。因此,我明确指定要执行的操作类型。

值得注意的一个细节是:值 9 对应于市场买入事件。但这个值可能会有所不同。例如,如果块包含值 11,这确实表示关闭所有仓位。在这种情况下,该块将包含两个字符,而不是上面显示的单个字符。但为什么买入用的是 9,而全部平仓用的是 11 呢?这些值从何而来?这是一个非常有效的问题。查看第 338 行,字符串中放置的第一个值是一个 ushort 值。但仅凭这一点并不能解释为什么 9 表示买入,而 11 表示全部平仓,确实不能。

现在看第 278 行。这个值从何而来?它来自头文件 Defines.mqh。请密切注意:在 Defines.mqh 内部有一个名为 EnumEvents 的枚举。此枚举从零开始,对于每个新元素,编译器都会将值加一。从第一个事件 evHideMouse 开始算起,第 9 个事件是 evChartTradeBuy,第 11 个事件是 evChartTradeCloseAll。现在您知道出现在字符串开头的这些值来自哪里了:它们源自 EnumEvents 枚举。

让我们继续。请注意,所有问号字符都以紫色突出显示。终止字符串的 NULL 字符以蓝色标记。由于每个块都由问号分隔,因此我们可以根据需要插入尽可能多的字母数字字符来发送消息。但有一些重要的细节,该消息必须按照特定的顺序构造。请记住:接收方期望按照给定的顺序接收数据。尽管从理论上讲,信息可以按随机顺序排列,但接收方(在我将要演示的版本中)并不期望这样做。

下一个字符块提供订单所针对的资产的名称。同样,如果 EA 交易和 Chart Trade 在同一张图表上操作,则没有必要这样做。但我正在考虑并非如此的情况。

此外,将来您将看到这些消息详细信息可用于其他目的。在示例中,资产是 BOVA11,它是一只 ETF。如果不使用分隔符,资产名称会使问题复杂化。因为在某些市场中,资产符号由四个字母数字字符组成,而在其他市场中,它可能有五个。在本例中,我们是五个字符。即使在 B3(巴西证券交易所),许多资产也使用四个字符的符号。

还有一点,请记住,这里的目标是设计 Chart Trade,它也可以用于回放/模拟。在这种情况下,交易品种名称可以包含任意数量的字母数字字符。因此,动态大小的块是非常可取的。

现在我们回到我们的 D 字符。在此阶段,我希望您再次查看第 338 行。如果操作不是当天平仓的类型(即日内交易),则此块中的字符将会有所不同。它将被 S 替换。如果您愿意,您可以选择任何其他字符,但请记住也更新接收方;否则通信将中断,因为接收方可能无法正确解释字母或字符序列。

紧接着,我们得到了一个文字值:250。这代表什么?再次查看第 338 行:该值是所需的杠杆水平。这里有一个有趣的点,我们使用三个数字字符来表示杠杆值。

我们可以使用二进制值吗?这似乎是合适的,因为杠杆不可能为零。但有一个条件阻止了这一点。这并不是因为我们无法用对应于 250 的字符来格式化字符串,而是因为分隔符本身。找一个 ASCII 表并查看问号的值,是 63。

为了使这更容易,我在下面提供了从字符 0 到字符 128 的 ASCII 表。

图4

为什么 63 对我们很重要?因为任何包含 63 的杠杆值都会被接收方(我将很快展示)解释为分隔符。换句话说,接收方将不会意识到第四个块代表杠杆水平。

你可能会想:如果我通过加 63 来抵消杠杆值呢?由于杠杆率永远不会为 0,因此第一个有效值将变为 64。问题解决了吗?我希望事情就这么简单。但事实并非如此:通过增加 63 至杠杆,您只是在推迟问题的发生。

理由如下。您可能想知道:有什么问题?如果我加上 63,所有的值都会大于 63 吧?这就是关键所在。问题在于,在编程中没有任何值是无限的。每个值都有一个最大值,这取决于所使用的字节数大小。即使在 64 位处理器(例如 MetaTrader 5 当前运行的处理器)上,系统仍然依赖 8 位概念进行字符处理。

这意味着即使使用 64 位处理器,您实际上也无法计算出 2^64(18,446,744,073,709,551,615)个字符。最多可以数到255,这对应2^8。为什么呢?这个问题可以解决吗?是的,一种方法是使用 ASCII 以外的字符集,例如 Unicode。

但还有另一个问题。StringFormat 不使用 Unicode,至少在撰写本文时是这样。MQL5 中的字符串函数通常遵循 C/C++ 原则,因此使用 ASCII。尽管 C/C++ 可以处理 Unicode,但最初情况并非如此。

因此,即使您将杠杆增加 63,每 255 个位置仍会产生一个复合值。这将是一个因子和当前计数的组合,该因子显示计数周期达到 255 的次数,值 575 是因子 2 加上 63,等等。

为了正确表示这一点,您需要两个字节:第二个字节在某个阶段始终是 63。第一个字节表示因子,即达到最大计数的次数(2^8,如上所述)。这介绍了各种数学含义,我在这里不再赘述,因为它们超出了本文的范围。

总结一下如何构建此消息协议的解释:请注意,我们还有两类值可以表示为 double 或 float。出于与杠杆相关的相同原因,这些必须写成文字值。这就是为什么它们在图片中呈现出那样的样子。

但你现在可能会问:为什么这些值看起来是这样的?它们代表什么?它们可能看起来很奇怪,因为您可能忘记检查代码片段的第 338 行。在那里,货币价值被转换成点数。因此,值 3.60 对应 900 美元,而 3.02 对应 755 美元。

为什么不直接使用货币价值而使用点数?原因很简单。使用已转换的值来实现 EA 交易比内部执行转换要简单得多。现在可能还不完全清楚,但以后你就会看到好处。我们将在未来更深入地探讨这一点,因为需要更多的解释来充分理解将预转换值直接发送到 EA 交易的优点。但是,正如我已经说过的,这是为了未来。


结论

在本文中,我尝试尽可能详细地解释如何创建通信协议。该主题尚未完成,因为我们仍需要查看负责接收这些消息的部分。然而,我相信主要目标已经实现:说明为什么在设计通信协议时必须小心,特别是如果你选择使用与我在这里介绍的不同的东西。

这是你现在必须计划的事情。如果你把它留到以后,你最终会很难让你的协议正确地传输信息。这些信息对于 EA 交易了解要做什么和如何做至关重要。不要拖延,现在就开始学习,并开始做出您认为应该实施的必要调整。在下一篇文章中,我们将最终研究接收方 - 即 EA 交易本身。

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

附加的文件 |
Anexo.zip (420.65 KB)
MQL5自动化交易策略(第四部分):构建多层级区域恢复系统 MQL5自动化交易策略(第四部分):构建多层级区域恢复系统
本文将介绍如何在MQL5中开发一个基于相对强弱指数(RSI)生成交易信号的多层级区域恢复(反转)系统(Multi-Level Zone Recovery System)。该系统通过动态数组结构管理多个信号实例,使区域恢复逻辑能够同时处理多重交易信号。通过这种设计,我们展示了如何在保持代码可扩展性和健壮性的前提下,有效应对复杂的交易管理场景。
集成学习模型中的门控机制 集成学习模型中的门控机制
在本文中,我们继续探讨集成模型,重点讨论“门控”的概念,尤其是门控如何通过整合模型输出来提升预测准确性或模型泛化能力。
以 MQL5 实现强化分类任务的融汇方法 以 MQL5 实现强化分类任务的融汇方法
在本文中,我们讲述以 MQL5 实现若干融汇分类器,并讨论了它们在不同状况下的功效。
3D 柱线上的趋势强度和方向指标 3D 柱线上的趋势强度和方向指标
我们将研究一种市场趋势分析新方法,基于市场微观结构的三维可视化、及张量分析。