
リプレイシステムの開発(第65回)サービスの再生(VI)
はじめに
前の記事「リプレイシステムの開発(第64回):サービスの再生(V)」では、リプレイ/シミュレーションアプリケーションに存在していた2つのバグを修正しました。しかし、すべてが完全に解決されたわけではありません。少なくとも、この記事で新たな展開を進められるほどには至っていません。システムに影響を与え続けているいくつかの小さな問題が依然として残っています。これらの問題は、グローバル端末変数を使用していたころには存在しませんでした。しかし、私たちはそのアプローチを離れ、リプレイ/シミュレーションアプリケーションを機能させるために、新しい技術や手法を取り入れてきました。したがって、現在はそれらに適応し、新しい実装を構築していく必要があります。とはいえ、読者の皆様はお気づきかと思いますが、私たちはゼロから始めているわけではありません。実際には、グローバル端末変数を用いて以前におこなっていた処理が完全に失われないよう、既存のコードを調整・改良しながら対応しています。
その結果、機能面では以前とほぼ同じレベルにまで回復しています。しかし、その水準に完全に到達するには、まだいくつかの細かい問題を解決しなければなりません。この記事では、それら比較的単純な問題の解決に取り組んでいきます。これは、以前取り上げたメモリダンプに関するバグとは異なります。あの問題は非常に複雑で、コードが一見正しく見えていても、なぜバグが発生したのかを詳細に説明する必要がありました。追加する必要のある行を単に示すだけでは不十分であり、多くの読者が混乱したことでしょう。逆に、何の説明もなくコードを修正するだけでは、同様にがっかりされていたと思います。そうした問題に直面したことがなければ、誤った安心感を抱いてしまう可能性があります。そして、明確な指針がないまま同じような問題に直面すると、苛立ちや不安を感じることでしょう。さらに悪いことに、プロフェッショナルとしての自分の能力に疑問を抱くようになってしまうかもしれません。私はそれを避けたいのです。熟練したプロでもミスをすることはあります。普段は目にしないかもしれませんが、これは実際に起こっていることです。彼らが他の人と違うのは、そのような問題を素早く見つけ、修正できる能力を持っている点です。だからこそ、私は開発者を目指すすべての方に、真のプロフェッショナルとして成長してほしいと願っています。そして、ただのプロフェッショナルではなく、それぞれの分野で傑出した存在となってほしいと考えています。それを念頭に置いて、残された課題のうち、まず最初の問題に取り組んでいきましょう。
早送り機能の追加(基本モデル)
この機能は以前にも存在しており、グローバル端末変数に依存していた時期に実装されていました。現在はこれらの変数を使用していないため、早送り機能を再導入するには、コードを適応させる必要があります。今回は、以前と同じ早送りロジックをそのまま使用します。これにより、従来の実装がどのように新しいシステムに適応されているのかを、より分かりやすく理解できるはずです。
はじめに、前回の記事で紹介したコードに対して、少しだけ修正を加える必要があります。この変更によって、コントロールインジケーターが正しく機能するようになります。以下にその変更点を示します。変更内容は以下から確認できます。
35. //+------------------------------------------------------------------+ 36. inline void UpdateIndicatorControl(void) 37. { 38. double Buff[]; 39. 40. if (m_IndControl.Handle == INVALID_HANDLE) return; 41. if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position) 42. { 43. if (CopyBuffer(m_IndControl.Handle, 0, 0, 1, Buff) == 1) 44. m_IndControl.Memory.dValue = Buff[0]; 45. if ((C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus] != C_Controls::eTriState) 46. if ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay) 47. m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition]; 48. }else if (m_IndControl.Mode == C_Controls::ePause) 49. { 50. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position; 51. m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (ushort)m_IndControl.Mode; 52. m_IndControl.Memory._8b[7] = 'D'; 53. m_IndControl.Memory._8b[6] = 'M'; 54. EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, 0, m_IndControl.Memory.dValue, ""); 55. } 56. } 57. //+------------------------------------------------------------------+
ファイルC_Replay.mqhからのコードスニペット
変更(より正確には追加)は、具体的には48行目におこなわれました。しかし、なぜこの変更が必要だったのでしょうか。その理由は、LoopEventOnTime関数の内部にあります。これは少し分かりにくいかもしれません。LoopEventOnTimeの中に問題があるのなら、なぜUpdateIndicatorControlプロシージャの方を変更する必要があるのでしょうか。これは意味を成しません。実際、LoopEventOnTime関数がメッセージを送受信してコントロールインジケーターの読み取りと書き込みをおこなっていなければ、これには意味がありません。もし48行目にある条件チェックが存在しなければ、早送り後にすぐ再生ボタンを押した際に、通常では起こらない挙動が発生します。これは、まだ実際の早送りロジックを実装していない段階でも起こり得ます。
たとえば、早送りしてから再生を押すと、サービスに一時停止コマンドを送ることができなくなります。そんな馬鹿な、と思うかもしれません。一時停止ボタンを押せば、当然、サービスに停止指示が送られるはずです。実際には一時停止の更新コマンド自体は送信されているのですが、その効果が即座に現れないのです。なぜでしょう。その原因は41行目にあります。問題は、コントロールインジケーターのバッファと参照しているメモリ領域が同期していないことにあります。サービスを起動し、再生ボタンを押してから時間を早送りする限りは、表面的には何も問題は起こらないように見えます。しかし、サービスを一時停止し、48行目のチェックがない状態で早送りを行うと、インジケーターのロックバーが早送りに追従して動いてしまいます。これにより、ユーザーが手動で位置を調整できなくなるという問題が発生します。
さらに、リプレイ/シミュレーションサービスを開始し、1ステップだけ進めてから再生ボタンを押した場合、その後に一時停止ボタンを押しても、サービスを停止させることができません。サービスが止まるのは、41行目の条件がtrueになり、バッファが一時停止モードを示すようになってからです。これには、かなり時間がかかることもあります。少し説明が複雑に感じられるかもしれませんが、それもそのはずで、この問題には3つの異なるシナリオが絡んでいます。それぞれが、LoopEventOnTime関数がリプレイ/シミュレーションの実行中に常にコントロールインジケーターとメッセージのやり取りをしているという仕様に起因して、固有の課題を抱えているのです。
しかし、スニペットに示されているように、48行目に1つの条件チェックを加えるだけで、これらすべての問題は解消されます。LoopEventOnTime関数に起因する不具合が取り除かれ、早送り機能の実装に専念できるようになります。
なお、早送り機能の実装自体は、それほど複雑な作業ではありません。実際には、以下のコードスニペットを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. //+------------------------------------------------------------------+ 014. class C_Replay : public C_ConfigService 015. { 016. private : ... 035. //+------------------------------------------------------------------+ 036. inline void UpdateIndicatorControl(void) 037. { 038. double Buff[]; 039. 040. if (m_IndControl.Handle == INVALID_HANDLE) return; 041. if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position) 042. { 043. if (CopyBuffer(m_IndControl.Handle, 0, 0, 1, Buff) == 1) 044. m_IndControl.Memory.dValue = Buff[0]; 045. if ((C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus] != C_Controls::eTriState) 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 if (m_IndControl.Mode == C_Controls::ePause) 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. //+------------------------------------------------------------------+ ... 068. //+------------------------------------------------------------------+ 069. inline void CreateBarInReplay(bool bViewTick) 070. { 071. bool bNew; 072. double dSpread; 073. int iRand = rand(); 074. 075. if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[0], bNew)) 076. { 077. m_Infos.tick[0] = m_MemoryData.Info[m_Infos.CountReplay]; 078. if (m_MemoryData.ModePlot == PRICE_EXCHANGE) 079. { 080. dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 ); 081. if (m_Infos.tick[0].last > m_Infos.tick[0].ask) 082. { 083. m_Infos.tick[0].ask = m_Infos.tick[0].last; 084. m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread; 085. }else if (m_Infos.tick[0].last < m_Infos.tick[0].bid) 086. { 087. m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread; 088. m_Infos.tick[0].bid = m_Infos.tick[0].last; 089. } 090. } 091. if (bViewTick) CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 092. CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate); 093. } 094. m_Infos.CountReplay++; 095. } 096. //+------------------------------------------------------------------+ ... 123. //+------------------------------------------------------------------+ 124. void AdjustPositionToReplay(void) 125. { 126. int nPos; 127. 128. if (m_IndControl.Position == (int)((m_Infos.CountReplay * def_MaxPosSlider * 1.0) / m_MemoryData.nTicks)) return; 129. nPos = (int)(m_MemoryData.nTicks * ((m_IndControl.Position * 1.0) / (def_MaxPosSlider + 1))); 130. while ((nPos > m_Infos.CountReplay) && def_CheckLoopService) 131. CreateBarInReplay(false); 132. } 133. //+------------------------------------------------------------------+ 134. public : 135. //+------------------------------------------------------------------+ ... 200. //+------------------------------------------------------------------+ 201. bool LoopEventOnTime(void) 202. { 203. int iPos; 204. 205. while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay)) 206. { 207. UpdateIndicatorControl(); 208. Sleep(200); 209. } 210. m_MemoryData = GetInfoTicks(); 211. AdjustPositionToReplay(); 212. iPos = 0; 213. while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService)) 214. { 215. if (m_IndControl.Mode == C_Controls::ePause) return true; 216. 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); 217. CreateBarInReplay(true); 218. while ((iPos > 200) && (def_CheckLoopService)) 219. { 220. Sleep(195); 221. iPos -= 200; 222. m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxPosSlider) / m_MemoryData.nTicks); 223. UpdateIndicatorControl(); 224. } 225. } 226. 227. return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService)); 228. } 229. //+------------------------------------------------------------------+ 230. }; 231. //+------------------------------------------------------------------+ 232. #undef macroRemoveSec 233. #undef def_SymbolReplay 234. #undef def_CheckLoopService 235. //+------------------------------------------------------------------+
ファイルC_Replay.mqhからのコードスニペット
上記のC_Replay.mqhファイルのコードスニペットに示されているように、基本的な早送り機能を実装するために必要な要素はすべて明確に確認できます。「基本的」と表現したのは、まだ説明が必要な細かな問題がいくつか残っているためです。ただし、このシンプルなアプローチは、これから構築していくより高度な実装の基礎となるため、しっかりと理解しておくことが重要です。以前に言及した48行目の条件を思い出してください。続いて207行目を見てみましょう。この行は、48行目の条件が存在しない場合にコントロールインジケーターに不具合を引き起こします。48行目と211行目のチェックを両方無効にすると、その問題が確認できます。ただし、現在のバージョンではすでにコントロールインジケーターが正しく機能しているため、これらのチェックはそのままにしておきましょう。ここでは、この基本モデルにおける早送りの仕組みに焦点を当てて解説します。
サービスコードがLoopEventOnTime関数を呼び出すと、再生は一時停止モードに入ります。この状態では、205行目から209行目までのループが連続的に実行され、ユーザーはコントロールインジケーターの位置を調整できます。そして、ユーザーがリプレイ/シミュレーターインターフェイスの再生ボタンを押すと、210行目でアセットデータがキャプチャされ、高速アクセスの準備が整います。続いて、211行目で124行目に定義された手続きが呼び出され、現在の位置からユーザーが指定した位置まで早送りをおこないます。
128行目では、要求された位置が現在の位置と一致するかを確認します。一致していれば処理はそこで終了し、213行目から225行目のループに進みます。もし目的の位置が先であれば、129行目で目標オフセットが計算されます。ここまでは特に難しい処理ではありませんが、実際の「トリック」は130行目から始まります。ここで、131行目を繰り返し呼び出すループに入り、バーを生成する手続きが実行されます。この処理は、位置カウンタが目標オフセットに到達するまで、可能な限り高速で進みます。131行目で呼び出される手続きは、69行目から95行目までのコードを実行します。このとき、falseパラメータを渡しているため、91行目の条件によってCustomTicksAdd関数は呼び出されません。つまり、ティックデータは銘柄にプッシュされませんが、92行目によりバーは1本ずつチャート上に描画されていきます。
全体的に、この処理は非常にうまく機能しますが、早送り中に明らかな遅延を引き起こす要因が2つあります。ユーザーが大きなスキップを要求すると、そのバーが1本ずつ描画される様子が視認できるほどです。この遅延の主な原因は、75行目と92行目にあります。プロシージャ呼び出し自体のオーバーヘッドは比較的小さく、無視できますが、特に75行目は遅延に大きく影響しています。
これが、基本的な早送り機能の実装方法です。しかし、それよりもかなり速い代替手段があります。バーの生成過程を視覚的に確認したいのであれば、このより基本的な方法でも十分に対応できます。C_Replay.mqhファイルを編集し、ここで紹介したコードを組み込むことで、基本的な早送り機能はすぐに利用可能になります。この方法を使うか、それとも次に紹介する、より高度で高速な実装を選ぶかはあなた次第です。それでは、情報の整理のため、次のセクションに進みましょう。
早送り機能の追加(動的モデル)
コードを詳しく見ると、C_FileTicksクラスがティック読み込み中にすでに1分足を生成していることがわかります。では、すでに作成されたものを再構築するのになぜ時間を無駄にするのでしょうか。代わりに、事前に構築されたバーを活用して、ターゲットポイントに可能な限り近づきます。必要であれば、計算された正確な位置まで早送りすることができます。これにより、早送りのプロセスがほぼ瞬時に感じられるようになります。
もちろん、すべてが最初から完璧に機能するわけではありません。期待される効率を実現するためには、迅速な検索のためのインデックスや参照リンクを導入する必要があります。幸いなことに、リプレイ/シミュレーションシステムの文脈では特に役立っていないデータ構造の一部を再利用できます。それが、MqlRates構造体内のSpreadフィールドです。この変更を理解するために、次のコードスニペットをご覧ください。
126. //+------------------------------------------------------------------+ 127. bool BarsToTicks(const string szFileNameCSV, int MaxTickVolume) 128. { 129. C_FileBars *pFileBars; 130. C_Simulation *pSimulator = NULL; 131. int iMem = m_Ticks.nTicks, 132. iRet = -1; 133. MqlRates rate[1]; 134. MqlTick local[]; 135. bool bInit = false; 136. 137. pFileBars = new C_FileBars(szFileNameCSV); 138. ArrayResize(local, def_MaxSizeArray); 139. Print("Converting bars to ticks. Please wait..."); 140. while ((*pFileBars).ReadBar(rate) && (!_StopFlag)) 141. { 142. if (!bInit) 143. { 144. m_Ticks.ModePlot = (rate[0].real_volume > 0 ? PRICE_EXCHANGE : PRICE_FOREX); 145. pSimulator = new C_Simulation(SetSymbolInfos()); 146. bInit = true; 147. } 148. ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 3 : def_BarsDiary), def_BarsDiary); 149. m_Ticks.Rate[++m_Ticks.nRate] = rate[0]; 150. if (pSimulator == NULL) iRet = -1; else iRet = (*pSimulator).Simulation(rate[0], local, MaxTickVolume); 151. if (iRet < 0) break; 152. rate[0].spread = m_Ticks.nTicks; 153. for (int c0 = 0; c0 <= iRet; c0++) 154. { 155. ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray); 156. m_Ticks.Info[m_Ticks.nTicks++] = local[c0]; 157. } 158. m_Ticks.Rate[++m_Ticks.nRate] = rate[0]; 159. } 160. ArrayFree(local); 161. delete pFileBars; 162. delete pSimulator; 163. m_Ticks.bTickReal = false; 164. 165. return ((!_StopFlag) && (iMem != m_Ticks.nTicks) && (iRet > 0)); 166. } 167. //+------------------------------------------------------------------+ 168. datetime LoadTicks(const string szFileNameCSV, const bool ToReplay, const int MaxTickVolume) 169. { 170. int MemNRates, 171. MemNTicks, 172. nDigits, 173. nShift; 174. datetime dtRet = TimeCurrent(); 175. MqlRates RatesLocal[], 176. rate; 177. MqlTick TicksLocal[]; 178. bool bNew; 179. 180. MemNRates = (m_Ticks.nRate < 0 ? 0 : m_Ticks.nRate); 181. nShift = MemNTicks = m_Ticks.nTicks; 182. if (!Open(szFileNameCSV)) return 0; 183. if (!ReadAllsTicks()) return 0; 184. rate.time = 0; 185. nDigits = SetSymbolInfos(); 186. m_Ticks.bTickReal = true; 187. for (int c0 = MemNTicks, c1, MemShift = nShift; c0 < m_Ticks.nTicks; c0++, nShift++) 188. { 189. if (nShift != c0) m_Ticks.Info[nShift] = m_Ticks.Info[c0]; 190. if (!BuildBar1Min(c0, rate, bNew)) continue; 191. if (bNew) 192. { 193. if ((m_Ticks.nRate >= 0) && (ToReplay)) if (m_Ticks.Rate[m_Ticks.nRate].tick_volume > MaxTickVolume) 194. { 195. nShift = MemShift; 196. ArrayResize(TicksLocal, def_MaxSizeArray); 197. C_Simulation *pSimulator = new C_Simulation(nDigits); 198. if ((c1 = (*pSimulator).Simulation(m_Ticks.Rate[m_Ticks.nRate], TicksLocal, MaxTickVolume)) > 0) 199. nShift += ArrayCopy(m_Ticks.Info, TicksLocal, nShift, 0, c1); 200. delete pSimulator; 201. ArrayFree(TicksLocal); 202. if (c1 < 0) return 0; 203. } 204. rate.spread = MemShift; 205. MemShift = nShift; 206. ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 2 : def_BarsDiary), def_BarsDiary); 207. }; 208. m_Ticks.Rate[(m_Ticks.nRate += (bNew ? 1 : 0))] = rate; 209. } 210. if (!ToReplay) 211. { 212. ArrayResize(RatesLocal, (m_Ticks.nRate - MemNRates)); 213. ArrayCopy(RatesLocal, m_Ticks.Rate, 0, 0); 214. CustomRatesUpdate(def_SymbolReplay, RatesLocal, (m_Ticks.nRate - MemNRates)); 215. dtRet = m_Ticks.Rate[m_Ticks.nRate].time; 216. m_Ticks.nRate = (MemNRates == 0 ? -1 : MemNRates); 217. m_Ticks.nTicks = MemNTicks; 218. ArrayFree(RatesLocal); 219. }else m_Ticks.nTicks = nShift; 220. 221. return dtRet; 222. }; 223. //+------------------------------------------------------------------+
ファイルC_FileTicks.mqhからのコードスニペット
このスニペットでは、C_FileTicks.mqhファイルで変更する必要がある行を正確に強調表示しています。149行目は削除するか、より正確には、新しい位置(158行目)に再配置する必要があることに注意してください。しかし、なぜこの変更をおこなうのでしょうか。少しお待ちください、親愛なる読者の皆さん。すべては後ほど説明されます。ここで、新たに152行目が追加されていることがわかります。重要なのは、ここでの関数がバーをティックに変換するシミュレーションをおこなっていることです。152行目では、新しいバーが開始されるインデックス値を取得し、その値をバーのSpreadフィールドに保存しています。
次に進み、非常に似た処理を行う別の関数を見てみましょう。ティックを読み取る処理を担当する関数内には、204行目という新しい行が1行追加されていることがわかります。しかし、ここで覚えておくべき重要な点は、ファイルからティックを読み取る際に、これらのティックを破棄し、シミュレートされたティックに置き換える必要がある場合があることです。この点については以前の記事で議論しており、その理由も説明しました。実際に私たちが重視しているのは、メモリインデックスMemShiftであり、これが新しいバーの開始位置を示します。前述の関数と同様、この値をMqlRates構造体のSpreadフィールドに格納します。
では、なぜこれをおこなうのでしょうか。その目的は何かを、今はっきりさせておきましょう。前の関数で行われるティックのバーへのシミュレーション処理では、各バーが正確にいつどこで始まるかを把握しています。なぜなら、シミュレーションが実行される前に、インデックスが直接バーの開始位置を指し示しているからです。ファイルからティックを読み込むこの関数でも、同様のことが言えます。例えば、190行目ではティックがバーに変換され、C_Replayクラス内と同様の処理がおこなわれます。その結果、新しいバーが作成されるたびに、そのバーの開始位置を正確に把握できるようになります。これにより、手動で開始点を決定するためにC_Replayクラスを使う必要はなくなります。また、Spreadフィールドはリプレイ/シミュレーションの文脈では実用的な用途がないため、各バーの正確な開始インデックスを格納するために再利用します。
前のセクションを思い出してみてください。早送りをおこなう手順では、129行目で計算がおこなわれ、効率的にシミュレーションを早送りするためにジャンプすべき正確なインデックスが決定されます。ティックを読み込む際に生成されるこの値の重要性がわかり始めているのではないでしょうか。これこそが、早送りプロセスを大幅に加速させるカギとなるのです。つまり、各バーを一つずつ再構築する必要がなくなり、正しい位置に直接ジャンプして、MetaTrader 5に前の位置と現在の位置の間のバーをレンダリングして更新するように依頼できます。言い換えれば、物事を処理する新たな方法を手に入れたということです。
このデータを効果的に利用するためには、C_Replayクラスのいくつかの部分を修正する必要があります。幸い、これらの変更は前のセクションで扱ったものに比べて最小限で済みます。次に示すスニペットを元に、C_Replay.mqhファイルを更新してください。
013. #define def_MaxSlider (def_MaxPosSlider + 1) ... 124. //+------------------------------------------------------------------+ 125. void AdjustPositionToReplay(void) 126. { 127. int nPos, nCount; 128. 129. if (m_IndControl.Position == (int)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks)) return; 130. nPos = (int)((m_MemoryData.nTicks * m_IndControl.Position) / def_MaxSlider); 131. for (nCount = 0; m_MemoryData.Rate[nCount].spread < nPos; m_Infos.CountReplay = m_MemoryData.Rate[nCount++].spread); 132. if (nCount > 0) CustomRatesUpdate(def_SymbolReplay, m_MemoryData.Rate, nCount - 1); 133. while ((nPos > m_Infos.CountReplay) && def_CheckLoopService) 134. CreateBarInReplay(false); 135. } 136. //+------------------------------------------------------------------+
ファイルC_Replay.mqhからのコードスニペット
ご覧のとおり、C_Replay.mqhヘッダーファイルに新しい行を追加する必要がありました。これは13行目で、全体的なオフセット計算に小さな修正を適用します。そしてもう一度言いますが、ここにあるすべてのものには理由と目的があります。この小さな調整を行わないと、別の問題が発生するか、別の方法で対処する必要が生じます。データモデリングの大部分をやり直す必要がないように、ここで調整を行うだけにすることをお勧めします。さて、13行目のこの調整はなぜ重要なのでしょうか?その理由は、これがないと、コントロールインジケーターのスライダーの最終位置によってシミュレーションまたは再生が途中で終了してしまうオフセットが発生するためです。ここでポジションを追加するだけで、チャートにさらにいくつかのティックを適用できるようになります。これは、リプレイまたはシミュレーションプロセスのよりスムーズな終了を保証するため、実際には有益です。
しかし、本当に重要な部分は131行目と132行目にあります。これら2行を追加するだけで、以前よりも大幅に高速な早送りを実現できます。それでも、処理されていないティックがいくつか残る可能性があり、その場合は以前と同じように処理する必要があります。これらのティックは133行目から始まるループを使用します。ただし、残っているティックは通常は最小限であるため、プロセスは非常に高速のままです。
ここで実際に何をしているのでしょうか。131行目では、値が目標位置のすぐ下にあるバーのインデックスを検索します。これは完全にforループ内で実行されます。この構造はほとんどの人にとっては奇妙に思えるかもしれませんが、実際には問題なく機能します。この異常な外観は、CountReplay値の割り当てをループ宣言内に直接配置しているためです。ただし、必要に応じてこの割り当てをループの外側に移動することもできます。
次に、132行目でnCountの値を確認する必要があります。これは、処理されるデータポイントまたはバーの数を正しく解釈できない可能性がある、MQL5ライブラリからのCustomRatesUpdate呼び出しで失敗するリスクを冒したくないためです。関数の残りの部分については、前のセクションで既に説明しました。これらの最新の変更に関して興味深いのは、結果として、C_Replay.mqhファイルの最終コードを再度更新する必要があったことです。これらの変更は簡単で、これ以上の説明は必要ないので、コードの最終バージョン(少なくとも現時点では)を単に紹介するだけにします。完全なコードは以下にあります。
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. //+------------------------------------------------------------------+ 004. #include "C_ConfigService.mqh" 005. #include "C_Controls.mqh" 006. //+------------------------------------------------------------------+ 007. #define def_IndicatorControl "Indicators\\Market Replay.ex5" 008. #resource "\\" + def_IndicatorControl 009. //+------------------------------------------------------------------+ 010. #define def_CheckLoopService ((!_StopFlag) && (ChartSymbol(m_Infos.IdReplay) != "")) 011. //+------------------------------------------------------------------+ 012. #define def_ShortNameIndControl "Market Replay Control" 013. #define def_MaxSlider (def_MaxPosSlider + 1) 014. //+------------------------------------------------------------------+ 015. class C_Replay : public C_ConfigService 016. { 017. private : 018. struct st00 019. { 020. C_Controls::eObjectControl Mode; 021. uCast_Double Memory; 022. ushort Position; 023. int Handle; 024. }m_IndControl; 025. struct st01 026. { 027. long IdReplay; 028. int CountReplay; 029. double PointsPerTick; 030. MqlTick tick[1]; 031. MqlRates Rate[1]; 032. }m_Infos; 033. stInfoTicks m_MemoryData; 034. //+------------------------------------------------------------------+ 035. inline bool MsgError(string sz0) { Print(sz0); return false; } 036. //+------------------------------------------------------------------+ 037. inline void UpdateIndicatorControl(void) 038. { 039. double Buff[]; 040. 041. if (m_IndControl.Handle == INVALID_HANDLE) return; 042. if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position) 043. { 044. if (CopyBuffer(m_IndControl.Handle, 0, 0, 1, Buff) == 1) 045. m_IndControl.Memory.dValue = Buff[0]; 046. if ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay) 047. m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition]; 048. }else 049. { 050. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position; 051. m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (ushort)m_IndControl.Mode; 052. m_IndControl.Memory._8b[7] = 'D'; 053. m_IndControl.Memory._8b[6] = 'M'; 054. EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, 0, m_IndControl.Memory.dValue, ""); 055. } 056. } 057. //+------------------------------------------------------------------+ 058. void SweepAndCloseChart(void) 059. { 060. long id; 061. 062. if ((id = ChartFirst()) > 0) do 063. { 064. if (ChartSymbol(id) == def_SymbolReplay) 065. ChartClose(id); 066. }while ((id = ChartNext(id)) > 0); 067. } 068. //+------------------------------------------------------------------+ 069. inline void CreateBarInReplay(bool bViewTick) 070. { 071. bool bNew; 072. double dSpread; 073. int iRand = rand(); 074. 075. if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[0], bNew)) 076. { 077. m_Infos.tick[0] = m_MemoryData.Info[m_Infos.CountReplay]; 078. if (m_MemoryData.ModePlot == PRICE_EXCHANGE) 079. { 080. dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 ); 081. if (m_Infos.tick[0].last > m_Infos.tick[0].ask) 082. { 083. m_Infos.tick[0].ask = m_Infos.tick[0].last; 084. m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread; 085. }else if (m_Infos.tick[0].last < m_Infos.tick[0].bid) 086. { 087. m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread; 088. m_Infos.tick[0].bid = m_Infos.tick[0].last; 089. } 090. } 091. if (bViewTick) CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 092. CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate); 093. } 094. m_Infos.CountReplay++; 095. } 096. //+------------------------------------------------------------------+ 097. void AdjustViewDetails(void) 098. { 099. MqlRates rate[1]; 100. 101. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_ASK_LINE, GetInfoTicks().ModePlot == PRICE_FOREX); 102. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_BID_LINE, GetInfoTicks().ModePlot == PRICE_FOREX); 103. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_LAST_LINE, GetInfoTicks().ModePlot == PRICE_EXCHANGE); 104. m_Infos.PointsPerTick = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE); 105. CopyRates(def_SymbolReplay, PERIOD_M1, 0, 1, rate); 106. if ((m_Infos.CountReplay == 0) && (GetInfoTicks().ModePlot == PRICE_EXCHANGE)) 107. for (; GetInfoTicks().Info[m_Infos.CountReplay].volume_real == 0; m_Infos.CountReplay++); 108. if (rate[0].close > 0) 109. { 110. if (GetInfoTicks().ModePlot == PRICE_EXCHANGE) 111. m_Infos.tick[0].last = rate[0].close; 112. else 113. { 114. m_Infos.tick[0].bid = rate[0].close; 115. m_Infos.tick[0].ask = rate[0].close + (rate[0].spread * m_Infos.PointsPerTick); 116. } 117. m_Infos.tick[0].time = rate[0].time; 118. m_Infos.tick[0].time_msc = rate[0].time * 1000; 119. }else 120. m_Infos.tick[0] = GetInfoTicks().Info[m_Infos.CountReplay]; 121. CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 122. } 123. //+------------------------------------------------------------------+ 124. void AdjustPositionToReplay(void) 125. { 126. int nPos, nCount; 127. 128. if (m_IndControl.Position == (int)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks)) return; 129. nPos = (int)((m_MemoryData.nTicks * m_IndControl.Position) / def_MaxSlider); 130. for (nCount = 0; m_MemoryData.Rate[nCount].spread < nPos; m_Infos.CountReplay = m_MemoryData.Rate[nCount++].spread); 131. if (nCount > 0) CustomRatesUpdate(def_SymbolReplay, m_MemoryData.Rate, nCount - 1); 132. while ((nPos > m_Infos.CountReplay) && def_CheckLoopService) 133. CreateBarInReplay(false); 134. } 135. //+------------------------------------------------------------------+ 136. public : 137. //+------------------------------------------------------------------+ 138. C_Replay() 139. :C_ConfigService() 140. { 141. Print("************** Market Replay Service **************"); 142. srand(GetTickCount()); 143. SymbolSelect(def_SymbolReplay, false); 144. CustomSymbolDelete(def_SymbolReplay); 145. CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay)); 146. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0); 147. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, 0); 148. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, 0); 149. CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation"); 150. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, 8); 151. SymbolSelect(def_SymbolReplay, true); 152. m_Infos.CountReplay = 0; 153. m_IndControl.Handle = INVALID_HANDLE; 154. m_IndControl.Mode = C_Controls::ePause; 155. m_IndControl.Position = 0; 156. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = C_Controls::eTriState; 157. } 158. //+------------------------------------------------------------------+ 159. ~C_Replay() 160. { 161. SweepAndCloseChart(); 162. IndicatorRelease(m_IndControl.Handle); 163. SymbolSelect(def_SymbolReplay, false); 164. CustomSymbolDelete(def_SymbolReplay); 165. Print("Finished replay service..."); 166. } 167. //+------------------------------------------------------------------+ 168. bool OpenChartReplay(const ENUM_TIMEFRAMES arg1, const string szNameTemplate) 169. { 170. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == 0) 171. return MsgError("Asset configuration is not complete, it remains to declare the size of the ticket."); 172. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == 0) 173. return MsgError("Asset configuration is not complete, need to declare the ticket value."); 174. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == 0) 175. return MsgError("Asset configuration not complete, need to declare the minimum volume."); 176. SweepAndCloseChart(); 177. m_Infos.IdReplay = ChartOpen(def_SymbolReplay, arg1); 178. if (!ChartApplyTemplate(m_Infos.IdReplay, szNameTemplate + ".tpl")) 179. Print("Failed apply template: ", szNameTemplate, ".tpl Using template default.tpl"); 180. else 181. Print("Apply template: ", szNameTemplate, ".tpl"); 182. 183. return true; 184. } 185. //+------------------------------------------------------------------+ 186. bool InitBaseControl(const ushort wait = 1000) 187. { 188. Print("Waiting for Mouse Indicator..."); 189. Sleep(wait); 190. while ((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, 0, "Indicator Mouse Study") == INVALID_HANDLE)) Sleep(200); 191. if (def_CheckLoopService) 192. { 193. AdjustViewDetails(); 194. Print("Waiting for Control Indicator..."); 195. if ((m_IndControl.Handle = iCustom(ChartSymbol(m_Infos.IdReplay), ChartPeriod(m_Infos.IdReplay), "::" + def_IndicatorControl, m_Infos.IdReplay)) == INVALID_HANDLE) return false; 196. ChartIndicatorAdd(m_Infos.IdReplay, 0, m_IndControl.Handle); 197. UpdateIndicatorControl(); 198. } 199. 200. return def_CheckLoopService; 201. } 202. //+------------------------------------------------------------------+ 203. bool LoopEventOnTime(void) 204. { 205. int iPos; 206. 207. while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay)) 208. { 209. UpdateIndicatorControl(); 210. Sleep(200); 211. } 212. m_MemoryData = GetInfoTicks(); 213. AdjustPositionToReplay(); 214. iPos = 0; 215. while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService)) 216. { 217. if (m_IndControl.Mode == C_Controls::ePause) return true; 218. 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); 219. CreateBarInReplay(true); 220. while ((iPos > 200) && (def_CheckLoopService)) 221. { 222. Sleep(195); 223. iPos -= 200; 224. m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks); 225. UpdateIndicatorControl(); 226. } 227. } 228. 229. return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService)); 230. } 231. }; 232. //+------------------------------------------------------------------+ 233. #undef macroRemoveSec 234. #undef def_SymbolReplay 235. #undef def_CheckLoopService 236. #undef def_MaxSlider 237. //+------------------------------------------------------------------+
C_Replay.mqhファイルの最終ソースコード
バーの時間と変動率の更新
この問題の解決は比較的簡単です。必要なのは、マウスインジケーターにメッセージを送信して、関連する情報を正しく解釈・表示できるようにすることだけです。今回のタスクは、「新しいバーが始まるまでの残り時間」と「前日の終値と現在の提示価格との間の変動率(パーセンテージ)」を、インジケーターに表示させることです。
作業を簡略化するため、まずは変動率の処理から取り掛かります。必要に応じて、ここで紹介する方法をベースにして独自のやり方を構築しても構いません。ご自身のニーズに合うように自由にアレンジしてください。まず、変動率の問題点を理解しましょう。マウスインジケーターを確認すると、前日の終値と現在の提示価格との間の変動率が正しく表示されていないことに気づくはずです。ただし、マウスの位置に基づいた値は正確です。では、なぜこのようなズレが発生するのでしょうか。一見すると、マウスインジケーターが履歴データの終点と、シミュレーション(またはリプレイ)の開始点を正しく認識できていないためだと思われるかもしれません。しかし、実際にはそうではありません。マウスインジケーターはデータを正しく読み取り、解釈しています。これは、マウスを動かして変化を観察すれば確認できます。本当の問題は、何らかの要因によって、マウスインジケーターが一部のデータを誤って解釈し、時折おかしな値を表示してしまうという点にあります。とはいえ、正しい値が表示されることもあるため、この不安定さが私たちが対処すべき課題です。解決方法は、実は非常にシンプルです。ただし、注意点として、これから紹介する手法を安易に多用しないようにしてください。扱いを誤ると、開発全体のロジックやフローの制御が困難になる可能性があります。では、この問題の具体的な解決方法を見ていきましょう。
最初のステップは、マウスインジケーターのコードを次のように変更することです。09. #property indicator_chart_window 10. #property indicator_plots 0 11. #property indicator_buffers 1 12. //+------------------------------------------------------------------+ 13. double GL_PriceClose; 14. //+------------------------------------------------------------------+ 15. #include <Market Replay\Auxiliar\Study\C_Study.mqh> 16. //+------------------------------------------------------------------+ 17. C_Study *Study = NULL; 18. //+------------------------------------------------------------------+ 19. input color user02 = clrBlack; //Price Line 20. input color user03 = clrPaleGreen; //Positive Study 21. input color user04 = clrLightCoral; //Negative Study 22. //+------------------------------------------------------------------+ 23. C_Study::eStatusMarket m_Status; 24. int m_posBuff = 0; 25. double m_Buff[]; 26. //+------------------------------------------------------------------+ 27. int OnInit() 28. { 29. ResetLastError(); 30. Study = new C_Study(0, "Indicator Mouse Study", user02, user03, user04); 31. if (_LastError != ERR_SUCCESS) return INIT_FAILED; 32. if ((*Study).GetInfoTerminal().szSymbol != def_SymbolReplay) 33. { 34. MarketBookAdd((*Study).GetInfoTerminal().szSymbol); 35. OnBookEvent((*Study).GetInfoTerminal().szSymbol); 36. m_Status = C_Study::eCloseMarket; 37. }else 38. m_Status = C_Study::eInReplay; 39. SetIndexBuffer(0, m_Buff, INDICATOR_DATA); 40. ArrayInitialize(m_Buff, EMPTY_VALUE); 41. 42. return INIT_SUCCEEDED; 43. } 44. //+------------------------------------------------------------------+ 45. int OnCalculate(const int rates_total, const int prev_calculated, const datetime& time[], const double& open[], 46. const double& high[], const double& low[], const double& close[], const long& tick_volume[], 47. const long& volume[], const int& spread[]) 48. //int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 49. { 50. GL_PriceClose = close[rates_total - 1]; 51. m_posBuff = rates_total; 52. (*Study).Update(m_Status); 53. 54. return rates_total; 55. } 56. //+------------------------------------------------------------------+
マウスポインタのコードスニペット
なお、13行目に変数を追加したことに注意してください。この変数は「グローバル変数」です。ただし、ここで言う「グローバル」とは、単に関数やプロシージャの外で宣言されているという意味ではなく、そのスコープとコード内での配置から、本当の意味でのグローバル変数であることを指します。この変数自体には、特別な機能があるわけではありません。ただし、50行目ではMetaTrader 5から提供される値をこの変数が受け取るようになっています。また、48行目にあったOnCalculateイベント関数の宣言は、新しいバージョンに置き換えられていることにも注意してください。これは非常に重要な変更です。これにより、13行目で宣言した変数を使って、前述した変動率の問題を解決することが可能になります。次に変更を加えるのは、C_Study.mqhヘッダーファイル内のコードです。以下にその内容を示します。
41. //+------------------------------------------------------------------+ 42. void Draw(void) 43. { 44. double v1; 45. 46. if (m_Info.bvT) 47. { 48. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn1, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - 18); 49. ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn1, OBJPROP_TEXT, m_Info.szInfo); 50. } 51. if (m_Info.bvD) 52. { 53. v1 = NormalizeDouble((((GetInfoMouse().Position.Price - m_Info.Rate.close) / m_Info.Rate.close) * 100.0), 2); 54. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - 1); 55. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_BGCOLOR, (v1 < 0 ? m_Info.corN : m_Info.corP)); 56. ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_TEXT, StringFormat("%.2f%%", MathAbs(v1))); 57. } 58. if (m_Info.bvP) 59. { 60. v1 = NormalizeDouble((((GL_PriceClose - m_Info.Rate.close) / m_Info.Rate.close) * 100.0), 2); 61. v1 = NormalizeDouble((((iClose(GetInfoTerminal().szSymbol, PERIOD_D1, 0) - m_Info.Rate.close) / m_Info.Rate.close) * 100.0), 2); 62. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - 1); 63. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_BGCOLOR, (v1 < 0 ? m_Info.corN : m_Info.corP)); 64. ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_TEXT, StringFormat("%.2f%%", MathAbs(v1))); 65. } 66. } 67. //+------------------------------------------------------------------+
ファイルC_Study.mqhからのコードスニペット
以前は旧ロジックが記述されていた61行目が、60行目の新しいロジックに置き換えられているのが分かると思います。ここで、インジケーターファイル内で宣言されたグローバル変数が参照されている点にも注目してください。どうしてこのようなことが可能なのでしょうか。その理由は、その変数がグローバルとして、つまりファイルのグローバルスコープ内で宣言されていることです。これにより、現在構築中のコードのどこからでも、その変数にアクセスできるようになっています。ただし、このようなグローバルアクセスは、場合によっては予期しない問題を引き起こす可能性があります。そのため、グローバル変数を扱う際には、常に慎重であるべきです。
私がグローバル変数を使用する場合は、必ずその必要性を明確に認識した上で行います(たいていは非常に限定的な目的のためです)。その際、意図しない影響を防ぐために、通常は#include文よりも前に宣言し、「GL_」という接頭辞を付けて一目で識別できるようにしています。ちなみに、25行目にもグローバルスコープの変数がありますが、こちらについてはあまり気にしていません。特定の目的のみに使用されており、誤って変更されるリスクは非常に低いからです。一方で、13行目の変数には特に注意が必要です。というのも、うっかり変更してしまいやすく、それに気づきにくいからです。
このようなちょっとした変更によって、時折誤った変動率が示されていた問題を解消することができました。加えて、パフォーマンス面でも向上が見られます。というのも、終値を取得するためにiCloseを呼び出す必要がなくなったからです。MetaTrader 5がこの情報を直接提供してくれるため、手動での取得によるオーバーヘッドを回避できるのです。
結論
バーが閉じるまでの残り時間を、特にリプレイ/シミュレーションのコンテキストでどのように追跡するかという問題については、まだ取り上げていませんが、この記事では大きな前進がありました。現在のアプリケーションの動作は、以前グローバルターミナル変数に依存していた頃の挙動と、ほぼ一致する状態にまで近づいています。外部ライブラリに頼らず、純粋なMQL5だけでここまでのことが実現できることに驚いた方も多いのではないでしょうか。しかし、これはあくまで始まりにすぎません。やるべきことはまだ多くあり、その一歩一歩が新たな課題と学びの機会をもたらしてくれます。
なお、リプレイ/シミュレーションモードでバーの残り時間をMetaTrader 5から取得する方法については、次回の記事の冒頭で詳しく解説します。また、現在の構成の中でシームレスに動作させるために必要な、別の重要な改善ポイントにも着手していきますので、お見逃しなく。
残念ながら、今回の記事の内容だけでは、プログラマーでない方がアプリケーションを完全に使いこなすことはまだできません。というのも、バーの終了時間に関する詳細が未解決のままだからです。とはいえ、プログラマーの方で、これまでの手順に沿って修正を加えてきた方であれば、リプレイ/シミュレーション機能が、以下のデモビデオに示すようにすでに正しく動作していることを確認できるでしょう。それでは、次回の記事でお会いしましょう。
デモビデオ
MetaQuotes Ltdによりポルトガル語から翻訳されました。
元の記事: https://www.mql5.com/pt/articles/12265





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