
リプレイシステムの開発(第62回):サービスの再生(III)
はじめに
前回の「リプレイシステムの開発(第61回):サービスの再生(II)」では、シミュレーションモードを使用している際に私たちのシステムで発生している問題について説明しました。これは、必ずしも開発中のアプリケーションに重大な障害があるというわけではなく、システム全体の応答速度に起因するものです。応答が追いつかず、すべての受信データをアプリケーションが適切に処理できなかったため、一定の調整が必要になりました。もちろん、私たちのサービスが理想的なシナリオ通りに動作していないとしても、現実にはそうした理想的な状況が存在すること自体まれであることも理解しています。
私が導き出した最適な解決策は、シミュレーションにおける最大制限値を調整することでした。ただし、この記事ではその変更がどのような影響をもたらすのかを慎重に検証し、なぜそのアプローチを選択したのかについても詳しく解説します。さらに、開発中のアプリケーション外で生成された実データや外部シミュレーションデータに直接関係する要素もあります。意外に思われるかもしれませんが、特に先物契約においては、1分間に異常な数のティックや取引が集中するケースがあります。こうした状況では、取引サーバーに接続していても、MetaTrader 5プラットフォームが価格の変動を処理・表示する速度に問題が生じることがあります。この問題に直面したことがない方であれば、MetaTrader 5を実行しているハードウェアの性能不足やOSの不具合が原因だと考えるかもしれません。しかし残念ながら、そうした推測は完全に誤りであり、コンピューティングに関する理解が不十分な人々によって広まった誤解に過ぎません。
実際の取引サーバーに接続している状態でも大量のティックデータの処理にプラットフォームが苦戦していることを考えると、それをリプレイする場合にはさらに深刻な状況となります。タイミングの精度が大きく損なわれ、完全な惨事となるでしょう。そのため、プラットフォームの処理能力の限界が露呈したり、新たな問題を引き起こしたりしないよう、実データや外部シミュレーションデータに対しても上限を設ける必要があります。それでは、新しいコードがどのように構成されるかを見ていきましょう。
新しいコンセプト、新しいシステム
おそらく、このセクションのタイトルは完全には自己説明的ではなく、これから実装しようとしている内容を十分に伝えていないかもしれません。しかし、まずはシステム内であらゆるシミュレーションの生成およびモデリングを担当するクラスに加えられた変更を分析していきます。シミュレーションクラスのコードを確認する前に、定義ファイルを最初に見ておく必要があります。そこに新しい行が追加されており、コード内のさまざまな箇所で参照されるからです。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_VERSION_DEBUG 05. //+------------------------------------------------------------------+ 06. #ifdef def_VERSION_DEBUG 07. #define macro_DEBUG_MODE(A) \ 08. Print(__FILE__, " ", __LINE__, " ", __FUNCTION__ + " " + #A + " = " + (string)(A)); 09. #else 10. #define macro_DEBUG_MODE(A) 11. #endif 12. //+------------------------------------------------------------------+ 13. #define def_SymbolReplay "RePlay" 14. #define def_MaxPosSlider 400 15. #define def_MaxTicksVolume 2000 16. //+------------------------------------------------------------------+ 17. union uCast_Double 18. { 19. double dValue; 20. long _long; // 1 Information 21. datetime _datetime; // 1 Information 22. uint _32b[sizeof(double) / sizeof(uint)]; // 2 Informations 23. ushort _16b[sizeof(double) / sizeof(ushort)]; // 4 Informations 24. uchar _8b [sizeof(double) / sizeof(uchar)]; // 8 Informations 25. }; 26. //+------------------------------------------------------------------+ 27. enum EnumEvents { 28. evHideMouse, //Hide mouse price line 29. evShowMouse, //Show mouse price line 30. evHideBarTime, //Hide bar time 31. evShowBarTime, //Show bar time 32. evHideDailyVar, //Hide daily variation 33. evShowDailyVar, //Show daily variation 34. evHidePriceVar, //Hide instantaneous variation 35. evShowPriceVar, //Show instantaneous variation 36. evSetServerTime, //Replay/simulation system timer 37. evCtrlReplayInit, //Initialize replay control 38. }; 39. //+------------------------------------------------------------------+
Defines.mqhファイルのソースコード
コードに追加されたのは行15です。この値がどこから来るのかについては、今は説明しません。シミュレーションコードで実際に使用する際に解説します。ここがその値が最初に使われる場所です。この変更について説明したので、次はティックのシミュレーションを担当するクラスのソースコードを見ていきましょう。以下に、変更が加えられたコードを示します。
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. //+------------------------------------------------------------------+ 004. #include "..\..\Defines.mqh" 005. //+------------------------------------------------------------------+ 006. class C_Simulation 007. { 008. private : 009. //+------------------------------------------------------------------+ 010. int m_NDigits; 011. bool m_IsPriceBID; 012. double m_TickSize; 013. struct st00 014. { 015. bool bHigh, bLow; 016. int iMax; 017. }m_Marks; 018. //+------------------------------------------------------------------+ 019. template < typename T > 020. inline T RandomLimit(const T Limit01, const T Limit02) 021. { 022. T a = (Limit01 > Limit02 ? Limit01 - Limit02 : Limit02 - Limit01); 023. return (Limit01 >= Limit02 ? Limit02 : Limit01) + ((T)(((rand() & 32767) / 32737.0) * a)); 024. } 025. //+------------------------------------------------------------------+ 026. inline void Simulation_Time(const MqlRates &rate, MqlTick &tick[]) 027. { 028. for (int c0 = 0, iPos, v0 = (int)(60000 / m_Marks.iMax), v1 = 0, v2 = v0; c0 <= m_Marks.iMax; c0++, v1 = v2, v2 += v0) 029. { 030. iPos = RandomLimit(v1, v2); 031. tick[c0].time = rate.time + (iPos / 1000); 032. tick[c0].time_msc = iPos % 1000; 033. } 034. } 035. //+------------------------------------------------------------------+ 036. inline void CorretTime(MqlTick &tick[]) 037. { 038. for (int c0 = 0; c0 <= m_Marks.iMax; c0++) 039. tick[c0].time_msc += (tick[c0].time * 1000); 040. } 041. //+------------------------------------------------------------------+ 042. inline int Unique(const double price, const MqlTick &tick[]) 043. { 044. int iPos = 1; 045. 046. do 047. { 048. iPos = (m_Marks.iMax > 20 ? RandomLimit(1, m_Marks.iMax - 1) : iPos + 1); 049. }while ((m_IsPriceBID ? tick[iPos].bid : tick[iPos].last) == price); 050. 051. return iPos; 052. } 053. //+------------------------------------------------------------------+ 054. inline void MountPrice(const int iPos, const double price, const int spread, MqlTick &tick[]) 055. { 056. if (m_IsPriceBID) 057. { 058. tick[iPos].bid = NormalizeDouble(price, m_NDigits); 059. tick[iPos].ask = NormalizeDouble(price + (m_TickSize * spread), m_NDigits); 060. }else 061. tick[iPos].last = NormalizeDouble(price, m_NDigits); 062. } 063. //+------------------------------------------------------------------+ 064. inline void Random_Price(const MqlRates &rate, MqlTick &tick[]) 065. { 066. for (int c0 = 1; c0 < m_Marks.iMax; c0++) 067. { 068. MountPrice(c0, NormalizeDouble(RandomLimit(rate.high, rate.low), m_NDigits), (rate.spread + RandomLimit((int)(rate.spread | (m_Marks.iMax & 0xF)), 0)), tick); 069. m_Marks.bHigh = (rate.high == (m_IsPriceBID ? tick[c0].bid : tick[c0].last)) || m_Marks.bHigh; 070. m_Marks.bLow = (rate.low == (m_IsPriceBID ? tick[c0].bid : tick[c0].last)) || m_Marks.bLow; 071. } 072. } 073. //+------------------------------------------------------------------+ 074. inline void DistributeVolumeReal(const MqlRates &rate, MqlTick &tick[]) 075. { 076. for (int c0 = 0; c0 <= m_Marks.iMax; c0++) 077. { 078. tick[c0].volume_real = 1.0; 079. tick[c0].volume = 1; 080. } 081. if ((m_Marks.iMax + 1) < rate.tick_volume) for (int c0 = (int)(rate.tick_volume - m_Marks.iMax); c0 > 0; c0--) 082. tick[RandomLimit(0, m_Marks.iMax - 1)].volume += 1; 083. for (int c0 = (int)(rate.real_volume - m_Marks.iMax); c0 > 0; c0--) 084. tick[RandomLimit(0, m_Marks.iMax)].volume_real += 1.0; 085. } 086. //+------------------------------------------------------------------+ 087. inline int RandomWalk(int In, int Out, const double Open, const double Close, double High, double Low, const int Spread, MqlTick &tick[], int iMode, int iDesloc) 088. { 089. double vStep, vNext, price, vH = High, vL = Low; 090. char i0 = 0; 091. 092. vNext = vStep = (Out - In) / ((High - Low) / m_TickSize); 093. for (int c0 = In, c1 = 0, c2 = 0; c0 <= Out; c0++, c1++) 094. { 095. price = (m_IsPriceBID ? tick[c0 - 1].bid : tick[c0 - 1].last) + (m_TickSize * ((rand() & 1) == 1 ? -iDesloc : iDesloc)); 096. price = (price > vH ? vH : (price < vL ? vL : price)); 097. MountPrice(c0, price, (Spread + RandomLimit((int)(Spread | (m_Marks.iMax & 0xF)), 0)), tick); 098. switch (iMode) 099. { 100. case 1: 101. i0 |= (price == High ? 0x01 : 0); 102. i0 |= (price == Low ? 0x02 : 0); 103. vH = (i0 == 3 ? High : vH); 104. vL = (i0 ==3 ? Low : vL); 105. break; 106. case 0: 107. if (price == Close) return c0; 108. default: 109. break; 110. } 111. if (((int)floor(vNext)) >= c1) continue; else if ((++c2) <= 3) continue; 112. vNext += vStep; 113. vL = (iMode != 2 ? (Close > vL ? (i0 == 3 ? vL : vL + m_TickSize) : vL) : (((c2 & 1) == 1) ? (Close > vL ? vL + m_TickSize : vL) : (Close < vH ? vL : vL + m_TickSize))); 114. vH = (iMode != 2 ? (Close > vL ? vH : (i0 == 3 ? vH : vH - m_TickSize)) : (((c2 & 1) == 1) ? (Close > vL ? vH : vH - m_TickSize) : (Close < vH ? vH - m_TickSize : vH))); 115. } 116. 117. return Out; 118. } 119. //+------------------------------------------------------------------+ 120. public : 121. //+------------------------------------------------------------------+ 122. C_Simulation(const int nDigits) 123. { 124. m_NDigits = nDigits; 125. m_IsPriceBID = (SymbolInfoInteger(def_SymbolReplay, SYMBOL_CHART_MODE) == SYMBOL_CHART_MODE_BID); 126. m_TickSize = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE); 127. } 128. //+------------------------------------------------------------------+ 129. inline int Simulation(const MqlRates &rate, MqlTick &tick[], const int MaxTickVolume = def_MaxTicksVolume) 130. { 131. int i0, i1, i2; 132. bool b0; 133. 134. m_Marks.iMax = (MaxTickVolume <= 0 ? 1 : (MaxTickVolume >= def_MaxTicksVolume ? def_MaxTicksVolume : MaxTickVolume)); 135. m_Marks.iMax = ((int)rate.tick_volume > m_Marks.iMax ? m_Marks.iMax : (int)rate.tick_volume - 1); 136. m_Marks.bHigh = (rate.open == rate.high) || (rate.close == rate.high); 137. m_Marks.bLow = (rate.open == rate.low) || (rate.close == rate.low); 138. Simulation_Time(rate, tick); 139. MountPrice(0, rate.open, rate.spread, tick); 140. if (m_Marks.iMax > 10) 141. { 142. i0 = (int)(MathMin(m_Marks.iMax / 3.0, m_Marks.iMax * 0.2)); 143. i1 = m_Marks.iMax - i0; 144. i2 = (int)(((rate.high - rate.low) / m_TickSize) / i0); 145. i2 = (i2 == 0 ? 1 : i2); 146. b0 = (m_Marks.iMax >= 1000 ? ((rand() & 1) == 1) : (rate.high - rate.open) < (rate.open - rate.low)); 147. i0 = RandomWalk(1, i0, rate.open, (b0 ? rate.high : rate.low), rate.high, rate.low, rate.spread, tick, 0, i2); 148. RandomWalk(i0, i1, (m_IsPriceBID ? tick[i0].bid : tick[i0].last), (b0 ? rate.low : rate.high), rate.high, rate.low, rate.spread, tick, 1, i2); 149. RandomWalk(i1, m_Marks.iMax, (m_IsPriceBID ? tick[i1].bid : tick[i1].last), rate.close, rate.high, rate.low, rate.spread, tick, 2, i2); 150. m_Marks.bHigh = m_Marks.bLow = true; 151. 152. }else Random_Price(rate, tick); 153. if (!m_IsPriceBID) DistributeVolumeReal(rate, tick); 154. if (!m_Marks.bLow) MountPrice(Unique(rate.high, tick), rate.low, rate.spread, tick); 155. if (!m_Marks.bHigh) MountPrice(Unique(rate.low, tick), rate.high, rate.spread, tick); 156. MountPrice(m_Marks.iMax, rate.close, rate.spread, tick); 157. CorretTime(tick); 158. 159. return m_Marks.iMax; 160. } 161. //+------------------------------------------------------------------+ 162. }; 163. //+------------------------------------------------------------------+
C_Simulation.mqhのソースコード
この時点では、特にこのクラスを最後に変更してからかなりの時間が経過しているため、変更点にすぐには気づかないかもしれません。最後にこのクラスを扱ったのは、ランダムウォークに関する記事でした。ただし、生成されるティック数が若干減少したとしても、システムがタイミングの面で一定の一貫性を維持する必要があるため、このコードに小さな修正を加える必要がありました。
何が変更されたのかを理解してもらうために、1つの重要なポイントを取り上げます。コード全体には、プロセスを新しい手法に合わせるためだけに行われた小さな調整もいくつかありますが、それらは特別に注意を払う必要はありません。この中で最も重要な変更は、16行目に新しく追加された変数です。この変数は、もともとクラス内には存在していませんでした。ただし、この変数がクラス全体にどのような影響を与えるかを確認する前に、まずどこで初期化されているのかを見てみましょう。おそらく初期化はクラスのコンストラクタでおこなわれていると想像されるかもしれませんが、そうではありません。実際には、134行目と135行目の間で初期化されています。この2行で何が行われているかに注目してください。システムを改修する予定がある方にとっては、非常に重要なポイントになります。129行目では、ティックシミュレーションを生成する関数が定義されています。そして今回、シミュレートされる最大ティック数を指定する追加パラメータが新たに導入されました。Defines.mqhファイルに追加されたあの1行を覚えていますか。その定義が実際に使われている場所の1つが、まさにここです。では、実際に何が起きているのかを見てみましょう。これにより、コードを変更する場合に、その変更がシステムの挙動にどのような影響を与えるかを理解しやすくなります。ティックシミュレーションを実行する関数を呼び出すときは、シミュレートするティックの最大数も引数として渡す必要があります。ただし、この値には既定のデフォルト値が設定されているため、明示的に指定する必要はありません。指定された値が0以下だった場合、システムは最小値として1ティックを自動的に適用します。これは恣意的な仕様ではなく、外為取引では最小のティックボリュームが1であるというルールに基づいています。一方で、定義済みの最大値を超える値が指定された場合には、その入力値は無視され、システムに設定された上限が使用されます。この上限値はDefines.mqhヘッダーファイルで定義されており、システムでシミュレート可能なティックの最大数を定めています。0以上、最大値以下の範囲内であれば、その値がシミュレーションにおける最大ティック数として使用されます。このようにして、任意の範囲で柔軟に調整が可能となっています。
ここで重要な補足があります。この最大ティック数はランダムに選ばれたわけではありません。1分足をこの最大値(2000ティック)で分割すると、1ティックあたりの間隔は約30ミリ秒になります。この30ミリ秒という間隔は、プロット処理全体をスムーズで一貫性のある動きに保つための最適な値とされています。
もちろん、より大きな値を指定することもできますが、それによって実際にシミュレートされるティックの数が増えるわけではありません。あくまで許容される上限値が引き上げられるだけです。この説明は134行目に関係する部分ですが、実際に使用される最大ティック数は135行目で決定されます。135行目では、134行目で計算された値とバーに記録されているティック数とを比較します。そして、134行目の値がバーのティック数より小さい場合はその値が使用され、逆にバーのティック数のほうが少ない場合は、指定された入力値は無視されて、バー側のティック数が使用されます。
先ほど述べたように、これらの変更により、最大ティック数に関連するすべてのテストコードの見直しが必要となりました。その結果として、このクラス内のすべての関数や処理に対しても小規模な修正が加えられています。ただし、それぞれの変更は非常に単純なものであるため、ここで詳細に説明することはしません。不明点がある場合は、ランダムウォークに関する記事「リプレイシステムの開発 - 市場シミュレーション(第15回):シミュレーターの誕生(V) - ランダムウォーク」を参照してください。
さて、これで最初の問題は解決しました。今後は、シミュレーションを実行するたびに、1本のバーを作成するために必要なティックの最大数を調整できるようになりました。しかし、これだけでは目的を達成するには不十分です。忘れないでください:私たちはティックシミュレーションの処理自体は制御していますが、ユーザーが生成されるティック数を自由に減らせるような設定機能はまだ提供していません。つまり、まだ解決すべき問題が2つあるということです。そして、そのどちらにもシステムへのさらなる変更が必要です。
それでは、次の課題に取り組みましょう。次に考慮すべきは、実際のデータあるいは外部からシミュレートされたデータに含まれるティック数の上限についてです。この点を踏まえて、次のトピックに進みます。
実際の市場データに合わせた調整
タイトルでは実際の市場データのみが使用されることが示唆されていますが、これは完全には正しくありません。市場の動きを外部でシミュレーションし、それをファイルに保存したうえで、そのファイルを利用してバーの代わりにティックデータを提供することも可能です。これは、非常に有効かつ現実的なアプローチです。とはいえ、根本的な問題は前のセクションと同じです。すなわち、実際の市場データを扱う場合であっても、ティックの最大数に制限を設ける必要があるという点です。
ただし、前回とは異なり、今回の課題は扱うデータの性質により、はるかに複雑になります。もしリプレイ/シミュレーションの仕組みが特定の市場タイプだけに特化して設計されていれば、解決は比較的簡単だったでしょう。たとえば、ティックを読み込みながら、指定された時間ウィンドウ内の合計ティック数をチェックし、それがある閾値を超えていれば、超過分のティックを破棄するようシステムに指示することが可能です。この時間ウィンドウは1分足に相当します。そして、超過分を破棄した後は、残ったティックに基づき、ランダムウォークを使って動きを再生成するようシミュレーターに指示します。これにより、一定のウィンドウ内でティック数を制限することができます。しかしながら、ティックデータを読み込む際に、それがBID値なのかLAST値なのかを判別できないという問題があります。ここに、この課題の難しさが潜んでいます。というのも、どのチャートシステム(BIDベースかLASTベースか)が使用されているのかを特定できないからです。
まず最初に考えるべき問いは、この問題に対して最善の解決策は何かということです。ユーザーにチャートの種類を指定させるべきか、それともこの状況をコード側で自動的に処理すべきかということです。前者、すなわちユーザーに任せる方法は実装が非常に簡単です。しかし、その代わりに、間違って指定されるリスクがつきまといます。正直なところ、チャートの表現形式には2種類あり、それぞれが異なる市場モデルに対応していることを理解しているユーザーはどれほどいるでしょうか。多くのユーザーは、MetaTrader 5が3種類のサーバー(BID専用、ASK専用、LAST専用)をサポートしていることすら知りませんし、それぞれの資産に対して正しいプロット方式を設定する必要があるということも理解していないのが現実です。
このような潜在的な誤認のリスクを避けるため、私たちの実装では追加の工夫と作業が求められます。ただし、出発点ははっきりしています。それは、C_FileTicksクラスに定義されているLoadTicks関数です。ここで、この関数の元の実装を確認し、どのような変更を加えるべきかを検討していきましょう。以下がその関数です。
01. datetime LoadTicks(const string szFileNameCSV, const bool ToReplay = true) 02. { 03. int MemNRates, 04. MemNTicks; 05. datetime dtRet = TimeCurrent(); 06. MqlRates RatesLocal[], 07. rate; 08. bool bNew; 09. 10. MemNRates = (m_Ticks.nRate < 0 ? 0 : m_Ticks.nRate); 11. MemNTicks = m_Ticks.nTicks; 12. if (!Open(szFileNameCSV)) return 0; 13. if (!ReadAllsTicks()) return 0; 14. rate.time = 0; 15. for (int c0 = MemNTicks; c0 < m_Ticks.nTicks; c0++) 16. { 17. if (!BuildBar1Min(c0, rate, bNew)) continue; 18. if (bNew) ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 2 : def_BarsDiary), def_BarsDiary); 19. m_Ticks.Rate[(m_Ticks.nRate += (bNew ? 1 : 0))] = rate; 20. } 21. if (!ToReplay) 22. { 23. ArrayResize(RatesLocal, (m_Ticks.nRate - MemNRates)); 24. ArrayCopy(RatesLocal, m_Ticks.Rate, 0, 0); 25. CustomRatesUpdate(def_SymbolReplay, RatesLocal, (m_Ticks.nRate - MemNRates)); 26. dtRet = m_Ticks.Rate[m_Ticks.nRate].time; 27. m_Ticks.nRate = (MemNRates == 0 ? -1 : MemNRates); 28. m_Ticks.nTicks = MemNTicks; 29. ArrayFree(RatesLocal); 30. }else SetSymbolInfos(); 31. m_Ticks.bTickReal = true; 32. 33. return dtRet; 34. };
C_FilesTicks.mqhソースコードの一部
行番号については気にしないでください。ここに記載されている行番号は、あくまで説明の参考用であり、実際のC_FileTicks.mqhファイル内の行を正確に示すものではありません。このファイルにはすでにいくつかの変更が加えられており、それらは後ほど確認していきます。それでは、実際のティックデータを読み込む際に何が起こるのかを見ていきましょう。
関数の開始時、行10および11では、読み込まれたティックの位置と、それらのティックを表すバーの現在の値が一時的に保存されます。呼び出し元が、ティックではなく既存のバーをリプレイの基準として使用するよう指定している場合は、これらの値はそれぞれ行27と28で上書きされます。これにより、システムは整合性を保ちつつ、リプレイ用のティックを待機する状態になります。
行12では、データファイルのオープンを試み、続く行13でファイル内のすべてのティック(文字通りすべて)を読み込みます。読み込みが成功すれば、すべてのティックがメモリにロードされます。しかし、時として単位時間内のティック数が、システムで定義された上限を超えることがあります。この時点では、私たちにはまだ何も対処できません。その理由は、どのチャートタイプが使われるのかがまだ確定していないということです。しかし、すべてのティックを読み込んだ後であれば、チャートタイプを特定でき、そこから処理に着手することが可能になります。
さて、ここからが面白い部分です。私たちは1分間の時間帯を分析しようとしています。行15からは、1分足の構築を目的としたループに入ります。これは、システムが必要に応じてバーにアクセスできるようにするための処理です。そして、この部分こそが、私たちが介入すべきポイントです。行17では、バー構築を担当する関数が呼び出されます。この関数では、バー内の動き(価格変動)を生成するために使用されたティックのボリュームが考慮されます。そして重要なのが行18です。ここの条件がtrueになると、バーは、あたかもバーファイルから直接読み込んだかのように扱われるようになります。まさにこのバー情報こそが、シミュレーションクラスへ渡すべきデータとなります。これについては、前回のトピックに戻り、行129を確認するとより明確になるはずです。
ここまでで、実装の方向性が見えてきたと思います。少なくとも初期段階では、ティック数がプログラム内で定義された上限を超えていることが判明した時点で、何らかのアクションを実行する必要があります。これに関しては、後ほどさらなる改良を加えていく予定です。
まずは簡単な部分からです。ティック数を確認し、必要に応じてシミュレーションクラスにランダムウォークを実行させることで、適切なティック数を確保して動きを生成させます。問題はここからです。シミュレーションクラスはティックを生成するものの、私たちは既に読み込まれたティックデータを、できる限り簡潔な方法で変更する必要があります。ティックの削除は、指定された時間ウィンドウ内で、上限を超えた場合に限りおこなう必要があります。また、別の小さな問題もあります。シミュレーション処理は、そのデータがリプレイ用に使われる場合にのみおこなうべきです。幸いなことに、これは非常に簡単に処理できます。呼び出し元からToReplayの値が通知されるため、それを確認すれば十分です。要するに、これは「楽勝」です。では、次に難関である不要なティックを効率的に上書きする方法を考えましょう。そのためには、前回提示した関数を修正し、別の関数に置き換える必要があります。
以下に示すのは、その実現に向けた最初の試みです(新しい機能の開発は常に試行錯誤の連続です)。
01. //+------------------------------------------------------------------+ 02. datetime LoadTicks(const string szFileNameCSV, const bool ToReplay, const int MaxTickVolume) 03. { 04. int MemNRates, 05. MemNTicks, 06. nDigits, 07. nShift; 08. datetime dtRet = TimeCurrent(); 09. MqlRates RatesLocal[], 10. rate; 11. MqlTick TicksLocal[]; 12. bool bNew; 13. 14. MemNRates = (m_Ticks.nRate < 0 ? 0 : m_Ticks.nRate); 15. nShift = MemNTicks = m_Ticks.nTicks; 16. if (!Open(szFileNameCSV)) return 0; 17. if (!ReadAllsTicks()) return 0; 18. rate.time = 0; 19. nDigits = SetSymbolInfos(); 20. ArrayResize(TicksLocal, def_MaxSizeArray); 21. m_Ticks.bTickReal = true; 22. for (int c0 = MemNTicks, c1, MemShift = nShift; c0 < m_Ticks.nTicks; c0++, nShift++) 23. { 24. if (!BuildBar1Min(c0, rate, bNew)) continue; 25. if (bNew) 26. { 27. if ((m_Ticks.nRate >= 0) && (ToReplay)) if (m_Ticks.Rate[m_Ticks.nRate].tick_volume > MaxTickVolume) 28. { 29. nShift = MemShift; 30. C_Simulation *pSimulator = new C_Simulation(nDigits); 31. if ((c1 = (*pSimulator).Simulation(m_Ticks.Rate[m_Ticks.nRate], TicksLocal, MaxTickVolume)) < 0) return 0; 32. ArrayCopy(m_Ticks.Info, TicksLocal, nShift, 0, c1); 33. nShift += c1; 34. delete pSimulator; 35. } 36. MemShift = nShift; 37. ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 2 : def_BarsDiary), def_BarsDiary); 38. }; 39. m_Ticks.Rate[(m_Ticks.nRate += (bNew ? 1 : 0))] = rate; 40. } 41. ArrayFree(TicksLocal); 42. if (!ToReplay) 43. { 44. ArrayResize(RatesLocal, (m_Ticks.nRate - MemNRates)); 45. ArrayCopy(RatesLocal, m_Ticks.Rate, 0, 0); 46. CustomRatesUpdate(def_SymbolReplay, RatesLocal, (m_Ticks.nRate - MemNRates)); 47. dtRet = m_Ticks.Rate[m_Ticks.nRate].time; 48. m_Ticks.nRate = (MemNRates == 0 ? -1 : MemNRates); 49. m_Ticks.nTicks = MemNTicks; 50. ArrayFree(RatesLocal); 51. }else m_Ticks.nTicks = nShift; 52. 53. return dtRet; 54. }; 55. //+------------------------------------------------------------------+
C_FileTicks.mqhクラスのソースコードの一部
ここでは、いくつかの項目が追加されていることに気づくかもしれません。また、特定のコマンドの実行順序にもいくつか変更が加えられています。しかしながら、このコード断片は一見すると解決策のように見えるものの、まだ小さな欠陥が残っています。それに入る前に、このC_FileTicksクラスのコード断片が、実際のティック数がシステム内部で定義された制限を超えないよう、どのように効果的に制御しているかを分析してみましょう。
この仕組みを実現するために、行6、7、11が追加されました。これらの行では、必要となる新しい変数を導入しています。そして行15では、読み込まれたティック数を追跡するための変数が初期化されます。行18と19は補助的な初期化をおこなうために追加されたもので、行20ではシミュレートされるティックを格納するメモリ領域が確保されます。このメモリは行41で解放される設計です。ただし、ここには行31に関わる小さな問題があり、これは最終バージョンで修正される予定です。今しばらくご辛抱ください。
行21は非常に重要な処理をおこなっていますが、この断片だけを見てもその理由は少し分かりにくいかもしれません。元のコードにおける行21の位置と比較してみれば、その本質が見えてきます。もし元の位置にこの処理を戻してしまうと、意図した動作を正しく果たせません。行24のBuildBar1Min関数が呼び出されるタイミングで、必要な条件が正しく整っていないためです。そのため、ここで示されているように処理の順番を調整する必要があります。
さて、本題である実装部分は、行27〜35に集中しています。このブロックは、分足においてティック数が定義された上限を超えた場合に、シミュレーションによって代替ティックを生成するための処理です。この上限値はアプリケーション内部で設定されています。
ここで注目すべきは、行27にある2つのチェックです。1つ目は、データがリプレイ用途で使われるか、あるいは既存のバーとして使用されるかを確認します。後者の場合はシミュレーションは不要です。2つ目のチェックは、ティック数が制限を超えているかを判定するものですが、これはインデックスが負の値になる可能性があるため、最初のチェックがこの問題を回避する役割も果たしています。その結果、ティック数のチェックは、直前に記録された現在のインデックスを対象とすることが保証されます。
ただし、少し疑問が生まれるかもしれません。行25の条件によって新しいバーが検出されたタイミングでこのチェックが実行されるのに、なぜ直前のバーのティック数を分析できるのかということです。もしここに気づいたなら素晴らしいです。コードの仕組みを本当に理解している証拠です。ただし、1つだけ見落としているかもしれない細かなポイントがあります。行39が実行されるまでは、システムは閉じたばかりのバーを参照し続けているのです。実際に新しいバーへの移行がおこなわれるのは行39のタイミングであり、これが実行順を厳密に制御する必要がある理由です。
では、ティックシミュレーションの流れに戻りましょう。行51を除けば、他のすべての部分はこれまでのシリーズで述べたとおりの動作を続けています。行51の動作は、シミュレーションで何が生成されるかに依存するため注意が必要です。ここには小さな問題が潜んでいますが、テストには大きな影響を与えません。
行29では、シミュレーションによって置き換える対象バーの先頭ティックにディスプレイスポインターを設定します。次に、行30でシミュレーターを初期化し、行31で実際のシミュレーションを開始します。ここで、シミュレーションに失敗した場合、行41が実行されないため、確保されたメモリが解放されないという欠陥があります。これはテスト段階では深刻な問題にはなりませんが、後ほど修正されるべき点です。ティックシミュレーションが成功すれば、行32でティックデータを格納する関数が呼び出されます。ちなみにこの処理はforループでも実装可能ですが、MQL5ライブラリのこのルーチンは高速なデータ転送に最適化されている可能性が高いため、こちらを使うほうが望ましいと考えられます。行33では、次に書き込むべき位置にポインタを更新し、行34でシミュレーションシステムを破棄します。
そして、行29の時点で常に正しい位置を参照できるよう、行36で位置情報を更新します。ただし、この一連の処理にも細かな欠陥がいくつか存在しており、それらについては次回の記事で解説します。さらに、シミュレーションフェーズにおけるほぼ見落とされがちなもう一つの問題も修正が必要です。
結論
1分足ごとのティック数を制限するという点では大きな前進がありましたが、それに伴って新たな問題が発生し、また今まで気づかなかった問題も表面化しました。この記事はすでに非常に内容が濃く、何が起きているのかを完全に理解するには丁寧に読み込む必要があるため、これ以上複雑な内容を追加するのは避けたいと思います。それでも、このコードの実装過程で現れた残りの不具合を自分で見つけて挑戦してみたい方のために、いくつかヒントをお伝えします。まず1つ目の問題は、シミュレーションすべきティックの最小数に関わるものです。これは非常に興味深い修正ポイントであり、この欠陥は、ティック数がある閾値を超えた際にシミュレーションをおこなおうとしたことで初めて明らかになりました。少し考えてみると、この問題がどのように起こるのか、きっと理解できるはずです。2つ目の問題は、シミュレートされた値をティック配列にコピーする際に発生します。このコピー処理によって、リプレイ用のバー生成システムが正しく動作しなくなることがあります。具体的には、非論理的または不自然なパターンが生成されたり、ある時間帯のティックが完全に消失してしまい、リプレイシステムの正確性と信頼性が損なわれるケースもあります。
次の記事を読む前に、ぜひこれらの問題の修正に挑戦してみてください。これは、問題の切り分け・解決方法や、自分自身のソリューションを設計・改善するスキルを磨くのに絶好のトレーニングになります。もちろん、次回の記事ではこれらの不具合をどのように修正するかを詳しく解説していきますので、ぜひ楽しみにしていてください。それでは、次の記事でお会いしましょう。
システムの欠陥を示すビデオ
すぐにこの問題を解決する方法を説明しますが、重大な問題ではないため、すぐには説明しません。この問題は、作成されたオブジェクトのロードまたはアンロードにのみ関連します。これは素晴らしい機会です。本当にプログラミングスキルをテストしたい場合は、方法を説明する前に問題を修正してみてください。あなたの解決策を私に示す必要はありませんが、私の解決策を読む前に試してみてください。これは、現在の学習レベルを評価するのに役立ちます。
MetaQuotes Ltdによりポルトガル語から翻訳されました。
元の記事: https://www.mql5.com/pt/articles/12231





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