
リプレイシステムの開発(第69回):正しい時間を知る(II)
はじめに
前回の「リプレイシステムの開発(第68回):正しい時間を知る(I)」では、マウスインジケーターに関するコード部分について説明しました。しかしながら、そのコード単体ではあまり意味を成しません。リプレイ/シミュレーターサービスのコードも併せて確認する必要があります。そのため、もし前回の記事をまだ読んでいない場合は、本記事を理解する前にそちらをお読みいただくことをお勧めします。両者は互いに補完し合う内容となっています。
ここでの主な焦点は、流動性が低い状態の資産において、バーの残り時間に関する情報をどのように提供するかという点です。このような状況下では、通常発生するはずのOnCalculateイベントが生成されないため、マウスインジケーターが経過秒数に対応した正しい値を受け取れなくなることがあります。しかしながら、前回の記事で触れた内容に基づけば、インジケーターが残り秒数を計算できるように、必要な値を渡すことは可能です。
本稿では、主にリプレイ/シミュレーターサービスに焦点を当てていきます。具体的には、C_Replay.mqhというファイルに注目し、どのような修正や追加が必要かを確認していきましょう。
ファイルC_Replay.mqhの調整
修正が必要な箇所は多くありません。しかし、これらの変更は、前回の記事で説明したコード、特にOnCalculateイベント内でiSpreadライブラリ関数を使用している部分に適切な文脈を与える重要なものです。特に、OnCalculate関数に渡される配列からスプレッドの値を直接取得したほうが簡単そうに思えるため、なぜあえてiSpread関数を使ったのかを疑問に感じた方もいるかもしれません。
確かに、非常に興味深いポイントです。ですがその理由を理解するためには、内部で実際に何が起きているのかを正しく把握する必要があります。そのためには、リプレイ/シミュレーターサービスのコードがどのように動作しているかを詳しく見ていく必要があります。もちろん、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行目から始まるループ内の各反復で、229行目においてCreateBarInReplayが呼び出されていることにも注目してください。ここで重要な点は、このCreateBarInReplay関数が約195ミリ秒ごとに実行されているということです。これは、232行目と、225行目から始まるループの処理時間に基づいています。反復の間に遅延がなければ、1秒あたりおよそ5回呼び出される計算になります。ここでは、高流動性のシナリオについては一旦忘れてください。流動性が極めて低い場合に、リプレイ/シミュレーターサービスが実際にどう動作しているのかを説明することが目的です。したがって、「CreateBarInReplay関数は1秒あたり約5回呼び出される」というこの数値を念頭に置いてください。
次に、流動性が十分な場合(1秒あたり少なくとも5回の呼び出しがある場合)におけるCreateBarInReplayプロシージャの動作を見ていきましょう。
このケースでは、76行目の条件がtrueと評価されるため、77行目から100行目までのコードブロックが実行されます。ただしこの範囲内の一部の行は取り消し線で示されており、コードから削除されていることに注意してください。その中には、新しい1分バーごとにカスタムイベントを発生させていた95行目も含まれています。この点は、なぜOnCalculateプロシージャでiSpread関数が使用されるのかを説明する上で重要になります。ですが、現時点ではこの点に深入りせず、まずは基礎を理解することに集中しましょう。なお、97行目には新たに変数を初期化するコードが追加されています。
ここで注目すべき点があります。98行目と99行目に取り消し線が引かれていますが、それらの処理ロジックは破棄されたわけではありません。単に再配置されただけです。元々は、76行目の条件がtrueの場合のみ実行されるブロック内にありましたが、現在では101行目と102行目で無条件に実行されるようになっています。ここでの違いとして、101行目は内容こそ異なりますが、98行目と同じ処理を行っています。違いは、ビットマスクの使用にあります。これにより、マウスインジケーターがスプレッド値がリプレイ/シミュレーターサービスによるものであると認識できるようになっています。具体的には、OR演算を使ってマスクを適切に設定しているだけですが、ここには潜在的な問題が潜んでいます。それは、st_Spread変数の値がマスク領域に侵入してしまうと、マウスインジケーターがその値を正しく解釈できなくなるという点です。
したがって、何かがおかしいと感じたり、動作に問題が生じた場合は、まずst_Spreadの値がビットマスクの領域を超えていないか確認してください。通常の条件下では、リプレイ/シミュレーターは日中足の分析を目的として設計されているため、このような問題は発生しません。しかし、リプレイ/シミュレーターサービスがタイムリミットいっぱいまで使用された場合には、そうした状態になる可能性があります。ちなみに、その時間的上限は約12日分の秒数に相当し、私たちの用途に対しては十分すぎる容量となっています。
このシステムの仕組みをさらに理解するために、前回の記事でコンパイルしたマウスインジケーターとともに、リプレイ/シミュレーターサービスをコンパイル・実行してみてください。資産に十分な流動性がある場合(1秒に少なくとも1ティックがある)、現在のバーが終了し、次のバーが始まるまでの残り時間が正確に更新されることが確認できるはずです。
ここまでの説明で、なぜ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の設定が削除されました。これにより、現在表示されている情報は、「次のバーが出現する予定の時刻そのもの」になります。ここで重要なのは、「次のバーまでの残り時間」ではなく、「次のバーの出現が見込まれる正確な時刻」であるという点です。この変更を加えた後、マウスインジケーターを再コンパイルして、実際にどのような出力が表示されるのかを確認します。以下のアニメーションで、その実際の挙動を確認することができます。
使用しているチャートの時間足は2分足であることに注意してください。現在おこなっている計算は、次のバーが出現する正確な時刻を示しています。この情報がマウスインジケーターに表示されているわけです。チャートの時間が指定された時刻に達すると、インジケーターはすぐに新しいバーの出現時刻を報告し始めます。言い換えれば、システムは想定どおりに動作しているということです。ただし、ここまでのテストで確認できたのは、あらかじめ想定していた情報が正しく表示されているという点だけであり、リプレイ/シミュレーションサービスが実際にどのような値を提供しているかまでは、まだ検証できていません。ここで、サービスが渡す実際の値を確認する必要があります。この点を検証するために重要なのは、チャートの時間足が1分に設定されていないことです。1分足ではテストが無効になります。したがって、時間足は2分足のままにしておく必要があります。この設定で、現在の挙動を分析するには十分です。
テストを正しく実行するために、ここでもうひとつ小さな変更を加える必要があります。次に示すコード断片に、もう一度注意深く目を通してください。
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. //+------------------------------------------------------------------+
マウスポインタファイルフラグメント
上記のコード断片で何をしているのか、よく観察してください。取り消し線が引かれている行は、前回の記事で使用されていた現在のバージョンのコードです。これらの行は一時的に削除しています。その代わりに、新たに追加した行は、MetaTrader 5がOnCalculate関数を通じて提供するデータを使用するようになっています。つまり、もはやiSpread関数の呼び出しに依存せず、OnCalculateに渡されるspread配列の値を使っているわけです。サービスとの互換性を保つために、54行目で少し調整を加えています。ここでおこなっている操作は、以前iSpreadでおこなっていた処理と同じですが、今回はOnCalculateの引数から直接値を取得している点が異なります。この変更は重要な意味を持ちます。なぜなら、MetaTrader 5が既に提供している値を取得するために余分な関数呼び出しをする必要がなくなるからです。
では、この更新されたコードを実行した結果を見てみましょう。以下のアニメーションにその様子が示されています。
さて、ここで一体何が起こったのでしょうか。なぜマウスインジケーターの値がフリーズしてしまったのでしょう。この答えは簡単ではありません。多くの人が想像するのとは逆に、正直なところ私もはっきりとした答えは知りません。なぜ私が知らないのでしょう。もちろん多少の推測はあります。ですが、単なる憶測をするよりも、思いもよらないことが実際に起こるかもしれないという事実をお見せしたいのです。そうすれば、皆さん自身で観察し、自分なりの結論を導き出せるからです。
それはさておき、サービスは前回のアニメーションと同じように値を出力し続けていますし、インジケーターもスプレッド値を正しく取得しています。では、なぜ値がフリーズしたのか。正直なところ説明できません。ただ分かっているのは、新しいバーがチャートに出現したタイミングで(だからこそ1分足以外の時間枠を使うことが重要なのです)、spread配列の値は正しく更新される、ということだけです。
もう一度お伝えしますが、ここで示している現象は、皆さんがこの記事を読む時点では既に修正されている可能性があります。なぜなら、MetaTrader 5はこの問題を修正するアップデートを受け取る可能性が高いからです。それまでは、この小さな問題を回避するためにiSpread関数を使い続けています。問題が修正されたらiSpreadは使わず、MetaTrader 5がOnCalculateに直接渡す値をそのまま使う予定です。ですので、コードの一部に固執しないでください。開発が進むにつれて改善されていきます。これでなぜ私がOnCalculateの引数として渡されるスプレッド値ではなく、iSpreadを使い続けているかがご理解いただけたと思います。ただ、まだやるべきことは残っています。流動性が低くて毎秒ティック(正確にはOnCalculateイベント)を受信できない場合に、サービス側がバーの残り時間を正確に知らせる方法を考えなければなりません。そのためには、今回示したiSpreadを使う理由を説明した部分の変更を元に戻し、サービスの開発に戻ることにします。
サービスの欠陥を修正する
残念ながら、ティック間の時間が1秒を超える場合でもすべてが正常に動作させるには、この記事の冒頭で計画していたものとは少し異なるアプローチを取る必要があります。問題の原因は、バー作成ルーチン内にカウンタを置くつもりだったことにありますが、重要な点を見落としていました。それは「時間」です。この記事の冒頭に戻り、ヘッダーファイルC_Replay.mqhのソースコードを確認してください。230行目付近に、次のティックが現れるまで適切な時間が経過するのをサービスが待つためのループがあります。ここに問題が潜んでいます。
開発やテスト時には流動性の高い資産、つまりティック間の時間が通常1秒未満の履歴データを使っていました。ティック間隔が長くなる可能性をサポートする変更を始めると、欠陥が明らかになりました。これは突然起きた問題ではなく、比較的短いティック間隔に隠れていた問題です。230行目から236行目の間のループに注目してください。何が問題でしょうか。問題は、ユーザーがシステムを一時停止する可能性を考慮していない点です。どういうことかというと、サービスがループ内で次のティックを待っている間は正常に見えますが、待ち時間が1秒を超えると問題が発生します。
たとえば、Forexデータの再生をしているとします。日中のセッション開始時などはティック間隔が非常に長くなることがあります。再生ボタンを押してサービスが「次のティックまで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を渡して関数を呼び出しており、その戻り値が同じ変数に再代入されている点に注目してください。これは重要なポイントです。記事の前半で「1秒あたり約5サイクルになる」と述べましたが、このカウンタはそのための仕組みです。目標は、マウスインジケーターに「1秒が経過した」という感覚を与えることにあります。ただし、これはあくまで近似的なものであり、厳密な時間計測ではありません。目的は厳密な精度ではなく、バーのクローズまでにどれくらいの時間が残っているかをおおよそユーザーに伝えることです。
次に、コードの別の部分、具体的には、80行目から始まるルーチンを見てみましょう。ここでは、取り消し線が引かれた古いコードがRateUpdateの呼び出しに置き換えられています。今回は引数にtrueを渡しています。これは新しいティックを追加する場合です。一方、ティックを受信せずに時間だけを更新する場合には、引数はfalseにします。少し面白くなってきました。それでは、69行目から始まるRateUpdate関数を詳しく見てみましょう。
この関数は、時間の更新処理を独立させるために新たに作成されました。なぜなら、時間を直接更新すると、誤ってティックをスキップしてしまうリスクがあったからです(これは110行目に関係しています)。その回避策として、時間更新の処理をRateUpdate関数に切り出しました。かつて85行目にあった変数宣言は71行目に移され、108〜109行目にあった処理は現在74〜75行目でおこなわれています。この関数は、かつて存在していたロジックのほぼコピーですが、決定的に違うのは73行目です。そして、関数は常に0を返すようになっています。これは242行目での利用を前提とした意図的な設計です。
さて、73行目に戻りましょう。この行では、時刻の正確さを重視せず、「ある程度近ければよい」という思想で時間を進めています。実際のデータからティックが到来する場合は、CreateBarInReplayによって処理され、st_Spreadのタイムスタンプは正確なものになります。しかし、LoopEventOnTimeから呼び出された場合は、st_Spreadの値を単純に1秒分インクリメントします(つまり、1秒のステップで進行)。実際のティックが次に到達すれば、そのときに正しい時刻で上書きされ、補正される仕組みです。つまり、たとえ流動性が低下してティック間に50秒の空白が生じたとしても、タイマーはわずかに進んだり遅れたりするだけで、マウスインジケーターはそれなりに時間の流れを感じさせる表示を続けることができます。値が1秒単位で正確に変化しないこともありますが、それはバグではありません。むしろ、これは便利な機能とすら言えます。数秒間ティックが途絶えても、「一時停止」してすぐに「再開」すれば、システムは長い待機時間をスキップして進行することができるのです。面白いと思いませんか。
最後に
ここまでの説明をより明確に理解したい場合は、流動性の低い資産を使ってシミュレーション/リプレイティックを実行してみるのが効果的です。ただし、実際に試さなくても大丈夫です。以下のビデオでは、一時停止と再生を切り替えることで、長い待機時間をスキップできるテクニックを実演しています。
とはいえ、まだ取り上げていない問題がひとつ残っています。それは、「マウスインジケーターは、資産が板寄せモードに入ったことをどうやってユーザーに知らせるのか」という点です。これは少々厄介なテーマで、1つの記事として独立させる価値があるほど複雑です。ただし、朗報があります。この機能はすでにマウスインジケーターに実装済みです。ライブチャートにインジケーターを適用し、リアルタイムで動いている資産を追ってみてください。資産が板寄せに入ると、その状態がインジケーター上に明確に表示されます。ですが、ここで問題になるのが、リプレイ/シミュレーターでカスタムアセットを使用している場合です。このケースでは、事情が少し複雑になります。そして、この特有の課題こそが、次回の記事で取り上げるテーマになります。それではまた、お会いしましょう。
デモビデオ
MetaQuotes Ltdによりポルトガル語から翻訳されました。
元の記事: https://www.mql5.com/pt/articles/12317





- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索