
开发回放系统(第 69 部分):取得正确的时间(二)
概述
在上一篇文章 “开发回放系统(第 68 部分):取得正确的时间(一) ” 中,我解释了与鼠标指标相关的代码部分。然而,除非您还检查回放/模拟器服务的代码,否则该代码就没有什么价值。无论如何,如果你还没有读过上一篇文章,我建议你在尝试理解这篇文章之前先读一读。这是因为两者真正相辅相成。
目前,这里的重点是提供有关资产流动性低时柱形剩余时间的信息。这可能是由于在此期间没有传统的 OnCalculate 事件生成。因此,鼠标指标将无法接收到与经过的秒数相对应的正确值。然而,基于前一篇文章中所涵盖的内容,我们确实可以传递必要的值,以便指标可以计算剩余的秒数。
在这个阶段,我们将主要关注回放/模拟器服务。更具体地说,我们的注意力将集中在文件 C_Replay.mqh 上。因此,让我们首先回顾一下需要修改或添加的代码。
调整文件 C_Replay.mqh
需要做的改变不多,但是,它们为上一篇文章中讨论的代码提供了适当的上下文,特别是涉及在 OnCalculate 事件中使用 iSpread 库函数的部分。您可能会质疑我为什么使用 iSpread 函数,特别是因为直接从传递给 OnCalculate 函数的数组中读取扩展值似乎更简单。
确实,这是一个相当有趣的观点。但要理解其中的原因,我们需要掌握事情在幕后是如何运作的。为此,我们需要检查并了解回放/模拟器服务代码的运行方式。当然,我们还需要了解 MetaTrader 5 如何处理这些信息。
我们从最简单的部分开始:理解 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. static int st_Spread = 0; 075. 076. if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[0], bNew)) 077. { 078. m_Infos.tick[0] = m_MemoryData.Info[m_Infos.CountReplay]; 079. if (m_MemoryData.ModePlot == PRICE_EXCHANGE) 080. { 081. dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 ); 082. if (m_Infos.tick[0].last > m_Infos.tick[0].ask) 083. { 084. m_Infos.tick[0].ask = m_Infos.tick[0].last; 085. m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread; 086. }else if (m_Infos.tick[0].last < m_Infos.tick[0].bid) 087. { 088. m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread; 089. m_Infos.tick[0].bid = m_Infos.tick[0].last; 090. } 091. } 092. if (bViewTick) 093. { 094. CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 095. if (bNew) EventChartCustom(m_Infos.IdReplay, evSetServerTime, (long)m_Infos.Rate[0].time, 0, ""); 096. } 097. st_Spread = (int)macroGetTime(m_MemoryData.Info[m_Infos.CountReplay].time); 098. m_Infos.Rate[0].spread = (int)macroGetSec(m_MemoryData.Info[m_Infos.CountReplay].time); 099. CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate); 100. } 101. m_Infos.Rate[0].spread = (int)(def_MaskTimeService | st_Spread); 102. CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate); 103. m_Infos.CountReplay++; 104. } 105. //+------------------------------------------------------------------+ 106. void AdjustViewDetails(void) 107. { 108. MqlRates rate[1]; 109. 110. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_ASK_LINE, GetInfoTicks().ModePlot == PRICE_FOREX); 111. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_BID_LINE, GetInfoTicks().ModePlot == PRICE_FOREX); 112. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_LAST_LINE, GetInfoTicks().ModePlot == PRICE_EXCHANGE); 113. m_Infos.PointsPerTick = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE); 114. CopyRates(def_SymbolReplay, PERIOD_M1, 0, 1, rate); 115. if ((m_Infos.CountReplay == 0) && (GetInfoTicks().ModePlot == PRICE_EXCHANGE)) 116. for (; GetInfoTicks().Info[m_Infos.CountReplay].volume_real == 0; m_Infos.CountReplay++); 117. if (rate[0].close > 0) 118. { 119. if (GetInfoTicks().ModePlot == PRICE_EXCHANGE) 120. m_Infos.tick[0].last = rate[0].close; 121. else 122. { 123. m_Infos.tick[0].bid = rate[0].close; 124. m_Infos.tick[0].ask = rate[0].close + (rate[0].spread * m_Infos.PointsPerTick); 125. } 126. m_Infos.tick[0].time = rate[0].time; 127. m_Infos.tick[0].time_msc = rate[0].time * 1000; 128. }else 129. m_Infos.tick[0] = GetInfoTicks().Info[m_Infos.CountReplay]; 130. CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 131. } 132. //+------------------------------------------------------------------+ 133. void AdjustPositionToReplay(void) 134. { 135. int nPos, nCount; 136. 137. if (m_IndControl.Position == (int)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks)) return; 138. nPos = (int)((m_MemoryData.nTicks * m_IndControl.Position) / def_MaxSlider); 139. for (nCount = 0; m_MemoryData.Rate[nCount].spread < nPos; m_Infos.CountReplay = m_MemoryData.Rate[nCount++].spread); 140. if (nCount > 0) CustomRatesUpdate(def_SymbolReplay, m_MemoryData.Rate, nCount - 1); 141. while ((nPos > m_Infos.CountReplay) && def_CheckLoopService) 142. CreateBarInReplay(false); 143. } 144. //+------------------------------------------------------------------+ 145. public : 146. //+------------------------------------------------------------------+ 147. C_Replay() 148. :C_ConfigService() 149. { 150. Print("************** Market Replay Service **************"); 151. srand(GetTickCount()); 152. SymbolSelect(def_SymbolReplay, false); 153. CustomSymbolDelete(def_SymbolReplay); 154. CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay)); 155. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0); 156. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, 0); 157. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, 0); 158. CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation"); 159. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, 8); 160. SymbolSelect(def_SymbolReplay, true); 161. m_Infos.CountReplay = 0; 162. m_IndControl.Handle = INVALID_HANDLE; 163. m_IndControl.Mode = C_Controls::ePause; 164. m_IndControl.Position = 0; 165. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = C_Controls::eTriState; 166. } 167. //+------------------------------------------------------------------+ 168. ~C_Replay() 169. { 170. SweepAndCloseChart(); 171. IndicatorRelease(m_IndControl.Handle); 172. SymbolSelect(def_SymbolReplay, false); 173. CustomSymbolDelete(def_SymbolReplay); 174. Print("Finished replay service..."); 175. } 176. //+------------------------------------------------------------------+ 177. bool OpenChartReplay(const ENUM_TIMEFRAMES arg1, const string szNameTemplate) 178. { 179. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == 0) 180. return MsgError("Asset configuration is not complete, it remains to declare the size of the ticket."); 181. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == 0) 182. return MsgError("Asset configuration is not complete, need to declare the ticket value."); 183. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == 0) 184. return MsgError("Asset configuration not complete, need to declare the minimum volume."); 185. SweepAndCloseChart(); 186. m_Infos.IdReplay = ChartOpen(def_SymbolReplay, arg1); 187. if (!ChartApplyTemplate(m_Infos.IdReplay, szNameTemplate + ".tpl")) 188. Print("Failed apply template: ", szNameTemplate, ".tpl Using template default.tpl"); 189. else 190. Print("Apply template: ", szNameTemplate, ".tpl"); 191. 192. return true; 193. } 194. //+------------------------------------------------------------------+ 195. bool InitBaseControl(const ushort wait = 1000) 196. { 197. Print("Waiting for Mouse Indicator..."); 198. Sleep(wait); 199. while ((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, 0, "Indicator Mouse Study") == INVALID_HANDLE)) Sleep(200); 200. if (def_CheckLoopService) 201. { 202. AdjustViewDetails(); 203. Print("Waiting for 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. UpdateIndicatorControl(); 207. } 208. 209. return def_CheckLoopService; 210. } 211. //+------------------------------------------------------------------+ 212. bool LoopEventOnTime(void) 213. { 214. int iPos; 215. 216. while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay)) 217. { 218. UpdateIndicatorControl(); 219. Sleep(200); 220. } 221. m_MemoryData = GetInfoTicks(); 222. AdjustPositionToReplay(); 223. EventChartCustom(m_Infos.IdReplay, evSetServerTime, (long)macroRemoveSec(m_MemoryData.Info[m_Infos.CountReplay].time), 0, ""); 224. iPos = 0; 225. while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService)) 226. { 227. if (m_IndControl.Mode == C_Controls::ePause) return true; 228. 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); 229. CreateBarInReplay(true); 230. while ((iPos > 200) && (def_CheckLoopService)) 231. { 232. Sleep(195); 233. iPos -= 200; 234. m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks); 235. UpdateIndicatorControl(); 236. } 237. } 238. 239. return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService)); 240. } 241. }; 242. //+------------------------------------------------------------------+ 243. #undef def_SymbolReplay 244. #undef def_CheckLoopService 245. #undef def_MaxSlider 246. //+------------------------------------------------------------------+
C_Replay.mqh 文件的源代码
在上面的代码中,您可能会注意到有几行被划掉了。这些行应该从本文之前存在的代码版本中删除。要删除的行不多,但删除它们的影响将是巨大的。
首先要注意的是,在第 74 行,引入了一个新变量。这个变量的目的很简单:计算流动性下降或变得非常低时的秒数。虽然这一逻辑目前还没有执行,但了解正在发生的事情以掌握如何实现这一点很重要。
首先,观察第 223 行,自定义事件已从原始代码中删除。此外,请注意,在从第 225 行开始的循环的每次迭代中,都会调用 CreateBarInReplay。这是在第 229 行完成的。现在,请密切关注以下细节:由于第 232 行和从第 225 行开始执行循环所需的时间,CreateBarInReplay 函数大约每 195 毫秒执行一次。假设迭代之间没有延迟,这将导致每秒大约五次调用。现在,你应该忘记高流动性的情况。我试图说明当流动性非常低时,回放/模拟器服务实际上是如何运作的。因此请记住这个数字:每秒大约有五次调用 CreateBarInReplay 函数。
现在让我们回到 CreateBarInReplay 过程,了解当流动性充足时会发生什么,即当我们每秒至少有五次调用时。
在这种情况下,第 76 行的条件将被评估为 true。因此,第 77 行和第 100 行之间的代码块将被执行。但是,请注意,在这个范围内,一些行已从代码中删除,如删除线所示。其中第 95 行用于为每个新的一分钟柱形触发自定义事件。这一特定细节对于解释为什么 iSpread 函数出现在 OnCalculate 过程中至关重要。但就目前而言,别担心。让我们专注于理解基础知识。请注意,第 97 行添加了一段新代码,用于初始化变量值。
现在,请密切注意这一点:第 98 行和第 99 行被删除了。但它们所包含的逻辑并没有被抛弃,它只是被重新定位了。以前,如果第76行的条件评估为 true,则此代码位于执行的块内。现在,它将无条件执行,如第 101 行和第 102 行所示。现在请注意以下几点:虽然第 101 行不同,但它执行的任务与第 98 行相同。现在的关键区别是使用位掩码。这使得鼠标指标能够识别出传播值源自回放/模拟器服务。我们在这里所做的就是使用 OR 操作来正确配置掩码。然而,这引入了一个潜在的问题:如果 st_Spread 变量的值侵犯了位掩码区域,鼠标指标将无法正确解释传入的值。
因此,如果出现任何问题或错误,只需验证 st_Spread 变量的值是否超出了为掩码保留的位边界。在正常情况下,这种情况不会发生,因为回放/模拟器是为日内研究和分析而设计的。只有当回放/模拟器服务达到其绝对时间限制时,才有可能出现这种情况。作为参考,这个时间限制是近 12 天,以秒为单位,这远远超过了我们的预期目的。
让我们继续了解系统是如何工作的。如果您编译并运行回放/模拟器服务以及上一篇文章中编译的鼠标指标版本,并且如果资产具有足够的流动性(即每秒至少一个分时报价),您将收到有关当前柱形关闭和下一个打开的剩余时间的准确更新。
这一切都很好,但它仍然没有解释为什么没有使用 OnCalculate 函数版本之一中可用的点差数组,以及为什么需要 iSpread 函数来获取服务报告的点差值,如第 101 行所示。为了理解这一点,我们需要探索一个不同的概念。
了解为什么使用 iSpread
在撰写本文时,MetaTrader 5 的最新版本如下所示:
即使在这个版本中 —— 亲爱的读者,当你读到这篇文章时,这种行为可能仍然没有改变 —— MetaTrader 5仍然以一种相当奇怪的方式处理柱形,至少对于自定义资产是这样。也许并非所有与柱形相关的信息都会受到影响,但由于我们通过点差传输数据,因此很明显这种行为有些奇怪。
为了证明这一点,我们对 C_Replay.mqh 头文件和鼠标指标中的代码做一些小的修改。我相信这将使清楚地展示实际发生的事情变得容易得多,因为仅仅解释是不够的。因此,在文件 C_Replay.mqh 中,我们修改以下片段中的代码:
068. //+------------------------------------------------------------------+ 069. inline void CreateBarInReplay(bool bViewTick) 070. { 071. bool bNew; 072. double dSpread; 073. int iRand = rand(); 074. static int st_Spread = 0; 075. 076. if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[0], bNew)) 077. { 078. m_Infos.tick[0] = m_MemoryData.Info[m_Infos.CountReplay]; 079. if (m_MemoryData.ModePlot == PRICE_EXCHANGE) 080. { 081. dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 ); 082. if (m_Infos.tick[0].last > m_Infos.tick[0].ask) 083. { 084. m_Infos.tick[0].ask = m_Infos.tick[0].last; 085. m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread; 086. }else if (m_Infos.tick[0].last < m_Infos.tick[0].bid) 087. { 088. m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread; 089. m_Infos.tick[0].bid = m_Infos.tick[0].last; 090. } 091. } 092. if (bViewTick) 093. CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 094. st_Spread = (int)macroGetTime(m_MemoryData.Info[m_Infos.CountReplay].time); 095. } 096. Print(TimeToString(st_Spread, TIME_SECONDS)); 097. m_Infos.Rate[0].spread = (int)(def_MaskTimeService | st_Spread); 098. CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate); 099. m_Infos.CountReplay++; 100. } 101. //+------------------------------------------------------------------+
来自 C_Replay.mqh 文件的代码
请注意,此片段中的代码已经清理完毕,因此行号可能略有不同。但是,代码本身与本文前面显示的代码相同。唯一的区别是第 96 行,添加该行是为了在终端中显示当前写入柱形点差字段的值。运行修改后的代码后,您将看到如下所示的输出:
请注意,打印的值与当前时间在分时报价图上显示的值完全相同。理解这一点非常重要。现在,我们可以确认,插入到柱形点差字段中的值实际上是图表上显示的时间值。现在,让我们转向其他话题。我们将对控制指标进行轻微的修改(非常微妙),以分析系统的行为方式。此修改将针对头文件 C_Study.mqh 中的代码进行。您可以看到下面的变化:
109. //+------------------------------------------------------------------+ 110. void Update(const eStatusMarket arg) 111. { 112. int i0; 113. datetime dt; 114. 115. switch (m_Info.Status = (m_Info.Status != arg ? arg : m_Info.Status)) 116. { 117. case eCloseMarket : 118. m_Info.szInfo = "Closed Market"; 119. break; 120. case eInReplay : 121. case eInTrading : 122. i0 = PeriodSeconds(); 123. dt = (m_Info.Status == eInReplay ? (datetime) GL_TimeAdjust : TimeCurrent()); 124. m_Info.Rate.time = (m_Info.Rate.time <= dt ? (datetime)(((ulong) dt / i0) * i0) + i0 : m_Info.Rate.time); 125. if (dt > 0) m_Info.szInfo = TimeToString((datetime)m_Info.Rate.time/* - dt*/, TIME_SECONDS); 126. break; 127. case eAuction : 128. m_Info.szInfo = "Auction"; 129. break; 130. default : 131. m_Info.szInfo = "ERROR"; 132. } 133. Draw(); 134. } 135. //+------------------------------------------------------------------+
C_Study.mqh 文件的一部分
请密切注意这里,因为变化非常微妙。在第 125 行,dt 设置被删除。这意味着现在显示的信息是预计新柱形出现的准确时间。请注意:它并不代表到下一个柱形还剩多少时间,而是代表下一个柱形实际预计出现的时间。完成此更改后,我们重新编译鼠标指标以测试将显示的输出。在下面的动画中,您可以观察到实际发生的情况:
请注意,使用的图表时间范围是两分钟。现在进行的计算表明下一个柱形出现的准确时刻。这是鼠标指标中显示的内容。您可以看到,当图表时间到达指定点时,指标立即开始报告新柱出现的时间。换句话说,系统正在按预期运行。然而,这些测试尚未验证回放/模拟服务提供的价值。到目前为止,我们所做的只是确认我们预期会出现的信息。现在,我们需要验证服务传递的实际值。重要的是确保图表时间范围不是设置为一分钟,否则测试将无效。所以,让我们把它保持在两分钟,这足以分析发生了什么。
为了使测试按预期进行,我们需要做一个小的修改。再次,请密切关注以下片段中的代码:
109. //+------------------------------------------------------------------+ 110. void Update(const eStatusMarket arg) 111. { 112. int i0; 113. datetime dt; 114. 115. switch (m_Info.Status = (m_Info.Status != arg ? arg : m_Info.Status)) 116. { 117. case eCloseMarket : 118. m_Info.szInfo = "Closed Market"; 119. break; 120. case eInReplay : 121. case eInTrading : 122. i0 = PeriodSeconds(); 123. dt = (m_Info.Status == eInReplay ? (datetime) GL_TimeAdjust : TimeCurrent()); 124. m_Info.Rate.time = (m_Info.Rate.time <= dt ? (datetime)(((ulong) dt / i0) * i0) + i0 : m_Info.Rate.time); 125. if (dt > 0) m_Info.szInfo = TimeToString((datetime)/*m_Info.Rate.time -*/ dt, TIME_SECONDS); 126. break; 127. case eAuction : 128. m_Info.szInfo = "Auction"; 129. break; 130. default : 131. m_Info.szInfo = "ERROR"; 132. } 133. Draw(); 134. } 135. //+------------------------------------------------------------------+
C_Study.mqh 文件的一部分
现在,我们选择服务提供的值并将其显示在鼠标指针中。结果如下所示:
正如你所看到的,它完全符合我们的预期。此时,我们不会对头文件进行任何进一步的更改。相反,我们将重点关注鼠标指标中的其他内容。让我们看看如果我们使用在 OnCalculate 调用期间获得的点差值会发生什么。为此,我们需要修改鼠标指标的代码。但请记住以下几点:指标中显示的值将是捕获并分配给 GL_TimeAdjust 变量的值。记住这一点至关重要。现在,让我们修改指标代码,以测试使用 OnCalculate 获得的点差值是否真正合适。更新后的代码如下所示:
46. //+------------------------------------------------------------------+ 47. int OnCalculate(const int rates_total, const int prev_calculated, const datetime& time[], const double& open[], 48. const double& high[], const double& low[], const double& close[], const long& tick_volume[], 49. const long& volume[], const int& spread[]) 50. //int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double& price[]) 51. { 52. GL_PriceClose = close[rates_total - 1]; 53. // GL_PriceClose = price[rates_total - 1]; 54. GL_TimeAdjust = (spread[rates_total - 1] & (~def_MaskTimeService); 55. // if (_Symbol == def_SymbolReplay) 56. // GL_TimeAdjust = iSpread(NULL, PERIOD_M1, 0) & (~def_MaskTimeService); 57. m_posBuff = rates_total; 58. (*Study).Update(m_Status); 59. 60. return rates_total; 61. } 62. //+------------------------------------------------------------------+
鼠标指针文件片段
仔细观察我们在上面的片段中正在做什么。我们正在隔离代码,以便划线代表当前版本,如前一篇文章所示。应暂时删除这些行。取而代之的是,我们添加了新行,旨在通过 OnCalculate 函数使用 MetaTrader 5 提供的数据。换句话说,我们不再依赖 iSpread 函数调用。相反,我们在点差数组中使用 MetaTrader 5 提供的值。为了确保与服务的兼容性,我们需要做一些小的调整,如第 54 行所示。您会注意到,它执行的操作与以前使用 iSpread 完成的操作相同,只是现在使用的值直接来自传递给 OnCalculate 的参数。这对我们来说意义重大,因为它消除了仅仅为了检索 MetaTrader 5 已经提供的值而进行额外函数调用的需要。
现在,让我们来看看运行此更新代码的结果。如下面动画所示:
啊,这里刚刚发生了什么?为什么鼠标指标中的值冻结了?这个问题的答案并不简单。与许多人可能认为的相反,我实际上并不知道答案。等等,我怎么会不知道呢?我确实有一些怀疑。但与其猜测,我更愿意简单地告诉你,你可能没有预料到的事情可能会发生。这样,你就可以自己观察并得出自己的结论。
无论如何,服务会像以前一样继续输出值,就像我们在前面的动画中看到的一样。该指标仍能捕捉点差值。但为什么它会冻结呢?我无法解释。我所知道的是,当图表上出现新的柱形时,这就是为什么使用一分钟以外的时间范围来揭示这个问题很重要,然后点差数组中的值将被正确更新。
让我再次提醒你:当你阅读这篇文章时,我在这里展示的内容可能还没有发生。这是因为 MetaTrader 5 很可能已经收到了纠正此问题的更新。在此之前,我正在使用 iSpread 函数解决这个问题。一旦这个小问题得到解决,我们将停止使用 iSpread,转而依赖 MetaTrader 5 直接传递给 OnCalculate 的值。因此,不要太执着于代码的任何特定部分 —— 随着开发的进展,一切都会得到改进。这样,我相信您现在明白为什么我使用 iSpread 而不是将点差值作为参数传递给 OnCalculate。但我们还没有完成。当低流动性阻止我们每秒接收报价(或更准确地说,OnCalculate 事件)时,我们仍然需要设计一种方法,让服务通知我们柱形的剩余时间。为了继续,我们现在将恢复本节中所做的更改(用于演示在指标中使用 iSpread 的原因)并返回到服务工作。
修复服务中的缺陷
不幸的是,当分时报价之间的时间超过一秒时,为了使一切正常工作,我们需要采取与本文开始时最初计划略有不同的方法。问题在于我本来打算将计数器放置在柱形创建程序中。然而,我忽略了一个重要的细节:时间。回到本文的开头,查看头文件 C_Replay.mqh 的源代码。在第 230 行中,我们有一个循环,使回放/模拟服务等待适当的时间过去后才出现新的分时报价。问题就在这里。
在开发和测试期间,我处理的是流动性高的资产,即历史数据的时间间隔通常小于一秒。当我开始实施变更以支持更长的分时报价间隔的可能性时,一个缺陷就出现了。并不是因为它突然出现,而是因为它一直都在那里,被相对较短的分时报价间隔所隐藏。现在密切关注第 230 行和第 236 行之间的循环。它有什么问题?问题是,它没有考虑到用户可能暂停系统的可能性。怎么会这样?如果服务处于循环中,等待下一个分时报价,那肯定没问题,对吧?不完全是。当等待时间超过一秒时,我们就会遇到麻烦。
假设我们正在回放外汇数据。在每日交易时段开始时,分时报价间隔可能相当大。如果您点击播放并且服务检测到需要等待 40 秒才能进行下一次播放,那么即使您按下暂停键,将控制滑块移动到不同的点,然后再次按下播放键,服务也不会响应。因为它卡在了第 230 行到第 236 行的循环中,等待了整整 40 秒。所以我们需要解决的第一件事就是它。但是,我们不要单独地修复这个问题,而是继续同时实施修复和在流动性低期间显示剩余时间的解决方案。整个 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. static int st_Spread = 0; 086. 087. if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[0], bNew)) 088. { 089. m_Infos.tick[0] = m_MemoryData.Info[m_Infos.CountReplay]; 090. if (m_MemoryData.ModePlot == PRICE_EXCHANGE) 091. { 092. dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 ); 093. if (m_Infos.tick[0].last > m_Infos.tick[0].ask) 094. { 095. m_Infos.tick[0].ask = m_Infos.tick[0].last; 096. m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread; 097. }else if (m_Infos.tick[0].last < m_Infos.tick[0].bid) 098. { 099. m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread; 100. m_Infos.tick[0].bid = m_Infos.tick[0].last; 101. } 102. } 103. if (bViewTick) 104. CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 105. RateUpdate(true); 106. st_Spread = (int)macroGetTime(m_MemoryData.Info[m_Infos.CountReplay].time); 107. } 108. m_Infos.Rate[0].spread = (int)(def_MaskTimeService | st_Spread); 109. CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate); 110. m_Infos.CountReplay++; 111. } 112. //+------------------------------------------------------------------+ 113. void AdjustViewDetails(void) 114. { 115. MqlRates rate[1]; 116. 117. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_ASK_LINE, GetInfoTicks().ModePlot == PRICE_FOREX); 118. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_BID_LINE, GetInfoTicks().ModePlot == PRICE_FOREX); 119. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_LAST_LINE, GetInfoTicks().ModePlot == PRICE_EXCHANGE); 120. m_Infos.PointsPerTick = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE); 121. CopyRates(def_SymbolReplay, PERIOD_M1, 0, 1, rate); 122. if ((m_Infos.CountReplay == 0) && (GetInfoTicks().ModePlot == PRICE_EXCHANGE)) 123. for (; GetInfoTicks().Info[m_Infos.CountReplay].volume_real == 0; m_Infos.CountReplay++); 124. if (rate[0].close > 0) 125. { 126. if (GetInfoTicks().ModePlot == PRICE_EXCHANGE) 127. m_Infos.tick[0].last = rate[0].close; 128. else 129. { 130. m_Infos.tick[0].bid = rate[0].close; 131. m_Infos.tick[0].ask = rate[0].close + (rate[0].spread * m_Infos.PointsPerTick); 132. } 133. m_Infos.tick[0].time = rate[0].time; 134. m_Infos.tick[0].time_msc = rate[0].time * 1000; 135. }else 136. m_Infos.tick[0] = GetInfoTicks().Info[m_Infos.CountReplay]; 137. CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 138. } 139. //+------------------------------------------------------------------+ 140. void AdjustPositionToReplay(void) 141. { 142. int nPos, nCount; 143. 144. if (m_IndControl.Position == (int)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks)) return; 145. nPos = (int)((m_MemoryData.nTicks * m_IndControl.Position) / def_MaxSlider); 146. for (nCount = 0; m_MemoryData.Rate[nCount].spread < nPos; m_Infos.CountReplay = m_MemoryData.Rate[nCount++].spread); 147. if (nCount > 0) CustomRatesUpdate(def_SymbolReplay, m_MemoryData.Rate, nCount - 1); 148. while ((nPos > m_Infos.CountReplay) && def_CheckLoopService) 149. CreateBarInReplay(false); 150. } 151. //+------------------------------------------------------------------+ 152. public : 153. //+------------------------------------------------------------------+ 154. C_Replay() 155. :C_ConfigService() 156. { 157. Print("************** Market Replay Service **************"); 158. srand(GetTickCount()); 159. SymbolSelect(def_SymbolReplay, false); 160. CustomSymbolDelete(def_SymbolReplay); 161. CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay)); 162. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0); 163. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, 0); 164. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, 0); 165. CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation"); 166. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, 8); 167. SymbolSelect(def_SymbolReplay, true); 168. m_Infos.CountReplay = 0; 169. m_IndControl.Handle = INVALID_HANDLE; 170. m_IndControl.Mode = C_Controls::ePause; 171. m_IndControl.Position = 0; 172. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = C_Controls::eTriState; 173. } 174. //+------------------------------------------------------------------+ 175. ~C_Replay() 176. { 177. SweepAndCloseChart(); 178. IndicatorRelease(m_IndControl.Handle); 179. SymbolSelect(def_SymbolReplay, false); 180. CustomSymbolDelete(def_SymbolReplay); 181. Print("Finished replay service..."); 182. } 183. //+------------------------------------------------------------------+ 184. bool OpenChartReplay(const ENUM_TIMEFRAMES arg1, const string szNameTemplate) 185. { 186. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == 0) 187. return MsgError("Asset configuration is not complete, it remains to declare the size of the ticket."); 188. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == 0) 189. return MsgError("Asset configuration is not complete, need to declare the ticket value."); 190. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == 0) 191. return MsgError("Asset configuration not complete, need to declare the minimum volume."); 192. SweepAndCloseChart(); 193. m_Infos.IdReplay = ChartOpen(def_SymbolReplay, arg1); 194. if (!ChartApplyTemplate(m_Infos.IdReplay, szNameTemplate + ".tpl")) 195. Print("Failed apply template: ", szNameTemplate, ".tpl Using template default.tpl"); 196. else 197. Print("Apply template: ", szNameTemplate, ".tpl"); 198. 199. return true; 200. } 201. //+------------------------------------------------------------------+ 202. bool InitBaseControl(const ushort wait = 1000) 203. { 204. Print("Waiting for Mouse Indicator..."); 205. Sleep(wait); 206. while ((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, 0, "Indicator Mouse Study") == INVALID_HANDLE)) Sleep(200); 207. if (def_CheckLoopService) 208. { 209. AdjustViewDetails(); 210. Print("Waiting for Control Indicator..."); 211. if ((m_IndControl.Handle = iCustom(ChartSymbol(m_Infos.IdReplay), ChartPeriod(m_Infos.IdReplay), "::" + def_IndicatorControl, m_Infos.IdReplay)) == INVALID_HANDLE) return false; 212. ChartIndicatorAdd(m_Infos.IdReplay, 0, m_IndControl.Handle); 213. UpdateIndicatorControl(); 214. } 215. 216. return def_CheckLoopService; 217. } 218. //+------------------------------------------------------------------+ 219. bool LoopEventOnTime(void) 220. { 221. int iPos, iCycles; 222. 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. while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService)) 232. { 233. if (m_IndControl.Mode == C_Controls::ePause) return true; 234. 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); 235. CreateBarInReplay(true); 236. while ((iPos > 200) && (def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePause)) 237. { 238. Sleep(195); 239. iPos -= 200; 240. m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks); 241. UpdateIndicatorControl(); 242. iCycles = (iCycles == 4 ? RateUpdate(false) : iCycles + 1); 243. } 244. } 245. 246. return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService)); 247. } 248. }; 249. //+------------------------------------------------------------------+ 250. #undef def_SymbolReplay 251. #undef def_CheckLoopService 252. #undef def_MaxSlider 253. //+------------------------------------------------------------------+
C_Replay.mqh 文件的源代码
现在让我们来解释一下这个更新的代码是如何工作的。我们将从最后开始,该服务之前在低流动性条件下表现得很奇怪。注意,在第 236 行进行了校正。代码不再等待检测到的长时间延迟,否则系统将对用户无响应。在这种情况下,我们所要做的就是添加一个检查,以确定用户是否已暂停系统。如果是,则退出循环,当执行到第 233 行时,函数终止,返回主控制流。然后,主逻辑对函数进行新的调用并再次等待。然而,这一次,它在第 223 行循环,这允许用户重新定位控制指标并移动到不同的时间点。这为我们提供了更加流畅的体验,尤其是当资产流动性较低或进入竞价阶段时。仅通过查看这个 LoopEventOnTime 例程,您可能无法完全理解我的意思。但随着解释的展开,事情会变得更加清晰。
让我们探索一下即使在分时报价活动稀少的情况下也能提供剩余时间反馈的改变。在第 221 行添加了一个新变量,并在第 230 行进行了初始化。现在请注意第 242 行。我们使用相同的变量从 0 计数到 4。当值达到 4 时,我们调用 RateUpdate 函数。但是 RateUpdate 是什么?别担心,我们会明白这一点。现在,请注意,函数是用参数 false 调用的,它的返回值被分配给变量。这个细节很重要。还记得在文章的前面,我提到我们每秒大约有五个周期吗?这就是我们设立这个计数器的原因。其目的是让鼠标指标感觉到一秒钟已经过去了。但请记住:这只是一个近似值。我们无法完美精确地安排时间。目标不是严格的准确性,而是让用户大致了解距离关闭还有多长时间。
现在让我们转到代码的另一部分,即从第 80 行开始的过程。在这里,被删除的行被替换为对 RateUpdate 的调用。然而,这一次,所传递的参数是 true。如果我们要添加新的分时报价,则该参数应该为 true。如果我们只是更新时间(没有收到分时报价),那么这个参数应该是 false。有趣吧?现在让我们看一下 RateUpdate 过程本身,它从第 69 行开始。
必须创建 RateUpdate 函数,因为更新时间直接会带来意外跳过一些分时报价的风险。这与第 110 行相关。为了避免这种情况,我们将时间更新逻辑移动到了它自己的函数中。您会注意到之前在第 85 行声明的变量已被移至第 71 行。同样,之前在第 108 行和第 109 行完成的工作现在在第 74 行和第 75 行处理。本质上,此函数几乎是我们之前函数的复制品。区别在于第 73 行,但请注意,此函数始终返回零。这是有意的,基于我们在第 242 行的预期。
但让我们回到第 73 行的问题。这一行的作用相当有趣。你看,时间不需要精确,它只需要相当接近即可。当真实数据出现分时报价时,它由 CreateBarInReplay 处理。在这种情况下,st_Spread 值将反映该分时报价的时间戳。但是当调用来自 LoopEventOnTime 时,st_Spread 只会增加一。这相当于一秒钟的步伐。无论 st_Spread 值是多少,只要下一个真实报价到来,它就会被修正并重新与实时值保持一致。因此,如果流动性下降,并且分时报价之间有 50 秒的延迟,计时器可能会稍微领先或滞后。您会看到鼠标指示器显示一个值,然后显示一个略有不同的值,不一定相差一秒。这不是一个错误。事实上,它确实带来了一点好处。如果流动性在几秒钟内枯竭,您可以简单地暂停并立即恢复服务。因此,该系统将有效地跳过漫长的等待期。很有趣,不是吗?
最后的探讨
为了更清楚地了解我刚才解释的所有内容,您可以对低流动性资产使用模拟/回放分时报价。但即使您不这样做,下面的视频也演示了暂停播放技巧的实际操作,让您可以跳过漫长的等待时间。
但是,我们还需要解决一个问题:鼠标指标如何告诉我们资产何时进入竞价模式?这是一个棘手的话题,足够复杂,值得写一篇专门的文章。是的,解决方案已经在鼠标指标中实现。如果您将其放在实时图表上并跟踪具有实时数据的资产,您会看到当资产进入竞价时,指标会清楚地显示这一点。但在我们的回放/模拟器中,我们使用自定义资产,这成为一个挑战。这里有一个具体问题,使我们的事情变得复杂。亲爱的读者,这将是我们下一篇文章的主题。期待很快与您见面!
演示视频
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/12317
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。


