
开发回放系统(第 66 部分):玩转服务(七)
概述
在本文中,我们将从做一些不同的事情开始。但首先,让我们简单回顾一下上一篇文章 ”开发回放系统(第 65 部分):玩转服务(六) “ 中讨论的内容。 在那里,我们解决了与鼠标指针提供的百分比指示器相关的问题。以前,在使用该指标回放或模拟期间,会显示不正确的值。此外,我们还实现了一个快进系统,使我们能够跳转到特定点,而无需等待几分钟甚至几小时。不过,在继续阅读本文之前,我希望您先观看下面的视频。这将帮助您更好地理解我将在本文开头解释的要点。
要观看的内容
我认为视频本身就说明了问题。尽管如此,我还是想解释一下正在发生的事情。此回放/模拟应用程序在很大程度上依赖于使用定制资产。这一点很清楚,我相信每个人都同意这一点。然而,有些失败不是由我们正在进行的编程引起的。这些失败有不同的根源。有些是不寻常的,有些则几乎是随机发生的。当问题的原因可以确定时,我们就能够解决它。然而,当问题随机发生时,情况会发生变化,我们必须学会解决它。
在撰写本文时,MetaTrader 5 平台的最新版本正是视频中显示的版本。我不确定问题在哪里,但它显然存在,你可能已经遇到过,或者将来最终会遇到。在视频中展示的问题得到明确解决之前,您需要适应。换句话说,在加载回放或模拟应用程序之前,请按照您在整个会话中使用它的方式进行配置。这样,您将避免需要更改图表时间范围,而这正是 MetaTrader 5 奇怪地与自定义资产中的分时报价或柱形失去联系的时候。也许当你阅读这篇文章时,视频中显示的问题已经得到解决。如果是这样,那就太好了。如果没有,请遵循我刚才提供的建议,以避免在使用回放/模拟应用程序时出现不愉快的体验。
话虽如此,我们现在可以继续本文接下来的工作。在上一篇文章中,还有一个悬而未决的问题。由于它更直接,我们将从它开始。
实现到下一根柱形的剩余时间
许多金融市场交易者重视并经常监控的一个特征是距离下一个柱形开始还剩余的时间。起初,这可能看起来微不足道,或者像是浪费时间。然而,在某些操作模型中,这些信息至关重要,因为交易员通常在下一个柱形开始前几秒钟下订单。
在某些时间范围内,或者仅仅通过交易者的经验,他们会对剩余的时间有一种直觉。然而,刚刚起步的交易者还不具备这种技能。因此,有必要为他们提供某种方式来了解这件事。在实时市场中,这很容易做到,因为我们总是连接到交易服务器(在模拟或真实账户上)。此外,由于时间不会停滞不前,因此操作不断发展的事实使实现这一指示变得更加容易。但正是最后一个方面使得回放/模拟的应用变得有点复杂。用户可以无限期地暂停应用程序,甚至可以到达柱形在开始或几乎在结束的位置,这使得事情变得更加复杂。它变得如此具有挑战性,以至于我们需要一个真正的杂耍动作来避免仅仅为了显示柱形的剩余时间而创建弗兰肯斯坦解决方案。
在继续之前,让我们回顾一下我们已经拥有的东西。在上一篇文章中,我们修改了鼠标指标代码,以便现在使用不同版本的 OnCalculate 事件处理程序。事实上,在这种情况下,这种更改被证明是非常有用。原因是它为我们提供了一个存储时间值的数组。只需稍作修改,我们将得到如下所示的代码片段:44. //+------------------------------------------------------------------+ 45. int OnCalculate(const int rates_total, const int prev_calculated, const datetime& time[], const double& open[], 46. const double& high[], const double& low[], const double& close[], const long& tick_volume[], 47. const long& volume[], const int& spread[]) 48. { 49. Print(TimeToString(time[rates_total - 1], TIME_DATE | TIME_SECONDS)); // To Testing ... 50. GL_PriceClose = close[rates_total - 1]; 51. m_posBuff = rates_total; 52. (*Study).Update(m_Status); 53. 54. return rates_total; 55. } 56. //+------------------------------------------------------------------+
Mouse Study.mq5 的源代码片段
请注意,已添加一行新代码。第 49 行将允许我们查看时间数组中找到的最新值。现在,请密切关注一个重要的细节。该数组中的值已被调整,以确保正确构建一分钟的柱形,即在一分钟窗口内。
如果您使用鼠标指标中的新代码运行回放/模拟应用程序,您将在工具箱中看到与下面的动画中显示的非常相似的信息:
很好,您可以观察到,每次调用 OnCalculate 函数时,鼠标指标都会打印信息,但以秒为单位的值并没有改变。因此,仔细思考一下,你可能会想:如果我们在这些信息中插入一个以秒为单位的值呢?那么,我们能计算出下一个柱形出现之前还有多少时间吗?如果你按照这些思路思考,这意味着你已经理解了我们需要做什么来创建这个功能。那么,让我们来测试一下这个想法。为此,我们需要在头文件 C_Replay.mqh 中做一个小的更改。相关代码片段如下所示。
69. //+------------------------------------------------------------------+ 70. inline void CreateBarInReplay(bool bViewTick) 71. { 72. bool bNew; 73. double dSpread; 74. int iRand = rand(); 75. 76. if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[0], bNew)) 77. { 78. m_Infos.tick[0] = m_MemoryData.Info[m_Infos.CountReplay]; 79. if (m_MemoryData.ModePlot == PRICE_EXCHANGE) 80. { 81. dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 ); 82. if (m_Infos.tick[0].last > m_Infos.tick[0].ask) 83. { 84. m_Infos.tick[0].ask = m_Infos.tick[0].last; 85. m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread; 86. }else if (m_Infos.tick[0].last < m_Infos.tick[0].bid) 87. { 88. m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread; 89. m_Infos.tick[0].bid = m_Infos.tick[0].last; 90. } 91. } 92. if (bViewTick) CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 93. m_Infos.Rate[0].time = m_MemoryData.Info[m_Infos.CountReplay].time; //< To Testing... 94. CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate); 95. } 96. m_Infos.CountReplay++; 97. } 98. //+------------------------------------------------------------------+
C_Replay.mqh 源代码片段
看看它的实现是多么有趣和简单。我们所要做的就是将第 93 行添加到代码片段中,以便能够包含以秒为单位的值。再简单不过了,但这真的有效吗?记住以下事实:CustomRatesUpdate 函数文档规定,应为其创建汇率的时间值必须在一分钟窗口内。然后你可能会想,这不是问题,因为我们不会改变窗口;我们只是添加秒值,很可能函数会忽略它,但会将其传递给鼠标指标,以便在 OnCalculate 函数中注意到它。在某些方面,我同意你的理论,但在我们真正检验这个假设之前,它仍然只是一个理论假设。所以,我们编译了代码,结果就是你在下面的视频中看到的:
快速演示
但那是什么?到底发生了什么事?说实话,我没想到会发生这种事。我以为它会起作用。事实上,这个想法并不完全错误:你可能已经注意到鼠标指标给了我们想要的东西。然而,图表内容...好吧,最好找到另一种方法来解决这个问题。但你应该注意到,我们仍然可以做一些有趣的事情。事实上,我过去也用过同样的想法来创建一个指标。您可以在文章中看到这个指标,至少是它的开放版本,在文章从头开始开发交易 EA 交易(第 13 部分):时间与交易(二) 中。这个版本现在已经过时了,但对理解这里发生的事情会有所帮助。在有人问之前:不,我不会出售、赠送、出借或展示该指标的当前版本。它仅供个人使用。
好的,我相信你现在明白了事情是如何运作的,以及为什么看似简单的事情实际上需要我们进行更复杂的操作来实现预期的结果。
你可能会认为我们需要想出一种新的做事方式。但是,使用终端全局变量的版本中已经存在解决方案。但是,我们将不再使用相同的解决方案。我们需要发挥创造力,真正理解程序如何在它们之间传递信息。我们已经通过使用控制指标来管理服务来实现这一点,并且服务会向指标报告我们的位置。这里需要应用相同的方法,但有一个重要的细节:我们不能每秒、或者更糟糕的是,每一个分时,触发自定义事件来通知鼠标指标我们当前的时间位置。如果这样做,我们最终会降低应用程序的性能,迫使我们进一步减少每分钟的最大分时报价数。这是我无论如何都想避免的事情。然而,我们确实需要在分时报价中的信息和与当前柱形时间相关的信息之间进行一些同步。这是主要关注的问题。因此,让我们撤消代码片段中看到的更改,并专注于尝试创建一个真正解决问题的不同解决方案。但这将是下一节的主题。
思考另一种显示剩余时间的方式
与我们连接到实时交易服务器时发生的情况不同,在回放/模拟器环境中处理这个问题要复杂得多,尤其是考虑到我想要如何处理它。我不打算使用终端全局变量来传输信息。事实上,当我们连接到实时服务器时,所有时钟都需要保持同步,至少要保持到秒。您可能不完全了解这对所有事情的影响有多大,但要根据剩余秒数进行任何检查,您真正需要的是知道距离给定时间还有多少秒。当连接到实时服务器时,这非常简单,因为我们可以使用系统的内部时钟来执行此计算并获得剩余时间。
然而,当使用回放/模拟环境时,一切都变得更加复杂。复杂性增加的原因恰恰在于我们所做的事情的性质:回放或模拟。但是,仅仅运行回放或模拟怎么会使得知道到下一柱形的剩余时间变得更加困难呢?这没有道理。事实上,如果你从表面上看这个问题,那么回放或模拟数据使得确定新柱开始之前还剩多少时间变得更加困难,这真的不合理。但整个问题都围绕着一个关键点:能够正确读懂时钟。
让我们考虑以下情况:假设图表上绘制的第一个一分钟柱形实际上需要整整一分钟才能绘制出来。好的,这是理解该问题的首要条件:该柱形完全按一分钟绘制。现在,如果您启动应用程序,以便绘图恰好从零秒开始,那么每个系统分钟都会带来一个新柱。完美,问题解决。这是因为同步是完美的。因此,我们需要做的就是检查系统时钟,然后我们就能确切地知道距离下一个柱形开始还剩多少时间。
然而,您的第一个柱形的长度很少(如果有的话)正好是一分钟。另一个问题是,您几乎无法在系统时钟的精确零秒标记处启动应用程序。但在这种情况下,我们可能会进行一些小的调整,或者进行一些测试,强制回放/模拟器以与系统时钟同步的方式启动。这个解决方案是可行且合理的。不过,如果您发出信号表示应用程序已准备就绪并且可以开始绘制柱形,那么您将需要等待一段时间才能真正发生同步。等待时间不会特别长:最坏的情况下最多 59 秒。对于个人使用的应用程序来说,这是可以接受的。但即使在个人使用环境中,你迟早也会厌倦每次在应用程序上点击播放时等待长达 59 秒。
尽管强制回放/模拟器与系统时钟保持同步简化了我们跟踪柱形剩余时间的工作,但它会让用户体验有些恼人。最糟糕的情况是当您暂停模拟或在中间回放时。在这种情况下,您需要再次等待最多 59 秒才能让应用程序恢复绘制图表,因为它必须等待系统时钟再次对齐。所以,实际上,这似乎不是最好的解决方案。
然而,我们或许能够找到一个中间地带。让我们进行一些细微的调整以与系统时钟保持同步,同时避免等待系统时钟长达 59 秒来“解锁”柱形绘制。这样,我们仍然可以确定柱形剩余的时间。现在事情开始变得更加有趣了。
亲爱的读者,我提供这个详细的解释不是为了让事情变得过于复杂或让你感到困惑,而是为了表明在我们坐下来写代码之前,我们需要先思考。我们必须分析可能性,权衡实施难度。许多人认为编程就是把一堆代码放到一个文件中并称之为完成。但事实上,编码只是这个过程的一小部分。最重要的部分在于解决方案的规划和分析。这就是我们迄今为止所做的。通过这个思考过程,我们可以对需要做什么形成一个坚实的想法。然而,一些改变仍然是必要的。我们需要稍微改进一下我们的组织。但是,为了真正了解我们要做什么,让我们进入一个新的部分。
实现显示剩余时间的基础版本
在我看来,我们在本节中要做的事情相对简单,但也相当大胆。我们将实现一种方法,将当前柱形时间信息快速传递到鼠标指标。我们将首先对鼠标指标代码进行一些更改。这些更改将涉及添加更多代码,如下面的代码片段所示:
12. //+------------------------------------------------------------------+ 13. double GL_PriceClose; 14. datetime GL_TimeAdjust; 15. //+------------------------------------------------------------------+ 16. #include <Market Replay\Auxiliar\Study\C_Study.mqh> 17. //+------------------------------------------------------------------+ 18. C_Study *Study = NULL; 19. //+------------------------------------------------------------------+ 20. input color user02 = clrBlack; //Price Line 21. input color user03 = clrPaleGreen; //Positive Study 22. input color user04 = clrLightCoral; //Negative Study 23. //+------------------------------------------------------------------+ 24. C_Study::eStatusMarket m_Status; 25. int m_posBuff = 0; 26. double m_Buff[]; 27. //+------------------------------------------------------------------+ 28. int OnInit() 29. { 30. ResetLastError(); 31. Study = new C_Study(0, "Indicator Mouse Study", user02, user03, user04); 32. if (_LastError != ERR_SUCCESS) return INIT_FAILED; 33. if ((*Study).GetInfoTerminal().szSymbol != def_SymbolReplay) 34. { 35. MarketBookAdd((*Study).GetInfoTerminal().szSymbol); 36. OnBookEvent((*Study).GetInfoTerminal().szSymbol); 37. m_Status = C_Study::eCloseMarket; 38. }else 39. m_Status = C_Study::eInReplay; 40. SetIndexBuffer(0, m_Buff, INDICATOR_DATA); 41. ArrayInitialize(m_Buff, EMPTY_VALUE); 42. 43. return INIT_SUCCEEDED; 44. } 45. //+------------------------------------------------------------------+ 46. int OnCalculate(const int rates_total, const int prev_calculated, const datetime& time[], const double& open[], 47. const double& high[], const double& low[], const double& close[], const long& tick_volume[], 48. const long& volume[], const int& spread[]) 49. { 50. GL_PriceClose = close[rates_total - 1]; 51. GL_TimeAdjust = (spread[rates_total - 1] < 60 ? spread[rates_total - 1] : 0); 52. m_posBuff = rates_total; 53. (*Study).Update(m_Status); 54. 55. return rates_total; 56. } 57. //+------------------------------------------------------------------+
Mouse Study.mq5 的源代码片段
如果将上面显示的代码片段与鼠标指标的最新源代码进行比较,您会注意到代码中已添加第 14 行。换句话说,我们现在在指标模块中有一个新的全局变量。我无意在此鼠标指示器模块中添加新的全局变量。但这个很特别,原因你很快就会明白的。无论如何,这个变量都会在第 51 行被赋值。该值将通过传播进行传递。其中存在一个潜在的风险,同时也暗示了一些非常有用的东西。无论如何,这个全局变量只能以非常特殊的方式使用。为了更好地理解这一点,让我们看一下更新后的 C_Study.mqh 文件,如下所示:
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. //+------------------------------------------------------------------+ 004. #include "..\C_Mouse.mqh" 005. //+------------------------------------------------------------------+ 006. #define def_ExpansionPrefix def_MousePrefixName + "Expansion_" 007. //+------------------------------------------------------------------+ 008. class C_Study : public C_Mouse 009. { 010. private : 011. //+------------------------------------------------------------------+ 012. struct st00 013. { 014. eStatusMarket Status; 015. MqlRates Rate; 016. string szInfo, 017. szBtn1, 018. szBtn2, 019. szBtn3; 020. color corP, 021. corN; 022. int HeightText; 023. bool bvT, bvD, bvP; 024. datetime TimeDevice; 025. }m_Info; 026. //+------------------------------------------------------------------+ 027. void Draw(void) 028. { 029. double v1; 030. 031. if (m_Info.bvT) 032. { 033. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn1, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - 18); 034. ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn1, OBJPROP_TEXT, m_Info.szInfo); 035. } 036. if (m_Info.bvD) 037. { 038. v1 = NormalizeDouble((((GetInfoMouse().Position.Price - m_Info.Rate.close) / m_Info.Rate.close) * 100.0), 2); 039. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - 1); 040. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_BGCOLOR, (v1 < 0 ? m_Info.corN : m_Info.corP)); 041. ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_TEXT, StringFormat("%.2f%%", MathAbs(v1))); 042. } 043. if (m_Info.bvP) 044. { 045. v1 = NormalizeDouble((((GL_PriceClose - m_Info.Rate.close) / m_Info.Rate.close) * 100.0), 2); 046. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - 1); 047. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_BGCOLOR, (v1 < 0 ? m_Info.corN : m_Info.corP)); 048. ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_TEXT, StringFormat("%.2f%%", MathAbs(v1))); 049. } 050. } 051. //+------------------------------------------------------------------+ 052. inline void CreateObjInfo(EnumEvents arg) 053. { 054. switch (arg) 055. { 056. case evShowBarTime: 057. C_Mouse::CreateObjToStudy(2, 110, m_Info.szBtn1 = (def_ExpansionPrefix + (string)ObjectsTotal(0)), clrPaleTurquoise); 058. m_Info.bvT = true; 059. break; 060. case evShowDailyVar: 061. C_Mouse::CreateObjToStudy(2, 53, m_Info.szBtn2 = (def_ExpansionPrefix + (string)ObjectsTotal(0))); 062. m_Info.bvD = true; 063. break; 064. case evShowPriceVar: 065. C_Mouse::CreateObjToStudy(58, 53, m_Info.szBtn3 = (def_ExpansionPrefix + (string)ObjectsTotal(0))); 066. m_Info.bvP = true; 067. break; 068. } 069. } 070. //+------------------------------------------------------------------+ 071. inline void RemoveObjInfo(EnumEvents arg) 072. { 073. string sz; 074. 075. switch (arg) 076. { 077. case evHideBarTime: 078. sz = m_Info.szBtn1; 079. m_Info.bvT = false; 080. break; 081. case evHideDailyVar: 082. sz = m_Info.szBtn2; 083. m_Info.bvD = false; 084. break; 085. case evHidePriceVar: 086. sz = m_Info.szBtn3; 087. m_Info.bvP = false; 088. break; 089. } 090. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, false); 091. ObjectDelete(GetInfoTerminal().ID, sz); 092. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, true); 093. } 094. //+------------------------------------------------------------------+ 095. public : 096. //+------------------------------------------------------------------+ 097. C_Study(long IdParam, string szShortName, color corH, color corP, color corN) 098. :C_Mouse(IdParam, szShortName, corH, corP, corN) 099. { 100. if (_LastError != ERR_SUCCESS) return; 101. ZeroMemory(m_Info); 102. m_Info.Status = eCloseMarket; 103. m_Info.Rate.close = iClose(GetInfoTerminal().szSymbol, PERIOD_D1, ((GetInfoTerminal().szSymbol == def_SymbolReplay) || (macroGetDate(TimeCurrent()) != macroGetDate(iTime(GetInfoTerminal().szSymbol, PERIOD_D1, 0))) ? 0 : 1)); 104. m_Info.corP = corP; 105. m_Info.corN = corN; 106. CreateObjInfo(evShowBarTime); 107. CreateObjInfo(evShowDailyVar); 108. CreateObjInfo(evShowPriceVar); 109. } 110. //+------------------------------------------------------------------+ 111. void Update(const eStatusMarket arg) 112. { 113. int i0; 114. datetime dt; 115. 116. switch (m_Info.Status = (m_Info.Status != arg ? arg : m_Info.Status)) 117. { 118. case eCloseMarket : 119. m_Info.szInfo = "Closed Market"; 120. break; 121. case eInReplay : 122. case eInTrading : 123. i0 = PeriodSeconds(); 124. dt = (m_Info.Status == eInReplay ? m_Info.TimeDevice + GL_TimeAdjust : TimeCurrent()); 125. m_Info.Rate.time = (m_Info.Rate.time <= dt ? (datetime)(((ulong) dt / i0) * i0) + i0 : m_Info.Rate.time); 126. m_Info.szInfo = TimeToString((datetime)m_Info.Rate.time - dt, TIME_SECONDS); 127. break; 128. case eAuction : 129. m_Info.szInfo = "Auction"; 130. break; 131. default : 132. m_Info.szInfo = "ERROR"; 133. } 134. Draw(); 135. } 136. //+------------------------------------------------------------------+ 137. virtual void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) 138. { 139. C_Mouse::DispatchMessage(id, lparam, dparam, sparam); 140. switch (id) 141. { 142. case CHARTEVENT_CUSTOM + evHideBarTime: 143. RemoveObjInfo(evHideBarTime); 144. break; 145. case CHARTEVENT_CUSTOM + evShowBarTime: 146. CreateObjInfo(evShowBarTime); 147. break; 148. case CHARTEVENT_CUSTOM + evHideDailyVar: 149. RemoveObjInfo(evHideDailyVar); 150. break; 151. case CHARTEVENT_CUSTOM + evShowDailyVar: 152. CreateObjInfo(evShowDailyVar); 153. break; 154. case CHARTEVENT_CUSTOM + evHidePriceVar: 155. RemoveObjInfo(evHidePriceVar); 156. break; 157. case CHARTEVENT_CUSTOM + evShowPriceVar: 158. CreateObjInfo(evShowPriceVar); 159. break; 160. case (CHARTEVENT_CUSTOM + evSetServerTime): 161. m_Info.TimeDevice = (datetime)lparam; 162. break; 163. case CHARTEVENT_MOUSE_MOVE: 164. Draw(); 165. break; 166. } 167. ChartRedraw(GetInfoTerminal().ID); 168. } 169. //+------------------------------------------------------------------+ 170. }; 171. //+------------------------------------------------------------------+ 172. #undef def_ExpansionPrefix 173. #undef def_MousePrefixName 174. //+------------------------------------------------------------------+
C_Study.mqh源代码
好的,非常好。请注意,因为即将解释的内容对于理解整体功能非常重要。您会注意到头文件中的代码看起来不同。然而,这里只有两个真正重要的部分。第一个是第 160 行,我们在这里处理自定义事件。请注意,在第 161 行,我们将此事件传递的值存储到类内的私有变量中。这是第一个地方。现在,让我们看看真正的“魔法”发生在哪里。为此,让我们向上滚动到第 111 行,其中定义了 Update 过程。此过程负责创建我们稍后将显示的信息。现在要注意了,在第 123 行,我们捕获自定义交易品种图表当前时间范围内的秒数。然后,在第 124 行,我们执行一个小计算,或者,我们使用 MetaTrader 5 提供的值。我们是否执行计算或使用提供的值取决于指标的状态。当鼠标指标在回放交易品种上使用时,我们会执行计算。否则,我们使用 MetaTrader 5 提供的值。
请注意,此计算考虑了我们从鼠标指标代码片段中捕获的值,以及回放/模拟器应用程序提供的另一个值。我们稍后会看到第二个值是如何传递给鼠标指标的。
无论如何,我们所拥有的是一个会随着时间而改变的数据点。然而,我们还需要另一个值,该值在下面第 125 行计算。此行确定图表上生成新柱形的确切时刻。然后在第 126 行,我们执行最后一次计算,以确定并向用户呈现当前柱形关闭前的剩余时间。
整个系统将正常运行,因为负责告诉我们距离新柱形成还剩多少时间的实体是交易服务器。或者,在使用回放/模拟的情况下,负责的实体是更新柱形的服务,允许 MetaTrader 5 呈现它们。
非常好。您可能已经注意到,由于上述解释,我们需要修改服务代码。然而,真正需要更新的是 C_Replay.mqh 文件中的代码。但在我们这样做之前,我们需要在服务代码的两个方面做一个小的修改。首先是在 Macros.mqh 头文件中添加一些内容。操作方法如下:
1. //+------------------------------------------------------------------+ 2. #property copyright "Daniel Jose" 3. //+------------------------------------------------------------------+ 4. #define macroRemoveSec(A) (A - (A % 60)) 5. #define macroGetDate(A) (A - (A % 86400)) 6. #define macroGetSec(A) (A - (A - (A % 60))) 7. //+------------------------------------------------------------------+
Macros.mqh 源代码
既然一切都很简单,我就不谈细节了。完成后,我们将修改 C_FilesTick.mqh 头文件。该修改具有特殊性质,如以下片段所示:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #include "C_FileBars.mqh" 05. #include "C_Simulation.mqh" 06. #include "..\..\Auxiliar\Macros.mqh" 07. //+------------------------------------------------------------------+ 08. //#define macroRemoveSec(A) (A - (A % 60)) 09. #define def_MaxSizeArray 16777216 // 16 Mbytes 10. //+------------------------------------------------------------------+ 11. class C_FileTicks 12. { 13. protected: 14. enum ePlotType {PRICE_EXCHANGE, PRICE_FOREX};
C_FilesTick.mqh 源代码片段
请注意,在这种情况下添加了第 06 行,而我们突出显示的第 08 行应从代码中删除。发生这种情况是因为 Macros.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 UpdateIndicatorControl(void) 038. { 039. double Buff[]; 040. 041. if (m_IndControl.Handle == INVALID_HANDLE) return; 042. if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position) 043. { 044. if (CopyBuffer(m_IndControl.Handle, 0, 0, 1, Buff) == 1) 045. m_IndControl.Memory.dValue = Buff[0]; 046. if ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay) 047. m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition]; 048. }else 049. { 050. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position; 051. m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (ushort)m_IndControl.Mode; 052. m_IndControl.Memory._8b[7] = 'D'; 053. m_IndControl.Memory._8b[6] = 'M'; 054. EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, 0, m_IndControl.Memory.dValue, ""); 055. } 056. } 057. //+------------------------------------------------------------------+ 058. void SweepAndCloseChart(void) 059. { 060. long id; 061. 062. if ((id = ChartFirst()) > 0) do 063. { 064. if (ChartSymbol(id) == def_SymbolReplay) 065. ChartClose(id); 066. }while ((id = ChartNext(id)) > 0); 067. } 068. //+------------------------------------------------------------------+ 069. inline void CreateBarInReplay(bool bViewTick) 070. { 071. bool bNew; 072. double dSpread; 073. int iRand = rand(); 074. 075. if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[0], bNew)) 076. { 077. m_Infos.tick[0] = m_MemoryData.Info[m_Infos.CountReplay]; 078. if (m_MemoryData.ModePlot == PRICE_EXCHANGE) 079. { 080. dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 ); 081. if (m_Infos.tick[0].last > m_Infos.tick[0].ask) 082. { 083. m_Infos.tick[0].ask = m_Infos.tick[0].last; 084. m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread; 085. }else if (m_Infos.tick[0].last < m_Infos.tick[0].bid) 086. { 087. m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread; 088. m_Infos.tick[0].bid = m_Infos.tick[0].last; 089. } 090. } 091. if (bViewTick) 092. { 093. CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 094. if (bNew) EventChartCustom(m_Infos.IdReplay, evSetServerTime, (long)m_Infos.Rate[0].time, 0, ""); 095. } 096. m_Infos.Rate[0].spread = (int)macroGetSec(m_MemoryData.Info[m_Infos.CountReplay].time); 097. CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate); 098. } 099. m_Infos.CountReplay++; 100. } 101. //+------------------------------------------------------------------+ 102. void AdjustViewDetails(void) 103. { 104. MqlRates rate[1]; 105. 106. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_ASK_LINE, GetInfoTicks().ModePlot == PRICE_FOREX); 107. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_BID_LINE, GetInfoTicks().ModePlot == PRICE_FOREX); 108. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_LAST_LINE, GetInfoTicks().ModePlot == PRICE_EXCHANGE); 109. m_Infos.PointsPerTick = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE); 110. CopyRates(def_SymbolReplay, PERIOD_M1, 0, 1, rate); 111. if ((m_Infos.CountReplay == 0) && (GetInfoTicks().ModePlot == PRICE_EXCHANGE)) 112. for (; GetInfoTicks().Info[m_Infos.CountReplay].volume_real == 0; m_Infos.CountReplay++); 113. if (rate[0].close > 0) 114. { 115. if (GetInfoTicks().ModePlot == PRICE_EXCHANGE) 116. m_Infos.tick[0].last = rate[0].close; 117. else 118. { 119. m_Infos.tick[0].bid = rate[0].close; 120. m_Infos.tick[0].ask = rate[0].close + (rate[0].spread * m_Infos.PointsPerTick); 121. } 122. m_Infos.tick[0].time = rate[0].time; 123. m_Infos.tick[0].time_msc = rate[0].time * 1000; 124. }else 125. m_Infos.tick[0] = GetInfoTicks().Info[m_Infos.CountReplay]; 126. CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 127. } 128. //+------------------------------------------------------------------+ 129. void AdjustPositionToReplay(void) 130. { 131. int nPos, nCount; 132. 133. if (m_IndControl.Position == (int)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks)) return; 134. nPos = (int)((m_MemoryData.nTicks * m_IndControl.Position) / def_MaxSlider); 135. for (nCount = 0; m_MemoryData.Rate[nCount].spread < nPos; m_Infos.CountReplay = m_MemoryData.Rate[nCount++].spread); 136. if (nCount > 0) CustomRatesUpdate(def_SymbolReplay, m_MemoryData.Rate, nCount - 1); 137. while ((nPos > m_Infos.CountReplay) && def_CheckLoopService) 138. CreateBarInReplay(false); 139. } 140. //+------------------------------------------------------------------+ 141. public : 142. //+------------------------------------------------------------------+ 143. C_Replay() 144. :C_ConfigService() 145. { 146. Print("************** Market Replay Service **************"); 147. srand(GetTickCount()); 148. SymbolSelect(def_SymbolReplay, false); 149. CustomSymbolDelete(def_SymbolReplay); 150. CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay)); 151. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0); 152. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, 0); 153. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, 0); 154. CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation"); 155. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, 8); 156. SymbolSelect(def_SymbolReplay, true); 157. m_Infos.CountReplay = 0; 158. m_IndControl.Handle = INVALID_HANDLE; 159. m_IndControl.Mode = C_Controls::ePause; 160. m_IndControl.Position = 0; 161. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = C_Controls::eTriState; 162. } 163. //+------------------------------------------------------------------+ 164. ~C_Replay() 165. { 166. SweepAndCloseChart(); 167. IndicatorRelease(m_IndControl.Handle); 168. SymbolSelect(def_SymbolReplay, false); 169. CustomSymbolDelete(def_SymbolReplay); 170. Print("Finished replay service..."); 171. } 172. //+------------------------------------------------------------------+ 173. bool OpenChartReplay(const ENUM_TIMEFRAMES arg1, const string szNameTemplate) 174. { 175. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == 0) 176. return MsgError("Asset configuration is not complete, it remains to declare the size of the ticket."); 177. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == 0) 178. return MsgError("Asset configuration is not complete, need to declare the ticket value."); 179. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == 0) 180. return MsgError("Asset configuration not complete, need to declare the minimum volume."); 181. SweepAndCloseChart(); 182. m_Infos.IdReplay = ChartOpen(def_SymbolReplay, arg1); 183. if (!ChartApplyTemplate(m_Infos.IdReplay, szNameTemplate + ".tpl")) 184. Print("Failed apply template: ", szNameTemplate, ".tpl Using template default.tpl"); 185. else 186. Print("Apply template: ", szNameTemplate, ".tpl"); 187. 188. return true; 189. } 190. //+------------------------------------------------------------------+ 191. bool InitBaseControl(const ushort wait = 1000) 192. { 193. Print("Waiting for Mouse Indicator..."); 194. Sleep(wait); 195. while ((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, 0, "Indicator Mouse Study") == INVALID_HANDLE)) Sleep(200); 196. if (def_CheckLoopService) 197. { 198. AdjustViewDetails(); 199. Print("Waiting for Control Indicator..."); 200. if ((m_IndControl.Handle = iCustom(ChartSymbol(m_Infos.IdReplay), ChartPeriod(m_Infos.IdReplay), "::" + def_IndicatorControl, m_Infos.IdReplay)) == INVALID_HANDLE) return false; 201. ChartIndicatorAdd(m_Infos.IdReplay, 0, m_IndControl.Handle); 202. UpdateIndicatorControl(); 203. } 204. 205. return def_CheckLoopService; 206. } 207. //+------------------------------------------------------------------+ 208. bool LoopEventOnTime(void) 209. { 210. int iPos; 211. 212. while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay)) 213. { 214. UpdateIndicatorControl(); 215. Sleep(200); 216. } 217. m_MemoryData = GetInfoTicks(); 218. AdjustPositionToReplay(); 219. EventChartCustom(m_Infos.IdReplay, evSetServerTime, (long)macroRemoveSec(m_MemoryData.Info[m_Infos.CountReplay].time), 0, ""); 220. iPos = 0; 221. while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService)) 222. { 223. if (m_IndControl.Mode == C_Controls::ePause) return true; 224. 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); 225. CreateBarInReplay(true); 226. while ((iPos > 200) && (def_CheckLoopService)) 227. { 228. Sleep(195); 229. iPos -= 200; 230. m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks); 231. UpdateIndicatorControl(); 232. } 233. } 234. 235. return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService)); 236. } 237. }; 238. //+------------------------------------------------------------------+ 239. #undef def_SymbolReplay 240. #undef def_CheckLoopService 241. #undef def_MaxSlider 242. //+------------------------------------------------------------------+
C_Replay.mqh 源代码
这里的问题比你最初想象的要复杂一些。原因是我们需要直接告知鼠标指标我们在模拟或回放中的确切位置。从某种程度上来说,这实际上不是一个问题。在本文前面我演示了如何实现这一点。然而,你也注意到发生了一些奇怪的事情,很明显我们需要一种不同的方法来正确处理它。
该方法是通过引入三行新代码实现的。但不要误以为这是一个完美的解决方案。因为事实并非如此。它仅仅解决了我们问题的一个方面。这种方法还不能解决另一个问题,至少目前还不能。但在我们深入探讨这种方法的局限性之前,让我们先看看这些代码行在做什么。
我们从最简单的开始,它几乎直接执行:第 96 行。如果您注意鼠标指标源代码片段,您会发现我们正在使用扩展值来实现以秒为单位的微调。好吧,如果我们不能直接通过汇率传递这个值,我们会尽快通过其他方式发送它。这是我发现的方法,因为在整个模拟或回放过程中我们都没有使用点差值。所以我们会以更有趣的方式使用它。这里有一个我们还不关心的小问题。目前我们还可以接受这一点。
继续解释新行:如果您稍微看一下上面的第 94 行,您会注意到,每次回放/模拟服务检测到新柱形时,我们都会触发自定义事件来通知鼠标指标要使用的新值。同样,在第 219 行,我们再次告诉指标使用哪个值。在这两种情况下,这都是通过自定义事件完成的。
但为什么要这么做呢?有没有其他方法可以达到同样的效果?是的,有。然而,事实证明这还不够充分。至少不是我们仍然需要解决的问题。事实上,只有当我们知道新的一分钟柱形已关闭时,才会触发这些自定义事件。我们可以用不同的方式来做这件事,而不需要向图表触发自定义事件。例如,我们可以使用 iTime 库函数来确定何时创建新的一分钟柱形。请密切注意这个细节:我们不关心当前图表时间范围内的柱形时间;一分钟柱形的时间戳很重要。您可能会有点困惑,但请看一下下面的代码片段。
110. //+------------------------------------------------------------------+ 111. void Update(const eStatusMarket arg) 112. { 113. int i0; 114. datetime dt; 115. 116. switch (m_Info.Status = (m_Info.Status != arg ? arg : m_Info.Status)) 117. { 118. case eCloseMarket : 119. m_Info.szInfo = "Closed Market"; 120. break; 121. case eInReplay : 122. case eInTrading : 123. i0 = PeriodSeconds(); 124. dt = (m_Info.Status == eInReplay ? iTime(NULL, 0, 0) + GL_TimeAdjust : TimeCurrent()); 125. m_Info.Rate.time = (m_Info.Rate.time <= dt ? (datetime)(((ulong) dt / i0) * i0) + i0 : m_Info.Rate.time); 126. m_Info.szInfo = TimeToString((datetime)m_Info.Rate.time - dt, TIME_SECONDS); 127. break; 128. case eAuction : 129. m_Info.szInfo = "Auction"; 130. break; 131. default : 132. m_Info.szInfo = "ERROR"; 133. } 134. Draw(); 135. } 136. //+------------------------------------------------------------------+
来自文件 C_Study.mqh 的代码片段
更改位于第 124 行。使用该函数来确定柱的时间是浪费时间,因为结果值将与 OnCalculate 函数中的值相同。但是,如果您更改如下所示的相同片段,则一切都将完全不同。
110. //+------------------------------------------------------------------+ 111. void Update(const eStatusMarket arg) 112. { 113. int i0; 114. datetime dt; 115. 116. switch (m_Info.Status = (m_Info.Status != arg ? arg : m_Info.Status)) 117. { 118. case eCloseMarket : 119. m_Info.szInfo = "Closed Market"; 120. break; 121. case eInReplay : 122. case eInTrading : 123. i0 = PeriodSeconds(); 124. dt = (m_Info.Status == eInReplay ? iTime(NULL, PERIOD_M1, 0) + GL_TimeAdjust : TimeCurrent()); 125. m_Info.Rate.time = (m_Info.Rate.time <= dt ? (datetime)(((ulong) dt / i0) * i0) + i0 : m_Info.Rate.time); 126. m_Info.szInfo = TimeToString((datetime)m_Info.Rate.time - dt, TIME_SECONDS); 127. break; 128. case eAuction : 129. m_Info.szInfo = "Auction"; 130. break; 131. default : 132. m_Info.szInfo = "ERROR"; 133. } 134. Draw(); 135. } 136. //+------------------------------------------------------------------+
来自文件 C_Study.mqh 的代码片段
请注意,变化仅仅在于我们现在要求 MetaTrader 5 告诉我们一分钟柱形何时开始。只需这样做,我们就不必让服务触发自定义事件来向我们传递相同的信息。但是等一下,我不太明白!好吧,我亲爱的读者,重点是:无论图表的时间范围如何,MetaTrader 5 提供的信息恰恰是一分钟柱形开始的时间戳。这正是自定义事件实际要传达的信息。但是,通过在服务中处理这个问题,我确保信息仅在适当的时间间隔传递,而不是从指标内部调用它,并且可能通过重复请求相同的信息而导致系统过载。
最后的探讨
虽然我们在这里看到的还不能代表问题的最终解决方案,但事实证明,它非常适合现阶段。因此,我认为目前没有理由不使用它。尽管如此,重要的是要认识到,我们很快就会需要一个更精细、更强大的解决方案。但在此之前,我们现在有一种可靠的方法来检测何时会生成新的柱形。
在下面的视频中,您可以看到当前系统的运行情况。
运行演示版本
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/12286



