开发回放系统(第 77 部分):新 Chart Trade(四)
概述
在上一篇文章“开发回放系统(第 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” 字段。现在的挑战是:如果信息必须在单个字符串中传输,我们如何确定一条数据的结束和另一条数据开始的位置?毕竟,这是关键问题。
为此设计一个策略至关重要。您需要一种格式,以便以后提取每条数据。有几种可行的方法,每种方法都有其优点和缺点。一种选择是为每个字段使用固定长度的数组,它们连接在一起,形成传输的字符串。这种方法有其优点和缺点。
这使得索引变得简单,因为每个块的大小总是相同的。然而,如果字段没有完全使用其分配的空间,则会浪费内存和带宽。事实上,空位会占用带宽,在我们的例子中,就是占用内存。
为了更清楚地说明,请看下面的图片:

蓝色块表示标记字符串结束的 NULL 字符。请注意,其中一个数组的位置为空,浪费了空间。正是这种情况使得使用固定大小的数组变得困难。
另一种方法是使用与数据大小完全匹配的可变长度数组。这避免了浪费空间,使提取更容易。缺点是增加了复杂性,我们需要编写更多的代码。随着我们创建更多的代码,我们需要对其进行测试。此外,在许多情况下,如果信息超过预期大小,则存在数据丢失的风险。图 2 显示了此方法的理想化版本:

正如您在第一张图片中看到的,蓝色块表示标记行结束的字符所在的位置。请注意,这里我们有一个理想化的状态,其中每种颜色代表将被压缩成一行的信息。尽管在某些情况下,这可能会使字符串的提取复杂化,但对于完整正确地恢复原始信息来说,这种情况似乎已经足够了。然而,在这种情况下,每组都有一个固定的大小。然后,我们得到了一种比第一幅图所示更先进的方法。但是,例如,如果一个预期为两个字节的字段突然需要三个字节,就会出现问题。
这将是最糟糕的情况。在组装绿色组的过程中,其中一个字节将丢失。如果绿色组可以增长以容纳这个额外的字节会怎样?可以这样做吗?不可以。如果我们这样做,绿色组之后的所有信息都会受到损害,并且接收方将无法理解绿色组中有三个字节,而不是它期望的两个字节。
这表明协议设计并非易事。如果接收方期望固定大小,而发送方更改了它们,则通信失败。我们需要采取不同的方法。
一种可能是可变长度块,其中每个字段可以根据需要尽可能大。那样会好得多,不是吗?但随后出现了一个新问题:接收方如何知道一个字段在哪里结束,下一个字段从哪里开始?这需要分隔符或标记。但在这里你需要仔细考虑,否则你最终可能会发送一条接收方无法理解的信息,即使这条信息适合你。但对于接收方来说,一切怎么可能不那么清楚呢?
想想看:消息的每一部分都可以是任何大小,使我们能够传达几乎任何类型的信息。无论信息是什么,都必须按照一定的顺序排列,但必须始终遵守顺序。到现在为止,一直都还不错。同样,我们如何表明一个部分已经完成,另一个部分正在开始?
这是最难的部分。这完全取决于我们放入字符串的数据类型。如果我们在一个字节中使用所有 255 个可能的值 —— 同样,我们必须避免使用空字符,因此 255 而不是 256 —— 我们有一个大问题:如何指示我们在字符串块中提供不同的信息。如果我们将值减少到 32 到 127 之间的字符,我们将不得不做更多的组装工作。但这允许我们使用 128 到 255 之间的任何值作为标记符号。
然而,我们可以进一步限制。我们只能使用字母数字字符来传达所需的信息。因此,我们可以保留标点符号作为分隔符。为什么我们可以在这里这样做?这是有效的,因为我们需要传输的数据相对简单:资产名称和数值,如杠杆、止盈和止损水平。这些是在 Chart Trade 中配置的,但必须传输给 EA 交易。
但除了这些非常简单的值之外,我们还需要传递一个值。尽管 MetaTrader 5 已经处理了这一点,但我们将包括操作类型,以确保通信处于控制之下。这不是必需的。
请记住:通信不一定只发生在一个终端内。通过适当的网络协议,一台计算机可以运行终端,而另一台计算机则可以管理订单和头寸。即使是一台普通的机器也可以处理交易,就像它更强大一样。
我们不会进一步讨论细节。这个想法是为了展示连接将如何实际工作。
结合两个世界的优点
我们选择的解决方案结合了固定长度和可变长度方法的优点。我们将以字母数字字符串的形式传输数据,为了清晰起见使用分隔符,并且仍然允许索引。每个字段可以根据需要占用尽可能多的字符,确保不会丢失任何内容。
请参阅前面代码片段的第 338 行。为了更清楚地说明这一点,让我们看一个实际的发送示例。

图 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 表。

为什么 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
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
MQL5自动化交易策略(第四部分):构建多层级区域恢复系统