
开发回放系统(第 71 部分):取得正确的时间(四)
概述
在上一篇文章“开发回放系统(第 70 部分):取得正确的时间(三)”中,我解释了鼠标指标所需的修改。这些修改的目的是使鼠标指标能够接收订单簿事件。这具体是指与回放/模拟应用程序一起使用的情况。亲爱的读者,你可能对所有这些变化感到非常沮丧和困惑。我明白,其中许多起初可能没有任何意义。它们可能比我希望的要复杂得多。然而,无论乍一看多么令人困惑,你都必须充分理解这些内容。我知道你们中的许多人可能很难理解我在那篇文章中想要传达的意思。然而,如果不理解之前的内容(我使用了一个更简单的服务来演示整个机制是如何工作的),试图理解这里要解释的内容将变得更加困难。
因此,在深入探讨本文实际要做的事情之前,请确保您已经理解了上一篇文章中涵盖的内容。特别是关于如何通过将订单簿事件添加到自定义交易品种中,以一种以前不可能的方式使用 OnCalculate 函数的部分。这要求我们使用 iSpread 调用来获取 MetaTrader 5 向我们提供的数据。
在本文中,我们将实际执行测试服务中使用的部分代码的转移(或更准确地说,转录),并将其带入回放/模拟服务。这里的主要问题不是我们如何做到这一点,而是我们应该如何去做。
亲爱的读者,让我提醒您,直到上一篇文章,我们正在研究回放/模拟服务,鼠标指标是通过模板加载的。然而,我不会再这样做了。如果愿意,您可以尝试继续使用模板来加载鼠标指标。但由于一些实际考虑,我们将开始手动将鼠标指标添加到回放/模拟交易品种的图表中。所以,如果在一些视频中,我以这种方式展示事物,不要感到惊讶。我有我的理由,但我不会在这里详细说明。那么,让我们开始将代码从测试服务转录到回放/模拟服务中。
开始转录
我们要做的第一件事是修改头文件 C_Replay.mqh 中的部分代码。请查看以下代码片段:
197. //+------------------------------------------------------------------+ 198. bool InitBaseControl(const ushort wait = 1000) 199. { 200. Print("Waiting for Mouse Indicator..."); 201. Sleep(wait); 202. while ((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, 0, "Indicator Mouse Study") == INVALID_HANDLE)) Sleep(200); 203. if (def_CheckLoopService) 204. { 205. AdjustViewDetails(); 206. Print("Waiting for Control Indicator..."); 207. if ((m_IndControl.Handle = iCustom(ChartSymbol(m_Infos.IdReplay), ChartPeriod(m_Infos.IdReplay), "::" + def_IndicatorControl, m_Infos.IdReplay)) == INVALID_HANDLE) return false; 208. ChartIndicatorAdd(m_Infos.IdReplay, 0, m_IndControl.Handle); 209. UpdateIndicatorControl(); 210. } 211. 212. return def_CheckLoopService; 213. } 214. //+------------------------------------------------------------------+
C_Terminal.mqh 文件中的代码
此片段是原始代码。现在,我希望你注意以下几点。该代码最初设计为在通过模板加载鼠标指标时起作用。然而,由于现在将手动将鼠标指标放置在图表上,因此该代码不再适用。从技术上来说,它仍然有效。但我们可以对其进行改进,以便在执行流和所呈现的消息方面提供更合适的配置。以下是将要使用的新代码:
197. //+------------------------------------------------------------------+ 198. bool InitBaseControl(const ushort wait = 1000) 199. { 200. Sleep(wait); 201. AdjustViewDetails(); 202. Print("Loading Control Indicator..."); 203. if ((m_IndControl.Handle = iCustom(ChartSymbol(m_Infos.IdReplay), ChartPeriod(m_Infos.IdReplay), "::" + def_IndicatorControl, m_Infos.IdReplay)) == INVALID_HANDLE) return false; 204. ChartIndicatorAdd(m_Infos.IdReplay, 0, m_IndControl.Handle); 205. Print("Waiting for Mouse Indicator..."); 206. while ((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, 0, "Indicator Mouse Study") == INVALID_HANDLE)) Sleep(200); 207. UpdateIndicatorControl(); 208. 209. return def_CheckLoopService; 210. } 211. //+------------------------------------------------------------------+
来自 C_Replay.mqh 文件的代码
基本上,代码本身的变化非常小。但这些信息现在更清楚地说明了正在发生的事情。此外,执行顺序已被颠倒。现在,我们将首先尝试加载控制指标,它是回放/模拟应用程序的一部分。这是因为控制指标嵌入在回放/模拟器可执行文件中。只有在那之后我们才会加载鼠标指标。请注意以下几点:如果应用程序中包含的控制指标无法加载,则表明存在严重问题。如果加载成功,我们就可以通知用户鼠标指标也需要加载。在我看来,这是一个更合适的工作流程。也就是说,如果您愿意,可以随意调整加载顺序。无论哪种方式,除非图表上存在鼠标指标,否则控制指标实际上不会起作用。
这种变化实际上具有美学性质。现在让我们继续讨论实际支持订单簿消息的更改。如果你不理解上一篇文章,请返回并使用之前的代码复习材料。不要试图使用从现在开始显示的代码来理解一切是如何工作的。如果你试图这样做,你会发现自己完全困惑了。
我们需要在类构造函数中添加新行。您可以在下面的代码中看到这一新行:
149. //+------------------------------------------------------------------+ 150. C_Replay() 151. :C_ConfigService() 152. { 153. Print("************** Market Replay Service **************"); 154. srand(GetTickCount()); 155. SymbolSelect(def_SymbolReplay, false); 156. CustomSymbolDelete(def_SymbolReplay); 157. CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay)); 158. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0); 159. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, 0); 160. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, 0); 161. CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation"); 162. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, 8); 163. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_TICKS_BOOKDEPTH, 1); 164. SymbolSelect(def_SymbolReplay, true); 165. m_Infos.CountReplay = 0; 166. m_IndControl.Handle = INVALID_HANDLE; 167. m_IndControl.Mode = C_Controls::ePause; 168. m_IndControl.Position = 0; 169. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = C_Controls::eTriState; 170. } 171. //+------------------------------------------------------------------+
来自 C_Replay.mqh 文件的代码
新行正好是第 163 行。完成此操作后,我们可以使用订单簿消息。现在请注意一件事,重点并不在于 C_Replay.mqh 头文件,而在于鼠标指标。因此,让我们从指标代码中进行抽象,以更好地理解其工作原理。因为它很重要。
27. //+------------------------------------------------------------------+ 28. int OnInit() 29. { 30. Study = new C_Study(0, "Indicator Mouse Study", user02, user03, user04); 31. if (_LastError >= ERR_USER_ERROR_FIRST) return INIT_FAILED; 32. MarketBookAdd((*Study).GetInfoTerminal().szSymbol); 33. OnBookEvent((*Study).GetInfoTerminal().szSymbol); 34. m_Status = C_Study::eCloseMarket; 35. SetIndexBuffer(0, m_Buff, INDICATOR_DATA); 36. ArrayInitialize(m_Buff, EMPTY_VALUE); 37. 38. return INIT_SUCCEEDED; 39. } 40. //+------------------------------------------------------------------+
鼠标指标文件片段
请注意,在第 34 行,我们设置了鼠标指标的初始状态。此状态表示市场已关闭。但我们这里处理的不是真正的市场。我们正在开发一个应用程序,其主要目标是允许回放或模拟潜在的市场动向。因此,当我们位于用于回放/模拟的自定义交易品种上时,鼠标指标当前显示的消息是不正确的。幸运的是,这个问题非常非常容易解决。但在我们修复它之前,您需要了解,当控制指标放置在图表上时,回放/模拟实际上处于暂停状态。这适用于应用程序从一开始初始化的情况。现在我们面临的决定将对接下来发生的事情产生重大影响。
让我们仔细思考一下:当我们处于暂停模式时,鼠标指标是否应该显示竞价信息?或者它应该显示当前柱的剩余时间?如果我们决定显示剩余时间,那么该信息应该在第一次播放触发之前出现,还是在之后出现?这听起来可能有点令人困惑,所以让我们澄清一下。在市场正式开市之前,会有一个竞价阶段。这使得参与者能够以最优价格下订单,即他们真正想要买入或卖出的价格。因此,一旦回放/模拟应用程序导致 MetaTrader 5 加载图表并且鼠标指标变得可见,我们应该看到的消息就是竞价消息。这是事实。现在,当您启用模拟或回放然后再次暂停时,消息应该说什么?是否还应显示“竞价”?或者现在应该显示当前柱剩余的时间?这是我们需要思考的问题。无论如何,我们需要确保的第一件事是,当应用程序启动时,它清楚地表明我们处于竞价阶段。
这样做其实很简单。请参阅下面的代码片段。
212. //+------------------------------------------------------------------+ 213. bool LoopEventOnTime(void) 214. { 215. int iPos, iCycles; 216. MqlBookInfo book[1]; 217. 218. book[0].price = 1.0; 219. book[0].volume = 1; 220. book[0].type = BOOK_TYPE_BUY_MARKET; 221. CustomBookAdd(def_SymbolReplay, book, 1); 222. while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay)) 223. { 224. UpdateIndicatorControl(); 225. Sleep(200); 226. } 227. m_MemoryData = GetInfoTicks(); 228. AdjustPositionToReplay(); 229. iPos = iCycles = 0; 230. while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService)) 231. { 232. if (m_IndControl.Mode == C_Controls::ePause) return true; 233. 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); 234. CreateBarInReplay(true); 235. while ((iPos > 200) && (def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePause)) 236. { 237. Sleep(195); 238. iPos -= 200; 239. m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks); 240. UpdateIndicatorControl(); 241. iCycles = (iCycles == 4 ? RateUpdate(false) : iCycles + 1); 242. } 243. } 244. 245. return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService)); 246. } 247. }; 248. //+------------------------------------------------------------------+
来自 C_Replay.mqh 文件的代码
以下是我们实际需要实施的简单部分。我将逐步介绍实现,这样你就可以真正地了解正在做的事情。看看第 216 行,我们有一个新变量。这是一个只有一个元素的数组。有趣的部分来了。
您可能认为,订单簿事件中注入的数值需要具有一定的意义。但现实是,它们根本不需要具有什么意义。我们只需要确保用于触发订单簿事件的值遵循一些内部逻辑。但它们不一定有意义,除非您的目标是模拟真实的订单簿。然而,这并不是我的本意。至少现在还不是,也许将来会。
无论如何,第 218 行和第 219 行用于为 MetaTrader 5 提供一些内容来填充订单簿。这些值不具有任何特殊含义。它们的存在只是为了支持我真正关心的东西,即第 220 行。在第 220 行,我们通知订单簿我们有一个指示竞价状态的位置。如果不理解这一点,我建议重新阅读上一篇文章以更好地理解这种行为。然后,在第 221 行,我们告诉 MetaTrader 5 触发自定义订单簿事件。此事件由 OnEventBook 函数捕获,在本例中该函数位于鼠标指标中。结果:每次进入 LoopEventOnTime 函数时,鼠标指标确实会显示我们正在进行竞价。LoopEventOnTime 从头开始运行的场景有两种:第一种情况是应用程序第一次初始化时。第二种情况是当用户通过按下暂停按钮与控制指标进行交互时发生的。在这种情况下,将执行第 232 行,然后立即重新执行 LoopEventOnTime 函数。你注意到这有多容易吗?现在我们已经显示了竞价信息,那么我们如何在柱形中显示剩余时间呢?这其实很简单。为此,我们需要修改代码如下:
212. //+------------------------------------------------------------------+ 213. bool LoopEventOnTime(void) 214. { 215. int iPos, iCycles; 216. MqlBookInfo book[1]; 217. 218. book[0].price = 1.0; 219. book[0].volume = 1; 220. book[0].type = BOOK_TYPE_BUY_MARKET; 221. CustomBookAdd(def_SymbolReplay, book, 1); 222. while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay)) 223. { 224. UpdateIndicatorControl(); 225. Sleep(200); 226. } 227. m_MemoryData = GetInfoTicks(); 228. AdjustPositionToReplay(); 229. iPos = iCycles = 0; 230. book[0].type = BOOK_TYPE_BUY; 231. CustomBookAdd(def_SymbolReplay, book, 1); 232. while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService)) 233. { 234. if (m_IndControl.Mode == C_Controls::ePause) return true; 235. 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); 236. CreateBarInReplay(true); 237. while ((iPos > 200) && (def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePause)) 238. { 239. Sleep(195); 240. iPos -= 200; 241. m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks); 242. UpdateIndicatorControl(); 243. iCycles = (iCycles == 4 ? RateUpdate(false) : iCycles + 1); 244. } 245. } 246. 247. return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService)); 248. } 249. }; 250. //+------------------------------------------------------------------+
来自 C_Replay.mqh 文件的代码
您注意到区别了吗?如果你没有注意到,那可能是因为你太分心了 —— 因为区别恰恰在于第 230 行和第 231 行的包含。这两行确保当用户在回放/模拟服务上按下播放键时,鼠标指标会收到自定义订单簿事件。此事件标志着我们已经退出竞价状态并进入活跃交易状态。结果,当前柱形的剩余时间将开始显示在鼠标指标上。正如您所见,一切都非常简单。然而,我们现在面临着一个稍微复杂一些的情况。
在真实市场中,即当我们连接到交易服务器时,有时证券可能会被暂停或进入竞价。这通常是由于特定的监管条件而发生的。我在上一篇文章中讨论过这个问题。但现在,我们将实现一个简化的规则。如果资产在一次报价和下一次报价之间的时间间隔等于或大于 60 秒,则鼠标指标将显示该资产已进入竞价。这其实很简单。棘手的部分是这样的:我们如何让鼠标指标随后返回显示柱形的剩余时间?
你可能会说:“当资产进入拍卖模式时,我们会向账簿发送 BOOK_TYPE_BUY_MARKET 常量;当退出拍卖时,我们会发送 BOOK_TYPE_BUY。”没错,这就是我们需要做的。但我们如何正确地做到这一点呢?让我们仔细思考一下:我们不希望 LoopEventOnTime 函数从头开始重新启动。我们希望系统在从第 232 行开始到第 245 行结束的循环中继续运行。现在,如果您在该循环中同时发送 BOOK_TYPE_BUY_MARKET 和 BOOK_TYPE_BUY,则会遇到问题。这是因为每次使用这些不同的常量调用 CustomBookAdd() 肯定会为观看鼠标指标的用户产生令人不快的视觉效果。它将闪烁并在剩余时间和“竞价”字样之间快速交替。
因此,我们需要发挥创造力。我们必须实现一种既能避免闪烁效应又能有效解决问题的解决方案。我提出的解决方案如下:
212. //+------------------------------------------------------------------+ 213. bool LoopEventOnTime(void) 214. { 215. int iPos, iCycles; 216. MqlBookInfo book[1]; 217. ENUM_BOOK_TYPE typeMsg; 218. 219. book[0].price = 1.0; 220. book[0].volume = 1; 221. book[0].type = BOOK_TYPE_BUY_MARKET; 222. CustomBookAdd(def_SymbolReplay, book, 1); 223. while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay)) 224. { 225. UpdateIndicatorControl(); 226. Sleep(200); 227. } 228. m_MemoryData = GetInfoTicks(); 229. AdjustPositionToReplay(); 230. iPos = iCycles = 0; 231. book[0].type = BOOK_TYPE_BUY; 232. CustomBookAdd(def_SymbolReplay, book, 1); 233. while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService)) 234. { 235. if (m_IndControl.Mode == C_Controls::ePause) return true; 236. 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); 237. if ((typeMsg = (iPos >= 60000 ? BOOK_TYPE_BUY_MARKET : BOOK_TYPE_BUY)) != book[0].type) 238. { 239. book[0].type = typeMsg; 240. CustomBookAdd(def_SymbolReplay, book, 1); 241. } 242. CreateBarInReplay(true); 243. while ((iPos > 200) && (def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePause)) 244. { 245. Sleep(195); 246. iPos -= 200; 247. m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks); 248. UpdateIndicatorControl(); 249. iCycles = (iCycles == 4 ? RateUpdate(false) : iCycles + 1); 250. } 251. } 252. 253. return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService)); 254. } 255. }; 256. //+------------------------------------------------------------------+
来自 C_Replay.mqh 文件的代码
我知道它不太优雅,但至少它有效。如果你愿意,你可以通过增加或减少时间阈值在不同条件下自由测试它。这完全取决于你。但在开始更改之前,我们花点时间了解一下这段代码中发生了什么,怎么样?
首先,请注意以下内容:在第 217 行,我声明了一个新变量。此变量用于保存订单簿接受的可能常量之一。然后在第 237 行,我使用三元运算符来简化逻辑,因为其思想是评估一个条件,并在此基础上为 typeMsg 变量分配一个值。现在请注意:我本可以进一步压缩这段代码,但这会使解释变得不必要地复杂。它的工作原理如下。在将常量分配给 typeMsg 后,我们检查其值是否与作为自定义订单簿事件发送的最后一个值不同。如果不同,那么在第 239 行,我们分配要使用的新常量,并在第 240 行调用 CustomBookAdd。您真正需要关注的部分是与 iPos 变量进行比较的值,具体来说是第 237 行。请注意,我们正在将它与值 60000(六万)进行比较。但为什么是这个值呢?我们不是使用一分钟的阈值吗?是的,但您可能忘记了这个简单的事实 —— 一分钟等于 60 秒。现在,看第 236 行 —— 分配给 iPos 的值以毫秒为单位。一秒钟有 1000 毫秒。这就是我们将 iPos 与 60000 进行比较的原因。它反映 60 秒的间隔,以毫秒表示。因此,如果您决定将此阈值更改为您认为更合适的其他阈值,请确保将其正确转换为毫秒。否则,鼠标指标可能会显示意外行为,尤其是当资产在一段时间内没有在图表中添加报价时。
至此,我们终于可以查看 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 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 int RateUpdate(bool bCheck) 070. { 071. static int st_Spread = 0; 072. 073. st_Spread = (bCheck ? (int)macroGetTime(m_MemoryData.Info[m_Infos.CountReplay].time) : st_Spread + 1); 074. m_Infos.Rate[0].spread = (int)(def_MaskTimeService | st_Spread); 075. CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate); 076. 077. return 0; 078. } 079. //+------------------------------------------------------------------+ 080. inline void CreateBarInReplay(bool bViewTick) 081. { 082. bool bNew; 083. double dSpread; 084. int iRand = rand(); 085. 086. if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[0], bNew)) 087. { 088. m_Infos.tick[0] = m_MemoryData.Info[m_Infos.CountReplay]; 089. if (m_MemoryData.ModePlot == PRICE_EXCHANGE) 090. { 091. dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 ); 092. if (m_Infos.tick[0].last > m_Infos.tick[0].ask) 093. { 094. m_Infos.tick[0].ask = m_Infos.tick[0].last; 095. m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread; 096. }else if (m_Infos.tick[0].last < m_Infos.tick[0].bid) 097. { 098. m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread; 099. m_Infos.tick[0].bid = m_Infos.tick[0].last; 100. } 101. } 102. if (bViewTick) 103. CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 104. RateUpdate(true); 105. } 106. m_Infos.CountReplay++; 107. } 108. //+------------------------------------------------------------------+ 109. void AdjustViewDetails(void) 110. { 111. MqlRates rate[1]; 112. 113. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_ASK_LINE, GetInfoTicks().ModePlot == PRICE_FOREX); 114. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_BID_LINE, GetInfoTicks().ModePlot == PRICE_FOREX); 115. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_LAST_LINE, GetInfoTicks().ModePlot == PRICE_EXCHANGE); 116. m_Infos.PointsPerTick = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE); 117. CopyRates(def_SymbolReplay, PERIOD_M1, 0, 1, rate); 118. if ((m_Infos.CountReplay == 0) && (GetInfoTicks().ModePlot == PRICE_EXCHANGE)) 119. for (; GetInfoTicks().Info[m_Infos.CountReplay].volume_real == 0; m_Infos.CountReplay++); 120. if (rate[0].close > 0) 121. { 122. if (GetInfoTicks().ModePlot == PRICE_EXCHANGE) 123. m_Infos.tick[0].last = rate[0].close; 124. else 125. { 126. m_Infos.tick[0].bid = rate[0].close; 127. m_Infos.tick[0].ask = rate[0].close + (rate[0].spread * m_Infos.PointsPerTick); 128. } 129. m_Infos.tick[0].time = rate[0].time; 130. m_Infos.tick[0].time_msc = rate[0].time * 1000; 131. }else 132. m_Infos.tick[0] = GetInfoTicks().Info[m_Infos.CountReplay]; 133. CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 134. } 135. //+------------------------------------------------------------------+ 136. void AdjustPositionToReplay(void) 137. { 138. int nPos, nCount; 139. 140. if (m_IndControl.Position == (int)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks)) return; 141. nPos = (int)((m_MemoryData.nTicks * m_IndControl.Position) / def_MaxSlider); 142. for (nCount = 0; m_MemoryData.Rate[nCount].spread < nPos; m_Infos.CountReplay = m_MemoryData.Rate[nCount++].spread); 143. if (nCount > 0) CustomRatesUpdate(def_SymbolReplay, m_MemoryData.Rate, nCount - 1); 144. while ((nPos > m_Infos.CountReplay) && def_CheckLoopService) 145. CreateBarInReplay(false); 146. } 147. //+------------------------------------------------------------------+ 148. public : 149. //+------------------------------------------------------------------+ 150. C_Replay() 151. :C_ConfigService() 152. { 153. Print("************** Market Replay Service **************"); 154. srand(GetTickCount()); 155. SymbolSelect(def_SymbolReplay, false); 156. CustomSymbolDelete(def_SymbolReplay); 157. CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay)); 158. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0); 159. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, 0); 160. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, 0); 161. CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation"); 162. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, 8); 163. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_TICKS_BOOKDEPTH, 1); 164. SymbolSelect(def_SymbolReplay, true); 165. m_Infos.CountReplay = 0; 166. m_IndControl.Handle = INVALID_HANDLE; 167. m_IndControl.Mode = C_Controls::ePause; 168. m_IndControl.Position = 0; 169. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = C_Controls::eTriState; 170. } 171. //+------------------------------------------------------------------+ 172. ~C_Replay() 173. { 174. SweepAndCloseChart(); 175. IndicatorRelease(m_IndControl.Handle); 176. SymbolSelect(def_SymbolReplay, false); 177. CustomSymbolDelete(def_SymbolReplay); 178. Print("Finished replay service..."); 179. } 180. //+------------------------------------------------------------------+ 181. bool OpenChartReplay(const ENUM_TIMEFRAMES arg1, const string szNameTemplate) 182. { 183. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == 0) 184. return MsgError("Asset configuration is not complete, it remains to declare the size of the ticket."); 185. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == 0) 186. return MsgError("Asset configuration is not complete, need to declare the ticket value."); 187. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == 0) 188. return MsgError("Asset configuration not complete, need to declare the minimum volume."); 189. SweepAndCloseChart(); 190. m_Infos.IdReplay = ChartOpen(def_SymbolReplay, arg1); 191. if (!ChartApplyTemplate(m_Infos.IdReplay, szNameTemplate + ".tpl")) 192. Print("Failed apply template: ", szNameTemplate, ".tpl Using template default.tpl"); 193. else 194. Print("Apply template: ", szNameTemplate, ".tpl"); 195. 196. return true; 197. } 198. //+------------------------------------------------------------------+ 199. bool InitBaseControl(const ushort wait = 1000) 200. { 201. Sleep(wait); 202. AdjustViewDetails(); 203. Print("Loading Control Indicator..."); 204. if ((m_IndControl.Handle = iCustom(ChartSymbol(m_Infos.IdReplay), ChartPeriod(m_Infos.IdReplay), "::" + def_IndicatorControl, m_Infos.IdReplay)) == INVALID_HANDLE) return false; 205. ChartIndicatorAdd(m_Infos.IdReplay, 0, m_IndControl.Handle); 206. Print("Waiting for Mouse Indicator..."); 207. while ((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, 0, "Indicator Mouse Study") == INVALID_HANDLE)) Sleep(200); 208. UpdateIndicatorControl(); 209. 210. return def_CheckLoopService; 211. } 212. //+------------------------------------------------------------------+ 213. bool LoopEventOnTime(void) 214. { 215. int iPos, iCycles; 216. MqlBookInfo book[1]; 217. ENUM_BOOK_TYPE typeMsg; 218. 219. book[0].price = 1.0; 220. book[0].volume = 1; 221. book[0].type = BOOK_TYPE_BUY_MARKET; 222. CustomBookAdd(def_SymbolReplay, book, 1); 223. while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay)) 224. { 225. UpdateIndicatorControl(); 226. Sleep(200); 227. } 228. m_MemoryData = GetInfoTicks(); 229. AdjustPositionToReplay(); 230. iPos = iCycles = 0; 231. book[0].type = BOOK_TYPE_BUY; 232. CustomBookAdd(def_SymbolReplay, book, 1); 233. while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService)) 234. { 235. if (m_IndControl.Mode == C_Controls::ePause) return true; 236. 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); 237. if ((typeMsg = (iPos >= 60000 ? BOOK_TYPE_BUY_MARKET : BOOK_TYPE_BUY)) != book[0].type) 238. { 239. book[0].type = typeMsg; 240. CustomBookAdd(def_SymbolReplay, book, 1); 241. } 242. CreateBarInReplay(true); 243. while ((iPos > 200) && (def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePause)) 244. { 245. Sleep(195); 246. iPos -= 200; 247. m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks); 248. UpdateIndicatorControl(); 249. iCycles = (iCycles == 4 ? RateUpdate(false) : iCycles + 1); 250. } 251. } 252. 253. return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService)); 254. } 255. }; 256. //+------------------------------------------------------------------+ 257. #undef def_SymbolReplay 258. #undef def_CheckLoopService 259. #undef def_MaxSlider 260. //+------------------------------------------------------------------+
C_Replay.mqh 文件的源代码
这是一个相当有趣但无害的 bug。
好吧,好吧,虽然我们刚刚构建和演示的系统运行良好,但仍然存在一个问题。尽管我没有在视频中显示这一点,但我相信,如果你仔细思考,你会注意到当前实现中的一个缺陷。但在我解释问题所在之前,让我们花点时间检查一下您对 MetaTrader 5 的工作原理有多了解。如果您对 MetaTrader 5 在不同条件下的操作方式没有深入的了解,那么您很可能没有注意到这个问题。这是因为缺陷存在于某个非常具体的地方:在使用回放/模拟系统时,您无法更改图表时间框架。
现在你可能会问:“你的意思是我不能改变图表的时间框架?如果我尝试的话会发生什么?”嗯,这就是发生的事情 —— 是的,它 100% 会发生 —— 我们系统中的缺陷将被触发。然而,这个缺陷只会在非常特殊的情况下出现。仅当回放/模拟系统在播放模式下运行时才会触发它。如果处于暂停模式或系统根据交易数量检测到该交易品种正在竞价,则不会引发错误。再次强调,只有当我们处于播放模式并且图表中添加分时报价时,这种情况才会发生。
让我再问一次,特别是对于那些一直关注本系列关于使用 MQL5 构建回放/模拟器系统的文章的读者:你知道这个缺陷可能是什么吗?如果您的回答是肯定的 —— 太好了!这表明您一直在研究并深入研究 MQL5 和 MetaTrader 5 的工作原理。如果您的答案是否定的 —— 不用担心!这只是意味着你还处于学习之旅的早期阶段。让这成为继续学习的动力。
让我们来看看这个 bug 到底是什么。它是轻微的、无害的,并且仅在您处于播放模式时才会显示,其中正在积极处理分时报价。一旦您更改图表时间框架,就会发生一些有趣的事情。如果满足上述所有条件,并且您更改时间框架,鼠标指标会突然显示市场已关闭。您也将不再看到当前柱中剩余的时间。要修复此问题,您必须暂停然后恢复模拟。
现在你可能会想:“为什么改变时间框架会导致指标显示市场已经关闭?这不大合理。”我同意你的看法。但如果您使用 MetaTrader 5 的时间足够长,您可能已经知道原因了。MetaTrader 5 从图表中卸载所有指标和其他元素。然后,它根据新的时间框架刷新图表数据并从头开始重新加载指标(和其他元素)。这就是 MetaTrader 5 的运作方式。因此,当鼠标指标重新加载时,它会从其默认状态启动。要确切了解我的意思,请看一下直接从鼠标指标中获取的以下代码片段:
27. //+------------------------------------------------------------------+ 28. int OnInit() 29. { 30. Study = new C_Study(0, "Indicator Mouse Study", user02, user03, user04); 31. if (_LastError >= ERR_USER_ERROR_FIRST) return INIT_FAILED; 32. MarketBookAdd((*Study).GetInfoTerminal().szSymbol); 33. OnBookEvent((*Study).GetInfoTerminal().szSymbol); 34. m_Status = C_Study::eCloseMarket; 35. SetIndexBuffer(0, m_Buff, INDICATOR_DATA); 36. ArrayInitialize(m_Buff, EMPTY_VALUE); 37. 38. return INIT_SUCCEEDED; 39. } 40. //+------------------------------------------------------------------+
鼠标指标文件片段
现在观察以下内容:在此代码片段的第 34 行,我们初始化状态变量的值。该值实际上表明市场已经关闭。然而,回放/模拟服务仍不断传来报价。而这正是缺陷所在。正如你所看到的,它是无害的,不会造成更严重的问题。这只是一种不便,只需暂停然后恢复服务即可解决。一旦你这样做,一切都会恢复正常。
您可能会想出一千种方法来解决这个问题。但我将展示的解决方案比你想象的要难得多。在我们讨论这个问题之前,让我问一下:你知道为什么简单地暂停然后恢复模拟会导致问题消失吗?如果没有,让我们来看看为什么这能解决问题。为了理解它,我们需要重新审视 C_Replay.mqh 头文件中的代码,特别是它的 LoopEventOnTime 函数。当出现错误时,鼠标指标显示市场已关闭。一旦触发暂停模式,第 235 行就会中断从第 233 行开始的循环。这会强制函数返回代码主体。由于返回值为 true,服务主例程立即再次调用 LoopEventOnTime。此时,第 222 行触发自定义订单簿事件,使用第 221 行定义的值。结果,鼠标指标将其状态从“市场关闭”更新为“竞价模式”。然后,当用户再次点击播放来恢复模拟时,第 232 行会触发另一个自定义订单簿事件。但现在它将使用第 231 行的常数。此常数允许鼠标指标再次显示当前柱形中的剩余时间。
这就是为什么当由于图表时间框架的变化而触发错误时,只需暂停然后恢复就足以恢复正常功能。
然而,虽然这个问题不会对鼠标指标产生重大影响,但它确实给另一个组件带来了问题:控制指标。在这种情况下,没有简单的解决方法。我们必须等待 C_Replay.mqh 头文件第 42 行检查的条件失败。为什么会这样?为什么我们需要这个条件失败才能使控制指标重新出现在图表上?因为只有当该检查失败时,才会执行第 54 行。我们需要运行第 54 行,以便更新控制指标。一旦更新,控制指标将再次被绘制,假设它之前由于某种原因被隐藏。
好吧,现在您可能意识到情况变得越来越复杂。虽然只需在播放和暂停之间切换即可解决鼠标指标故障,但如果控制指标不可见,我们甚至无法做到这一点。我们该如何解决这个问题?如果没有控制指标,我们就无法暂停应用程序,并且服务将继续向图表推送分时报价。除非服务停止添加分时报价,否则我们不会看到第 42 行的测试失败的情况。仅当图表中添加了一定数量的报价后,才会发生这种情况。现在这是一个严重的问题。
您可能会想,“我只需要监控服务中的图表时间框架。如果发生变化,我将触发鼠标和控制指标的重新初始化。”好吧,但是您知道如何检测服务中图表时间框架的变化吗?或许这是有可能的。但我向你保证,这将比我分享的解决方案复杂得多。不是在这篇文章中,而是在下一篇文章中。因为我想给你一个机会,亲爱的读者,先尝试自己解决这个问题。
结论
在本文中,我向您展示了如何重新利用上一篇文章中的代码,该代码用于测试自定义订单簿消息的功能,并将其集成到我们开发了一段时间的服务中。我还演示了实现中看似无害的小错误是如何导致大麻烦的。
不过,我希望您对如何解决图表时间框架问题感到好奇。具体来说,如何让服务检测图表时间框架何时发生变化。我将在下一篇文章中展示解决方案。
成为一名程序员意味着在别人看不到前进的道路时找到解决方案。问题总是存在的,没有它们,编程就不会那么有趣或有意义。因此,问题越多越好。问题越复杂,解决起来就越有成就感。它们推动我们跳出思维定势,它们让我们摆脱了一遍又一遍地以同样的方式做事的单调。
我们下篇文章再见。在阅读下一篇文章之前,请尝试找到图表时间框架变化问题的解决方案。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/12335
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。


