
开发回放系统(第 73 部分):不寻常的通信(二)
概述
在上一篇文章“开发回放系统(第 72 部分):不寻常的通信(一)“中,我开始展示如何使用指标来传输某些原本无法获得的信息。尽管我在那篇文章中提供了许多解释,但实际上在我们的回放/模拟器应用程序中实现代码并不是那么简单。你可能会认为我只是在装腔作势。实现这样的功能非常简单明了。我只是为了达到效果而制造悬念。
好吧,我希望我只是围绕这个话题制造悬念。但事实是,这真的比你最初想象的要复杂得多。我有意识地努力让所有真正想学习 MQL5 的人都能理解这些文章。目前的话题,至少在写这篇文章之前,我还没有看到有人探索过。不是因为这是不可想象的,而是因为它至少相当奇特,非常罕见。这就是为什么我试图尽可能多地强调在处理没有先例的事情时应该如何进行的细节。
继续实现
在深入了解服务本身之前,这无疑是整个实现中最复杂的部分,让我们先看看控制指标代码。这是因为在上一篇文章中,我只介绍了头文件 C_Controls.mqh。由于已经涵盖了很多信息,我决定在本文中完成对控制指标的解释。那么,让我们首先回顾一下它的源代码。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. #property icon "/Images/Market Replay/Icons/Replay - Device.ico" 04. #property description "Control indicator for the Replay-Simulator service." 05. #property description "This one doesn't work without the service loaded." 06. #property version "1.73" 07. #property link "https://www.mql5.com/pt/articles/12363" 08. #property indicator_chart_window 09. #property indicator_plots 0 10. #property indicator_buffers 1 11. //+------------------------------------------------------------------+ 12. #include <Market Replay\Service Graphics\C_Controls.mqh> 13. //+------------------------------------------------------------------+ 14. C_Controls *control = NULL; 15. //+------------------------------------------------------------------+ 16. input long user00 = 0; //ID 17. //+------------------------------------------------------------------+ 18. double m_Buff[]; 19. int m_RatesTotal = 0; 20. //+------------------------------------------------------------------+ 21. int OnInit() 22. { 23. if (CheckPointer(control = new C_Controls(user00, "Market Replay Control", new C_Mouse(user00, "Indicator Mouse Study"))) == POINTER_INVALID) 24. SetUserError(C_Terminal::ERR_PointerInvalid); 25. if ((_LastError >= ERR_USER_ERROR_FIRST) || (user00 == 0)) 26. { 27. Print("Control indicator failed on initialization."); 28. return INIT_FAILED; 29. } 30. SetIndexBuffer(0, m_Buff, INDICATOR_DATA); 31. ArrayInitialize(m_Buff, EMPTY_VALUE); 32. 33. return INIT_SUCCEEDED; 34. } 35. //+------------------------------------------------------------------+ 36. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 37. { 38. (*control).SetBuffer(m_RatesTotal = rates_total, m_Buff); 39. 40. return rates_total; 41. } 42. //+------------------------------------------------------------------+ 43. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 44. { 45. (*control).DispatchMessage(id, lparam, dparam, sparam); 46. (*control).SetBuffer(m_RatesTotal, m_Buff); 47. } 48. //+------------------------------------------------------------------+ 49. void OnDeinit(const int reason) 50. { 51. switch (reason) 52. { 53. case REASON_TEMPLATE: 54. Print("Modified template. Replay // simulation system shutting down."); 55. case REASON_INITFAILED: 56. case REASON_PARAMETERS: 57. case REASON_REMOVE: 58. case REASON_CHARTCLOSE: 59. ChartClose(user00); 60. break; 61. } 62. delete control; 63. } 64. //+------------------------------------------------------------------+
控制指标的源代码
如果你将这段代码与之前的代码进行比较,你会立即注意到第 25 行有一个差异。以前,_LastError 变量的检查是针对 ERR_SUCCESS 常量进行的。然而,这样做造成了一些问题。这是因为有时指标会放置在图表上,而 _LastError 变量包含与 ERR_SUCCESS 不同的值。
我花了一些时间才理解为什么初始化偶尔会失败。这很奇怪,因为即使在显式调用 ResetLastError 库函数并调试代码以消除任何错误后,C_Controls 类的构造函数有时也会返回 _LastError,其中仍包含一个值。
更奇怪的是,错误通常与另一个交易品种或打开的图表有关。因此,显然 —— 让我把这一点说清楚,因为我没有做出任何声明 —— 不同的图表之间可能存在信息泄露。但即使只有自定义交易品种图表,仍然会返回与应用程序无关的错误。因此,我决定隔离错误。只有使用 SetUserError 函数设置的错误才会导致指标从图表中删除。
但我们真正需要密切关注的是 OnCalculate 函数。为什么这个函数对我们如此重要?OnChartEvent 函数会在几个时刻被触发,特别是由于鼠标移动。请记住,我们在应用程序中使用鼠标。但是,每当有新的报价或报价到达时,都会调用 OnCalculate。好吧,所以即使你只使用键盘或通过按键触发某些事件的界面,你也会收到 OnChartEvent 的调用。但是,使用 OnCalculate,我们可以确保事情进展得更快,或者更确切地说,我们可以保证缓冲区中的信息尽可能最新。这就是为什么我们使用 OnCalculate 来实现这一目的。
请看第 38 行,它很简单,执行与第 46 行相同的任务。不同之处在于,每当指标收到新的分时报价时,第 38 行都会更新缓冲区。即使鼠标已冻结或鼠标事件尚未触发。因此,OnChartEvent 可能只在有变化之后运行,但 OnCalculate 几乎总是被触发。
现在您了解了控制指标的作用。但请不要忘记,切换时间框架时,将执行 OnDeinit 函数,从图表中删除指标,然后立即调用 OnInit。因此,在图表上显示任何内容之前,OnCalculate 会运行,并且只有在执行 OnChartEvent 之后才会运行。我们需要缓冲区数据始终保持最新。
这是否意味着我们已经准备好开始修改 C_Replay.mqh 头文件了?好吧,从技术上讲,我们现在就可以开始了。然而,我并不完全相信所有有抱负的开发人员都真正理解这种通信方式是如何运作的。因此,亲爱的读者,我希望您能再耐心一点。如果您已经知道这是如何工作的,让我们保持耐心,以便那些还不了解的人也可以了解这里发生的事情。这就是为什么我们要进入下一个话题。
了解快速信息交换
大多数 MQL5 经验有限的初学者和开发人员可能会认为交换信息,或者更确切地说,从服务中读取指标缓冲区很简单。您所需要的只是一个句柄。是的,事实上,它确实很有效。但有一个警告,或者更准确地说,在这种特定情况下使用句柄存在缺陷。
此时,您可能会想,“使用句柄怎么会有缺陷呢?我一直都是这么做的,而且总是有效。”我不是来反驳你的逻辑或经验的,毫无意义的争论不会让我们取得任何进展。与其争论或试图通过言语证明任何事情,不如让我们进行一次真正的测试。这样,没有人可以争论,因为一旦某件事被测试和证明,就已成为不容置疑的事实。
因此,为了证明使用句柄从指标缓冲区访问数据在通过服务访问时不是一种可靠的方法 - 让我明确一点:当您更改图表时间框架时,问题变得不稳定 - 我们将进行实际实验。理解这一点很重要,问题在于切换时间框架。所以我们要做的是:我们将创建两个简单的代码示例来测试这一点。别担心,它们很容易理解。让我们从第一个开始:指标代码。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. #property version "1.00" 04. #property indicator_chart_window 05. #property indicator_plots 0 06. #property indicator_buffers 1 07. //+------------------------------------------------------------------+ 08. #include <Market Replay\Defines.mqh> 09. //+------------------------------------------------------------------+ 10. double m_Buff[]; 11. //+------------------------------------------------------------------+ 12. int OnInit() 13. { 14. SetIndexBuffer(0, m_Buff, INDICATOR_DATA); 15. ArrayInitialize(m_Buff, EMPTY_VALUE); 16. IndicatorSetString(INDICATOR_SHORTNAME, "TEST"); 17. 18. return INIT_SUCCEEDED; 19. } 20. //+------------------------------------------------------------------+ 21. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 22. { 23. m_Buff[rates_total - 1] = 1.0 * def_IndicatorTimeFrame; 24. 25. return rates_total; 26. } 27. //+------------------------------------------------------------------+
测试指标的源代码
请注意,在第 5 行,我们指示编译器我们实际上不会绘制任何数据。这可以防止它在每次新的编译尝试期间发出警告。在第 6 行,我们声明将使用一个缓冲区。然后在第 8 行,我们包含回放/模拟应用程序中使用的头文件。这很重要,因为我们的目标是了解回放/模拟系统如何实际访问数据。在第 10 行,我们声明了缓冲区。在 OnInit 函数内部,我们初始化缓冲区并定义指标的名称。请注意这一点,因为我们稍后需要这些信息。
现在,仔细查看 OnCalculate 函数的主体。在第 23 行,我们将图表时间框架值写入缓冲区。这与上一篇文章中讨论的值相同,这就是为什么理解前面的内容至关重要。好吧,我相信这里的每个人都知道指标的作用和工作原理,至少在基本层面上知道。那么,现在让我们看一下第二个代码示例。
01. //+------------------------------------------------------------------+ 02. #property service 03. #property copyright "Daniel Jose" 04. #property description "Data synchronization demo service." 05. //+------------------------------------------------------------------+ 06. input string user01 = "IBOV"; //Accompanying Symbol 07. //+------------------------------------------------------------------+ 08. void OnStart() 09. { 10. int ret, handle; 11. long id; 12. double Buff[1]; 13. 14. if ((id = ChartFirst()) > 0) do 15. { 16. if (ChartSymbol(id) == user01) break; 17. }while ((id = ChartNext(id)) > 0); 18. handle = ChartIndicatorGet(id, 0, "TEST"); 19. do 20. { 21. ret = CopyBuffer(handle, 0, 0, 1, Buff); 22. PrintFormat("CopyBuffer: [ %d ] Value: [ %f ]", ret, Buff[0]); 23. Sleep(250); 24. } while ((!_StopFlag) && (ret > 0)); 25. IndicatorRelease(handle); 26. } 27. //+------------------------------------------------------------------+
测试服务的源代码
请注意,在第 2 行,我们告知编译器该脚本是一项服务。然后,在第 6 行,我们添加一个输入参数,允许用户定义服务将观察哪个交易品种。到目前为止,没有什么不寻常的。在第 10 行和第 12 行之间,我们声明了几个变量。现在到了重要的部分:为了使服务正常运行,必须至少打开一个图表。否则,我们就会遇到问题。但如果至少打开了一个图表,第 14 行将成功获取其 ID。然后,我们遍历打开的图表,寻找与第 6 行定义的交易品种匹配的图表。一旦找到它,我们就会获取其正确的图表 ID。
有了图表 ID,我们请求 MetaTrader 5 为我们提供指标的句柄。请注意这里使用的指标名称,它必须与指标源代码第 16 行定义的相匹配。从那里,我们进入一个循环。由于我们现在有一个有效的句柄,我们可以用它来读取指标缓冲区。这正是我们在第 21 行所做的。在第 22 行,我们将缓冲区数据和读取操作的结果打印到终端。这个循环一直运行到第 24 行的条件不再满足为止,换句话说,直到服务停止或缓冲区读取失败为止。一旦发生这种情况,第 25 行就会释放句柄。
因此,我们都同意该代码确实读取了指标缓冲区。并且由于我们在上一篇文章中讨论过的测试,它打印的值将会与预期的值相匹配。那么让我们在 MetaTrader 5 中运行它。为了您的方便,您可以在下面的视频中观看结果:
等一下,刚才发生了什么事?你可能认为我在骗你。没关系,你可以自由地相信任何你想相信的东西。事实上,您不需要相信我的话。运行您自己的测试。但一定要保持核心思想不变:使用句柄从服务中读取指标缓冲区。您在视频中看到的事情将会发生。 但为什么呢?
关键在于句柄。一旦 MetaTrader 5 改变图表时间框架,句柄 ID 就会改变。但是,您的代码仍然引用旧的句柄。但该句柄不再有效,因为 MetaTrader 5 已在内部重新创建了指标,并且新的缓冲区现在链接到了不同的句柄。当您使用 EA 交易或其他指标时,它们会在时间框架切换后自动重新附加到图表。这将导致重新发出所有函数调用并更新句柄。这是因为 EA 交易或指标已重新加载到图表上。此时,句柄已更新,从而可以正确读取缓冲区。
但服务是在图表环境之外运行的。因此,句柄并没有更新。这就是从缓冲区读取的值保持不变的原因。您可能仍然认为这并不适用于您的情况,不知何故,您的服务以不同的方式运行。好的,让我们来检验一下这个假设。以下是新版本的服务脚本:这次,我们只修改服务代码。代码如下所示:
01. //+------------------------------------------------------------------+ 02. #property service 03. #property copyright "Daniel Jose" 04. #property description "Data synchronization demo service." 05. //+------------------------------------------------------------------+ 06. input string user01 = "IBOV"; //Accompanying Symbol 07. //+------------------------------------------------------------------+ 08. void OnStart() 09. { 10. int ret; 11. long id; 12. double Buff[1]; 13. 14. if ((id = ChartFirst()) > 0) do 15. { 16. if (ChartSymbol(id) == user01) break; 17. }while ((id = ChartNext(id)) > 0); 18. do 19. { 20. ret = CopyBuffer(ChartIndicatorGet(id, 0, "TEST"), 0, 0, 1, Buff); 21. PrintFormat("CopyBuffer: [ %d ] Value: [ %f ]", ret, Buff[0]); 22. Sleep(250); 23. } while ((!_StopFlag) && (ret > 0)); 24. } 25. //+------------------------------------------------------------------+
测试服务的源代码
请注意,代码经过了细微的修改 —— 乍一看几乎难以察觉的变化。但关键点在于:第 20 行是发生关键更改的地方。现在,句柄不再是静态的 - 它已经变成动态的。这意味着即使服务不知道图表的当前时间框架,或者对其进行了哪些修改,它仍然可以找到并与指标的正确实例进行交互,并正确读取其缓冲区。为了使事情变得简单,您可以在下面的视频中看到这种行为:
现在你可能认为我在让你搞砸,这怎么可能?亲爱的读者,别紧张。正如我之前提到的,在首次介绍这个概念之前,我不想向您展示 C_Replay.mqh 文件中的变化。
看看那些看似简单但完全有效和合理的东西是如何彻底改变你的代码行为的?关键在于:您必须始终在尽可能简单的环境中进行测试。我经常看到人们在没有坚实、完善的基础的情况下试图编写过于复杂的代码。最后,他们放弃了,因为他们无法找出隐藏在哪里的微妙问题。
但事实是,你必须始终权衡计算成本和实现复杂性。如果代码太慢,那么拥有可用的代码是没有用的。同样,如果代码运行速度很快但根本不起作用,那也是没有意义的。
通过使句柄动态化,就像我们在服务的第二个版本中所做的那样,我们引入了轻微的性能成本。因为现在,每次执行时都会进行句柄查找。第一个版本已经存储了句柄,因此简单地引用预加载的变量要快得多。
这是您必须始终评估的权衡。这就是为什么我认为你应该在最少、简单的代码中构建和测试东西。但是,该逻辑实际上如何在控制回放/模拟器服务的 C_Replay.mqh 头文件中集成和实现?为了回答这个问题,让我们进入新的一节。
修改 C_Replay.mqh 文件
在这里,我面临着一个小困境:我应该展示已经修改的代码,还是一步一步地引导你完成修改过程?这种决定往往会使文章的速度比我想象的要慢。编写和测试代码很快,但解释发生了什么变化以及原因需要时间。
不过,亲爱的读者,我的目标是让您真正理解并学会如何做到这一点。我也经历过这个,多年的编程往往会留下痕迹。也就是说,我真的相信,逐步转型是最好的前进道路。即使这意味着您必须做一些清理工作来删除已弃用的部分。那么,我们现在看一下整个 C_Replay.mqh 头文件。
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. //+------------------------------------------------------------------+ 004. #include "C_ConfigService.mqh" 005. #include "C_Controls.mqh" 006. //+------------------------------------------------------------------+ 007. #define def_IndicatorControl "Indicators\\Market Replay.ex5" 008. #resource "\\" + def_IndicatorControl 009. //+------------------------------------------------------------------+ 010. #define def_CheckLoopService ((!_StopFlag) && (ChartSymbol(m_Infos.IdReplay) != "")) 011. //+------------------------------------------------------------------+ 012. #define def_ShortNameIndControl "Market Replay Control" 013. #define def_MaxSlider (def_MaxPosSlider + 1) 014. //+------------------------------------------------------------------+ 015. class C_Replay : public C_ConfigService 016. { 017. private : 018. struct st00 019. { 020. C_Controls::eObjectControl Mode; 021. uCast_Double Memory; 022. ushort Position; 023. int Handle; 024. }m_IndControl; 025. struct st01 026. { 027. long IdReplay; 028. int CountReplay; 029. double PointsPerTick; 030. MqlTick tick[1]; 031. MqlRates Rate[1]; 032. }m_Infos; 033. stInfoTicks m_MemoryData; 034. //+------------------------------------------------------------------+ 035. inline bool MsgError(string sz0) { Print(sz0); return false; } 036. //+------------------------------------------------------------------+ 037. inline void SendEventCustom(const ENUM_BOOK_TYPE Arg1 = BOOK_TYPE_BUY_MARKET) 038. { 039. MqlBookInfo book[1]; 040. 041. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position; 042. m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (ushort)m_IndControl.Mode; 043. m_IndControl.Memory._8b[7] = 'D'; 044. m_IndControl.Memory._8b[6] = 'M'; 045. EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, 0, m_IndControl.Memory.dValue, ""); 046. book[0].price = 1.0; 047. book[0].volume = 1; 048. book[0].type = Arg1; 049. CustomBookAdd(def_SymbolReplay, book, 1); 050. } 051. //+------------------------------------------------------------------+ 052. inline void CheckIndicatorControl(void) 053. { 054. static uchar memTimeFrame = 0; 055. static C_Controls::eObjectControl memMode = m_IndControl.Mode; 056. double Buff[]; 057. 058. if (CopyBuffer(ChartIndicatorGet(m_Infos.IdReplay, 0, "Market Replay Control"), 0, 0, 1, Buff) < 0) ChartClose(m_Infos.IdReplay); 059. m_IndControl.Memory.dValue = Buff[0]; 060. if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] >= m_IndControl.Position) 061. { 062. if ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay) 063. m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition]; 064. if (m_IndControl.Memory._8b[def_IndexTimeFrame] == memTimeFrame) 065. { 066. memMode = m_IndControl.Mode; 067. return; 068. } 069. memTimeFrame = m_IndControl.Memory._8b[def_IndexTimeFrame]; 070. m_IndControl.Mode = memMode; 071. } 072. SendEventCustom(m_IndControl.Mode != C_Controls::ePlay ? BOOK_TYPE_BUY_MARKET : BOOK_TYPE_BUY); 073. } 074. //+------------------------------------------------------------------+ 075. inline void UpdateIndicatorControl(void) 076. { 077. double Buff[]; 078. 079. if (m_IndControl.Handle == INVALID_HANDLE) return; 080. if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position) 081. { 082. if (CopyBuffer(m_IndControl.Handle, 0, 0, 1, Buff) == 1) 083. m_IndControl.Memory.dValue = Buff[0]; 084. if ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay) 085. m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition]; 086. { 087. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position; 088. m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (ushort)m_IndControl.Mode; 089. m_IndControl.Memory._8b[7] = 'D'; 090. m_IndControl.Memory._8b[6] = 'M'; 091. EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, 0, m_IndControl.Memory.dValue, ""); 092. } 093. } 094. //+------------------------------------------------------------------+ 095. void SweepAndCloseChart(void) 096. { 097. long id; 098. 099. if ((id = ChartFirst()) > 0) do 100. { 101. if (ChartSymbol(id) == def_SymbolReplay) 102. ChartClose(id); 103. }while ((id = ChartNext(id)) > 0); 104. } 105. //+------------------------------------------------------------------+ 106. inline int RateUpdate(bool bCheck) 107. { 108. static int st_Spread = 0; 109. 110. st_Spread = (bCheck ? (int)macroGetTime(m_MemoryData.Info[m_Infos.CountReplay].time) : st_Spread + 1); 111. m_Infos.Rate[0].spread = (int)(def_MaskTimeService | st_Spread); 112. CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate); 113. 114. return 0; 115. } 116. //+------------------------------------------------------------------+ 117. inline void CreateBarInReplay(bool bViewTick) 118. { 119. bool bNew; 120. double dSpread; 121. int iRand = rand(); 122. 123. if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[0], bNew)) 124. { 125. m_Infos.tick[0] = m_MemoryData.Info[m_Infos.CountReplay]; 126. if (m_MemoryData.ModePlot == PRICE_EXCHANGE) 127. { 128. dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 ); 129. if (m_Infos.tick[0].last > m_Infos.tick[0].ask) 130. { 131. m_Infos.tick[0].ask = m_Infos.tick[0].last; 132. m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread; 133. }else if (m_Infos.tick[0].last < m_Infos.tick[0].bid) 134. { 135. m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread; 136. m_Infos.tick[0].bid = m_Infos.tick[0].last; 137. } 138. } 139. if (bViewTick) 140. CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 141. RateUpdate(true); 142. } 143. m_Infos.CountReplay++; 144. } 145. //+------------------------------------------------------------------+ 146. void AdjustViewDetails(void) 147. { 148. MqlRates rate[1]; 149. 150. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_ASK_LINE, GetInfoTicks().ModePlot == PRICE_FOREX); 151. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_BID_LINE, GetInfoTicks().ModePlot == PRICE_FOREX); 152. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_LAST_LINE, GetInfoTicks().ModePlot == PRICE_EXCHANGE); 153. m_Infos.PointsPerTick = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE); 154. CopyRates(def_SymbolReplay, PERIOD_M1, 0, 1, rate); 155. if ((m_Infos.CountReplay == 0) && (GetInfoTicks().ModePlot == PRICE_EXCHANGE)) 156. for (; GetInfoTicks().Info[m_Infos.CountReplay].volume_real == 0; m_Infos.CountReplay++); 157. if (rate[0].close > 0) 158. { 159. if (GetInfoTicks().ModePlot == PRICE_EXCHANGE) 160. m_Infos.tick[0].last = rate[0].close; 161. else 162. { 163. m_Infos.tick[0].bid = rate[0].close; 164. m_Infos.tick[0].ask = rate[0].close + (rate[0].spread * m_Infos.PointsPerTick); 165. } 166. m_Infos.tick[0].time = rate[0].time; 167. m_Infos.tick[0].time_msc = rate[0].time * 1000; 168. }else 169. m_Infos.tick[0] = GetInfoTicks().Info[m_Infos.CountReplay]; 170. CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 171. } 172. //+------------------------------------------------------------------+ 173. void AdjustPositionToReplay(void) 174. { 175. int nPos, nCount; 176. 177. if (m_IndControl.Position == (int)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks)) return; 178. nPos = (int)((m_MemoryData.nTicks * m_IndControl.Position) / def_MaxSlider); 179. for (nCount = 0; m_MemoryData.Rate[nCount].spread < nPos; m_Infos.CountReplay = m_MemoryData.Rate[nCount++].spread); 180. if (nCount > 0) CustomRatesUpdate(def_SymbolReplay, m_MemoryData.Rate, nCount - 1); 181. while ((nPos > m_Infos.CountReplay) && def_CheckLoopService) 182. CreateBarInReplay(false); 183. } 184. //+------------------------------------------------------------------+ 185. void WaitIndicatorLoad(const string szArg, const bool ViewCtrl = true) 186. { 187. Print("Waiting for ", szArg); 188. while ((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, 0, szArg) == INVALID_HANDLE)) 189. { 190. if (ViewCtrl) CheckIndicatorControl(); 191. Sleep(100); 192. } 193. } 194. //+------------------------------------------------------------------+ 195. public : 196. //+------------------------------------------------------------------+ 197. C_Replay() 198. :C_ConfigService() 199. { 200. Print("************** Market Replay Service **************"); 201. srand(GetTickCount()); 202. SymbolSelect(def_SymbolReplay, false); 203. CustomSymbolDelete(def_SymbolReplay); 204. CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay)); 205. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0); 206. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, 0); 207. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, 0); 208. CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation"); 209. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, 8); 210. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_TICKS_BOOKDEPTH, 1); 211. SymbolSelect(def_SymbolReplay, true); 212. m_Infos.CountReplay = 0; 213. m_IndControl.Handle = INVALID_HANDLE; 214. m_IndControl.Mode = C_Controls::ePause; 215. m_IndControl.Position = 0; 216. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = C_Controls::eTriState; 217. } 218. //+------------------------------------------------------------------+ 219. ~C_Replay() 220. { 221. SweepAndCloseChart(); 222. IndicatorRelease(m_IndControl.Handle); 223. SymbolSelect(def_SymbolReplay, false); 224. CustomSymbolDelete(def_SymbolReplay); 225. Print("Finished replay service..."); 226. } 227. //+------------------------------------------------------------------+ 228. bool OpenChartReplay(const ENUM_TIMEFRAMES arg1, const string szNameTemplate) 229. { 230. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == 0) 231. return MsgError("Asset configuration is not complete, it remains to declare the size of the ticket."); 232. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == 0) 233. return MsgError("Asset configuration is not complete, need to declare the ticket value."); 234. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == 0) 235. return MsgError("Asset configuration not complete, need to declare the minimum volume."); 236. SweepAndCloseChart(); 237. m_Infos.IdReplay = ChartOpen(def_SymbolReplay, arg1); 238. if (!ChartApplyTemplate(m_Infos.IdReplay, szNameTemplate + ".tpl")) 239. Print("Failed apply template: ", szNameTemplate, ".tpl Using template default.tpl"); 240. else 241. Print("Apply template: ", szNameTemplate, ".tpl"); 242. 243. return true; 244. } 245. //+------------------------------------------------------------------+ 246. bool InitBaseControl(const ushort wait = 1000) 247. { 248. int handle; 249. 250. Sleep(wait); 251. AdjustViewDetails(); 252. Print("Loading Control Indicator..."); 253. if ((handle = iCustom(ChartSymbol(m_Infos.IdReplay), ChartPeriod(m_Infos.IdReplay), "::" + def_IndicatorControl, m_Infos.IdReplay)) == INVALID_HANDLE) return false; 254. ChartIndicatorAdd(m_Infos.IdReplay, 0, handle); 255. IndicatorRelease(handle); 256. WaitIndicatorLoad("Market Replay Control", false); 257. SendEventCustom(); 258. WaitIndicatorLoad("Indicator Mouse Study"); 259. UpdateIndicatorControl(); 260. SendEventCustom(); 261. 262. return def_CheckLoopService; 263. } 264. //+------------------------------------------------------------------+ 265. bool LoopEventOnTime(void) 266. { 267. int iPos, iCycles; 268. MqlBookInfo book[1]; 269. ENUM_BOOK_TYPE typeMsg, memBook; 270. 271. book[0].price = 1.0; 272. book[0].volume = 1; 273. book[0].type = BOOK_TYPE_BUY_MARKET; 274. CustomBookAdd(def_SymbolReplay, book, 1); 275. SendEventCustom(); 276. while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay)) 277. { 278. UpdateIndicatorControl(); 279. CheckIndicatorControl(); 280. Sleep(200); 281. } 282. m_MemoryData = GetInfoTicks(); 283. AdjustPositionToReplay(); 284. iPos = iCycles = 0; 285. SendEventCustom(memBook = BOOK_TYPE_BUY); 286. book[0].type = BOOK_TYPE_BUY; 287. CustomBookAdd(def_SymbolReplay, book, 1); 288. while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService)) 289. { 290. if (m_IndControl.Mode == C_Controls::ePause) return true; 291. iPos += (int)(m_Infos.CountReplay < (m_MemoryData.nTicks - 1) ? m_MemoryData.Info[m_Infos.CountReplay + 1].time_msc - m_MemoryData.Info[m_Infos.CountReplay].time_msc : 0); 292. if ((typeMsg = (iPos >= 60000 ? BOOK_TYPE_BUY_MARKET : BOOK_TYPE_BUY)) != book[0].type) 293. if ((typeMsg = (iPos >= 60000 ? BOOK_TYPE_BUY_MARKET : BOOK_TYPE_BUY)) != memBook) 294. SendEventCustom(memBook = typeMsg); 295. { 296. book[0].type = typeMsg; 297. CustomBookAdd(def_SymbolReplay, book, 1); 298. } 299. CreateBarInReplay(true); 300. while ((iPos > 200) && (def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePause)) 301. { 302. Sleep(195); 303. iPos -= 200; 304. m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks); 305. UpdateIndicatorControl(); 306. CheckIndicatorControl(); 307. iCycles = (iCycles == 4 ? RateUpdate(false) : iCycles + 1); 308. } 309. } 310. 311. return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService)); 312. } 313. }; 314. //+------------------------------------------------------------------+ 315. #undef def_SymbolReplay 316. #undef def_CheckLoopService 317. #undef def_MaxSlider 318. //+------------------------------------------------------------------+
C_Replay.mqh 文件的源代码
请注意,必须从代码中删除每个有删除线的代码行。但让我们仔细看看为什么删除了这么多行。
第一个重点在第 23 行。正如上一节所演示的,我们不应该使用静态句柄。因此,“handle” 变量已经过时了。因此,引用它的代码的所有部分都被删除了。此外,整个过程也被删除了。它就是 UpdateIndicatorControl,之前位于第 75 行和第 93 行之间。因此,所有对它的引用也已被删除。这意味着我们现在需要找到一种方法来替换 UpdateIndicatorControl 之前涵盖的功能。
但我们马上就会谈到这一点。在我们深入探讨之前,让我们快速浏览一下两个具体的函数。第一个是 InitBaseControl,从第 243 行开始。您会注意到此函数的一些细小但有意义的变化。这些调整既是为了提升用户体验,也是为了规范指标的初始化。让我们看看发生了什么。
在第 253 行和第 255 行之间,我们尝试加载控制指标。这里有一个关键的细节:指标加载不会立即发生,执行时有轻微的延迟。因此,在执行第 257 行之前,我们需要确保控制指标已正确加载到图表上。该检查是在第 253 行执行的。注意第 256 行和第 258 行中的指标名称 - 这些是我们期望加载的特定指标。这些调用在第 185 行触发。从这个地方开始,事情开始变得有趣,所以要密切关注。
第 188 行的循环将等待指定的指标加载到图表上。当第 256 行要求我们等待控制指标加载时,第 187 行的检查会阻止我们进行提前检查。然而,当第 255 行要求等待鼠标指标加载时,第 190 行会检查控制指标的缓冲区。为什么会发生这种情况?因为用户可能会改变图表的时间框架。为了更好地理解这一点,让我们跳到第 52 行。
在第 52 行,我们找到负责读取控制指标缓冲区的过程。现在是最有趣的部分。第 54 行和第 55 行声明了两个静态变量。但此刻,我们关心的是第 54 行。请注意,它的初始化值为 0。现在,如果尝试从第 58 行的缓冲区读取返回小于零的值,则意味着控制指标已从图表中删除。由于用户无法手动重新连接它,我们需要它的存在,因此图表已关闭。这将会终止该应用程序。因此,我们不能在控制指标仍处于加载阶段时对其进行检查。
在第 60 行,我们检查控制指标的值是否大于服务中正在分析的位置。如果是这样,则意味着我们必须在用户点击播放后快进执行。然而,真正关键的条件是第 64 行中的条件。这种检查是时间框架锁定的致命弱点 —— 上一节讨论的机制。如果用户没有更改图表时间框架,则此条件将返回 true,并且不会采取进一步的操作。
如果检查返回 false,则第 69 行存储新的时间框架,第 70 行获取指标的最后已知状态。这是必要的,因为在第 72 行将调用另一个过程。让我们跳到第 37 行,事情从这里开始变得非常有趣。这正是我们指示 MetaTrader 5 在图表上触发事件的地方。将其与缓冲区验证分开的原因是,有时我们只是想触发图表事件,而在其他情况下,我们需要确认控制指标的值是否发生了任何有意义的变化。
请注意,SendEventCustom 的所有逻辑在之前的代码版本中已经存在。因此,无需再次解释。代码简单明了,不言自明。我想我已经解释了核心的结构性变化。不过,我们仍然需要讨论对 LoopEventOnTime 函数所做的修改。尽管这些变化并不显著或结构性,但它们突出了 UpdateIndicatorControl 被拆分为两个新过程的原因。让我们快速回顾一下。
在第 275 行,我们要求 MetaTrader 5 向我们发送自定义事件。这可确保在开始模拟或回放之前正确配置鼠标和控制指标。虽然在第一次执行期间可以跳过第 275 行,但必须在第一个“播放”命令之后运行它以确保数据重新验证。一旦系统进入暂停模式,就会重新执行此行。
在第 279 行,我们不需要触发任何事件。我们只是观察控制指标。一旦用户按下播放,第 276 行的循环结束,模拟开始。
在第 285 行,我们再次请求自定义事件。这次我们需要让鼠标指标在柱上显示剩余时间。
另一个细微的变化发生在第 293 行和 294 行。这是一个更新鼠标指标状态的简单逻辑块。此状态使我们能够检测资产是否已进入或退出竞价模式。
最终的变化显示在第 306 行,我们在此检查时间框架是否已改变。如果检测到变化,则重新加载指标,如本节开头所述。
至此,所有必要的代码调整均已完成。下面的视频演示了当图表时间框架发生变化时系统现在如何运行。
结论
在最后两篇文章中,我展示了实验并将编程语言推向极限的重要性。即使一些读者可能觉得几乎没有获得直接的知识,事实并非如此。我相信,许多人认为我在这里解释的内容要么是不可能的,要么至少是不实用的。然而,在修改主代码之前,我解释了首先形成一个假设,然后构建一个简单的原型来测试它的重要性。
但最重要的教训是:第一次尝试就不要放弃。如果有些东西不起作用,调整你的方法,但始终专注于检验你的假设。这正是我所做的。我的目标是将数据推入指标缓冲区,以清晰可靠地检测时间范围的变化。直接在图表上做这件事很容易。所以问题变成了:服务也能检测到这种变化吗?第一次实现尝试失败。但是,我们仍然能够读取时间框架值。问题是,该值表示指标首次附加到图表时的状态。
这就是这个概念真正出现的地方。如果我只是放弃了,而不是重新思考在每次调用期间动态检索句柄的架构,我就会错过从指标访问实时更新数据的机会。通过重新思考设计,我们开辟了新的可能性,证明我们可以远远超越许多人认为的可能性。
真实系统就是这样构建的:你从一个假设开始,对其进行测试,即使最初的方法只部分有效,你也会在相同的核心思想的指导下进行迭代。
在附件中,您将找到使用回放/模拟器所需的可执行文件。在下一篇文章中,我们将开始探索需要集成到回放/模拟器应用程序中的其他功能。到时候见。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/12363
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。


