
リプレイシステムの開発(第63回):サービスの再生(IV)
はじめに
前の記事「リプレイシステムの開発(第62回):サービスの再生(III)」では、ティックをあたかも実ティックのように扱う方法について説明しました。このような過剰な処理は本質的な問題ではありませんが、1分間という決められたウィンドウ内で正確に1分足を構築するというアプリケーションのタイミング機構に悪影響を及ぼす可能性があります。
前回の記事では一定の進展が見られたものの、実データ内でのシミュレーション実装により特定のエラーが発生したことを最後に述べました。ここで強調しておきたいのは、これらのエラーは純粋なシミュレーション環境では発生しないという点です。しかし、実データとシミュレーションを組み合わせると、こうしたエラーは不可避となります。とはいえ、これは必ずしも悪いことではなく、システムが保守・改善に耐えうるかどうかをテストする良い機会でもあります。私たちは往々にして、アイデアの実現可能性を検証するためだけに開発をおこないます。その基礎となるコンセプトが現実的でないと判断された場合は、すぐに破棄し、修正や調整にかかる時間を最小限に抑えるのです。
同じ記事の中で、私はこれらのエラーとその原因を明らかにしました。ただし、既に多くの情報を提示していたため(読者の皆さんには、これらをしっかり理解していただく必要があります)、一旦その時点で記事を終えました。しかし今回は、コードの変更によってではなく、システムのタイミング機構が過負荷でなければ本来必要のない処理をあえて実装することによって、これらのエラーに対処し修正していきます。この過負荷は、まだ必要な全機能を実装していない現段階では目立っていません。
これらの問題は相互に関連しているため、どれから手を付けても構いませんが、私は慎重な姿勢を重視し、まずは実データを用いた際に必要となる「シミュレートすべき最小限のティック数」の問題から解決していくことにします。
生成する必要があるティックの最小数は?
の問いに答えるのは、一見単純そうに見えて実はそうではありません。なぜなら、リプレイやシミュレーションを可能にするこのアプリケーションを修正する際には、常に重要な前提を念頭に置く必要があるからです。リプレイモードでは、シミュレーションクラスに最小限のティック数を使用させることで、この問題に対応できます。リプレイ機能では実際のデータを扱っていることを忘れないでください。しかし、1分間のウィンドウ内でのタイミング制御を目的としてティックをシミュレートする場合、アプローチは異なります。これは、シミュレーションが取得済みのレートデータに基づいている場合に該当します。問題は、現時点ではユーザーが使用可能なティックの最大数を調整できないことにあります。
この制限を解消するために、設定ファイルにオプションを追加し、ユーザーがこの値を自由に設定できるようにする予定です。理由はシンプルです。ユーザーのワークステーションがアプリケーションの既定設定より多くのティックを処理できるのであれば、その値を増やすことで、より現実に近いリプレイ体験が可能になります。逆に、マシンスペックが一定数のティックを処理しきれない場合は、その数を減らすことで、動作をよりスムーズかつ安定させることができます。
この問いをさらに複雑にしている要素として、「単に数値を指定すればよい」という話ではない点が挙げられます。というのも、外部でデータをシミュレートして保存し、それを実データベースのように使用することも可能だからです。これは、リプレイシステムにとって最悪のシナリオといえます。ただし、現段階ではこの点については掘り下げません。ここでの焦点は、アプリケーションの利用者による構成ミスを修正することにあります。
さて、本題に戻ります。必要なティックの最小数はいくつかという問いの答えは、「状況による」です。これを理解するには、以下のような条件を考慮する必要があります。もし始値・終値・高値・安値のすべてが同じ値であるなら、ティックは1つで足ります。これは最も単純なケースです。しかし他のケースを想定するには、いくつかのルールを設けなければなりません。最初の条件は、始値と終値は必ず高値と安値の範囲内に収まる必要があるということです。これをもとに、以下のようなケースが考えられます。
- 始値が高値または安値と一致し、終値がその反対側と一致する場合:ティックは2つ必要です。
- 始値または終値のどちらかが高値または安値と一致し、もう一方がそれらの値の外側にある場合:ティックは3つ必要です。
- OHLC(始値・高値・安値・終値)のすべてが異なる場合、または始値と終値が同一だが、高値または安値と一致しない場合:ティックは4つ必要です。
このロジックが、シミュレーションに必要なティックの最小数を判断するための基礎となります。これまでのところ、シミュレーションは順調に動作しています。ただし、今後はユーザーが任意の上限値を設定できるようにして、それぞれのワークステーションにとって最適なリプレイ/シミュレーションがおこなえるようにしたいと考えています。
この説明は文章ではやや複雑に思えるかもしれませんが、コードで見ると遥かに理解しやすくなります。したがって、ここではC_Simulation.mqhファイル全体を掲載するのではなく、該当部分のコード断片のみを以下に示します。これは、既存の問題を解決するために修正を加えた箇所です。
128. //+------------------------------------------------------------------+ 129. inline int Simulation(const MqlRates &rate, MqlTick &tick[], const int MaxTickVolume) 130. { 131. int i0, i1, i2, dm = 0; 132. bool b0; 133. 134. m_Marks.iMax = (MaxTickVolume <= 0 ? 1 : (MaxTickVolume >= def_MaxTicksVolume ? def_MaxTicksVolume : MaxTickVolume)); 135. dm = (dm == 0 ? ((rate.open == rate.high == rate.low == rate.close) ? 1 : dm) : dm); 136. dm = (dm == 0 ? (((rate.open == rate.high) && (rate.low == rate.close)) || ((rate.open == rate.low) && (rate.close == rate.high)) ? 2 : dm) : dm); 137. if ((dm == 0 ? ((rate.open == rate.close == rate.high) || (rate.open == rate.close == rate.low) ? 3 : 4) : dm) == 0) return -1; 138. m_Marks.iMax = (MaxTickVolume <= dm ? dm : MaxTickVolume); 139. m_Marks.iMax = (((int)rate.tick_volume > m_Marks.iMax) || ((int)rate.tick_volume < dm) ? m_Marks.iMax : (int)rate.tick_volume - 1); 140. m_Marks.bHigh = (rate.open == rate.high) || (rate.close == rate.high); 141. m_Marks.bLow = (rate.open == rate.low) || (rate.close == rate.low); 142. Simulation_Time(rate, tick); 143. MountPrice(0, rate.open, rate.spread, tick); 144. if (m_Marks.iMax > 10) 145. { 146. i0 = (int)(MathMin(m_Marks.iMax / 3.0, m_Marks.iMax * 0.2)); 147. i1 = m_Marks.iMax - i0; 148. i2 = (int)(((rate.high - rate.low) / m_TickSize) / i0); 149. i2 = (i2 == 0 ? 1 : i2); 150. b0 = (m_Marks.iMax >= 1000 ? ((rand() & 1) == 1) : (rate.high - rate.open) < (rate.open - rate.low)); 151. i0 = RandomWalk(1, i0, rate.open, (b0 ? rate.high : rate.low), rate.high, rate.low, rate.spread, tick, 0, i2); 152. 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); 153. RandomWalk(i1, m_Marks.iMax, (m_IsPriceBID ? tick[i1].bid : tick[i1].last), rate.close, rate.high, rate.low, rate.spread, tick, 2, i2); 154. m_Marks.bHigh = m_Marks.bLow = true; 155. 156. }else Random_Price(rate, tick); 157. if (!m_IsPriceBID) DistributeVolumeReal(rate, tick); 158. if (!m_Marks.bLow) MountPrice(Unique(rate.high, tick), rate.low, rate.spread, tick); 159. if (!m_Marks.bHigh) MountPrice(Unique(rate.low, tick), rate.high, rate.spread, tick); 160. MountPrice(m_Marks.iMax, rate.close, rate.spread, tick); 161. CorretTime(tick); 162. 163. return m_Marks.iMax; 164. } 165. //+------------------------------------------------------------------+
ファイルC_Simulation.mqhの一部
このフラグメント内の行番号は、前回の記事に掲載された完全なコードのうち、どこを修正すべきかを正確に示しています。131行目では、新しい変数が追加され、宣言と同時に初期化されていることに注目してください。また、元の134行目は削除し、3行の新しいコードに置き換える必要があります。これらの3行は、先ほど説明した要点を実装し、必要最小限のティックが生成されるようにします。
ただし、非常に重要なポイントがあるため、ここには細心の注意が必要です。138行目の三項演算子では、MaxTickVolumeの値が調整後の値と比較され、最小のティック数が決定されます。MaxTickVolumeがこの調整後の値より小さい場合は、他のどのデータよりも優先して、その調整後の値が使用されます。さらに139行目では、同じ条件が再度チェックされます。rate.tick_volumeの値も調整後の値より小さい場合には、やはり調整後の値が優先されます。
このシミュレーション関数の戻り値は、特に他の用途に使う予定がある場合は必ずチェックしてください。というのも、最小ティック数の調整中にエラーが発生すると、関数は137行目で即座に終了するためです。そのため、返された配列の値が無効である可能性があり、関数の戻り値を確認せずにそれらの値を使用するべきではありません。
前回の記事で紹介したコードと比較すると、今回はごくわずかな変更が加えられています。ただし、この記事の最後で再度このフラグメントを取り上げ、今回の変更内容がしっかりと理解できるように説明を補足します。
これで最初の問題は解決しました。では次のトピックに進み、2番目の問題に取り組みましょう。
ティック調整問題の修正
この2つ目の問題を解決するには、もう少し手間がかかります。ただし、手間がかかるからといって、必ずしも難易度が高いわけではありません。単に、より多くの作業が必要になるというだけのことです。それでは、次のステップに進みましょう。前回の記事で紹介した、動きの処理とティックの調整を担当するコードフラグメントを確認し、チャート描画に使用できることを確実にするための見直しを行います。以下にそのコード断片を示します。
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ファイルの一部
このコードには修正が必要なエラーが含まれていることに注意してください。この記事では、それらを修正していきます。それでは、次のコードフラグメントに注意深く目を通し、前回のものと比較してみましょう。
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. m_Ticks.bTickReal = true; 21. for (int c0 = MemNTicks, c1, MemShift = nShift; c0 < m_Ticks.nTicks; c0++, nShift++) 22. { 23. if (nShift != c0) m_Ticks.Info[nShift] = m_Ticks.Info[c0]; 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. ArrayResize(TicksLocal, def_MaxSizeArray); 31. C_Simulation *pSimulator = new C_Simulation(nDigits); 32. if ((c1 = (*pSimulator).Simulation(m_Ticks.Rate[m_Ticks.nRate], TicksLocal, MaxTickVolume)) > 0) 33. nShift += ArrayCopy(m_Ticks.Info, TicksLocal, nShift, 0, c1); 34. delete pSimulator; 35. ArrayFree(TicksLocal); 36. if (c1 < 0) return 0; 37. } 38. MemShift = nShift; 39. ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 2 : def_BarsDiary), def_BarsDiary); 40. }; 41. m_Ticks.Rate[(m_Ticks.nRate += (bNew ? 1 : 0))] = rate; 42. } 43. if (!ToReplay) 44. { 45. ArrayResize(RatesLocal, (m_Ticks.nRate - MemNRates)); 46. ArrayCopy(RatesLocal, m_Ticks.Rate, 0, 0); 47. CustomRatesUpdate(def_SymbolReplay, RatesLocal, (m_Ticks.nRate - MemNRates)); 48. dtRet = m_Ticks.Rate[m_Ticks.nRate].time; 49. m_Ticks.nRate = (MemNRates == 0 ? -1 : MemNRates); 50. m_Ticks.nTicks = MemNTicks; 51. ArrayFree(RatesLocal); 52. }else m_Ticks.nTicks = nShift; 53. 54. return dtRet; 55. }; 56. //+------------------------------------------------------------------+
C_FileTicks.mqhファイルの一部(最終版)
違いがお判りでしょうか。それほど大きな変更ではありませんが、確かに存在します。特定の操作の実行順序をいくつか変更したことにお気づきかもしれません。最も顕著なのは、シミュレーション中に生成されたティックに対するメモリの割り当ておよび解放の処理です。LoadTicks関数は主要な関数であり、システムが完全に初期化されてパフォーマンス要求が始まる前に実行されるため、メモリの割り当てや解放で多少の時間を消費しても問題にはなりません。
もしこの時間的コストが許容できないと感じる場合は、実行順序を適宜変更して構いません。ただし、以下に示す修正済みのコードフラグメントでは、シミュレーションクラスのデストラクタを障害発生時に確実に呼び出すことが非常に重要である点を見逃さないでください。コードを比較すると、修正後のバージョンでは、関数は36行目でのみ失敗としてリターンされます。しかしその前に、34行目と35行目で、明示的にデストラクタを呼び出し、割り当てられたメモリをその順番通りに解放しています。
シミュレーションが成功し、データの移行が可能な場合は、33行目でこの処理がおこなわれ、ライブラリ関数の戻り値を使用して新しいオフセット値を更新します。
この対応により、以前はシミュレーションが失敗して戻ったときに発生していた問題を解決できます。しかし、ここにはもう一つ説明すべき重要な点があります。修正済みのコードフラグメントを見ると、ある箇所に違和感を覚えるはずです。一見すると意味が通らないかもしれません。それが23行目です。なぜ23行目ではティックカウンタとオフセット値を比較しているのでしょうか。このチェックの目的は何でしょうか。
確かに、意味がないように思えるかもしれません。しかし、実際にシミュレーターを実行すると、オフセット値はティックカウンタと一致しなくなります。その結果、一部の実ティックが誤ったインデックスで処理されてしまいます。この状態を放置しておくと、52行目が実行された際に、いくつかの実ティックが消失する可能性があります。さらに悪いことに、シミュレートされたバーと非シミュレートのバーとの間に、インデックスのずれによって不正なティックが表示されることもあります。
これで、この問題の本質を理解いただけたと思います。したがって、23行目に実ティックの検証と再配置のためのチェックを追加すれば、すべてが正しく実行され、チャート上に異常が現れることはなくなります。非常にシンプルな修正ではありますが、問題を完全に解決するものです。最終的なコードは、C_FileTicks.mqhファイルに記述されています。
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. //+------------------------------------------------------------------+ 004. #include "C_FileBars.mqh" 005. #include "C_Simulation.mqh" 006. //+------------------------------------------------------------------+ 007. #define macroRemoveSec(A) (A - (A % 60)) 008. #define def_MaxSizeArray 16777216 // 16 Mbytes 009. //+------------------------------------------------------------------+ 010. class C_FileTicks 011. { 012. protected: 013. enum ePlotType {PRICE_EXCHANGE, PRICE_FOREX}; 014. struct stInfoTicks 015. { 016. MqlTick Info[]; 017. MqlRates Rate[]; 018. int nTicks, 019. nRate; 020. bool bTickReal; 021. ePlotType ModePlot; 022. }; 023. //+------------------------------------------------------------------+ 024. inline bool BuildBar1Min(const int iArg, MqlRates &rate, bool &bNew) 025. { 026. double dClose = 0; 027. 028. switch (m_Ticks.ModePlot) 029. { 030. case PRICE_EXCHANGE: 031. if (m_Ticks.Info[iArg].last == 0.0) return false; 032. dClose = m_Ticks.Info[iArg].last; 033. break; 034. case PRICE_FOREX: 035. dClose = (m_Ticks.Info[iArg].bid > 0.0 ? m_Ticks.Info[iArg].bid : dClose); 036. if ((dClose == 0.0) || (m_Ticks.Info[iArg].bid == 0.0)) return false; 037. break; 038. } 039. if (bNew = (rate.time != macroRemoveSec(m_Ticks.Info[iArg].time))) 040. { 041. rate.time = macroRemoveSec(m_Ticks.Info[iArg].time); 042. rate.real_volume = 0; 043. rate.tick_volume = (m_Ticks.ModePlot == PRICE_FOREX ? 1 : 0); 044. rate.open = rate.low = rate.high = rate.close = dClose; 045. }else 046. { 047. rate.close = dClose; 048. rate.high = (rate.close > rate.high ? rate.close : rate.high); 049. rate.low = (rate.close < rate.low ? rate.close : rate.low); 050. rate.real_volume += (long) m_Ticks.Info[iArg].volume_real; 051. rate.tick_volume += (m_Ticks.bTickReal ? 1 : (int)m_Ticks.Info[iArg].volume); 052. } 053. 054. return true; 055. } 056. //+------------------------------------------------------------------+ 057. private : 058. int m_File; 059. stInfoTicks m_Ticks; 060. //+------------------------------------------------------------------+ 061. inline bool Open(const string szFileNameCSV) 062. { 063. string szInfo = ""; 064. 065. if ((m_File = FileOpen("Market Replay\\Ticks\\" + szFileNameCSV + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE) 066. { 067. for (int c0 = 0; c0 < 7; c0++) szInfo += FileReadString(m_File); 068. if (szInfo == "<DATE><TIME><BID><ASK><LAST><VOLUME><FLAGS>") return true; 069. Print("File ", szFileNameCSV, ".csv not a traded tick file."); 070. }else 071. Print("Tick file ", szFileNameCSV,".csv not found..."); 072. 073. return false; 074. } 075. //+------------------------------------------------------------------+ 076. inline bool ReadAllsTicks(void) 077. { 078. string szInfo; 079. 080. Print("Loading replay ticks. Please wait..."); 081. ArrayResize(m_Ticks.Info, def_MaxSizeArray, def_MaxSizeArray); 082. m_Ticks.ModePlot = PRICE_FOREX; 083. while ((!FileIsEnding(m_File)) && (m_Ticks.nTicks < (INT_MAX - 2)) && (!_StopFlag)) 084. { 085. ArrayResize(m_Ticks.Info, m_Ticks.nTicks + 1, def_MaxSizeArray); 086. szInfo = FileReadString(m_File) + " " + FileReadString(m_File); 087. m_Ticks.Info[m_Ticks.nTicks].time = StringToTime(StringSubstr(szInfo, 0, 19)); 088. m_Ticks.Info[m_Ticks.nTicks].time_msc = (m_Ticks.Info[m_Ticks.nTicks].time * 1000) + (int)StringToInteger(StringSubstr(szInfo, 20, 3)); 089. m_Ticks.Info[m_Ticks.nTicks].bid = StringToDouble(FileReadString(m_File)); 090. m_Ticks.Info[m_Ticks.nTicks].ask = StringToDouble(FileReadString(m_File)); 091. m_Ticks.Info[m_Ticks.nTicks].last = StringToDouble(FileReadString(m_File)); 092. m_Ticks.Info[m_Ticks.nTicks].volume_real = StringToDouble(FileReadString(m_File)); 093. m_Ticks.Info[m_Ticks.nTicks].flags = (uchar)StringToInteger(FileReadString(m_File)); 094. m_Ticks.ModePlot = (m_Ticks.Info[m_Ticks.nTicks].volume_real > 0.0 ? PRICE_EXCHANGE : m_Ticks.ModePlot); 095. m_Ticks.nTicks++; 096. } 097. FileClose(m_File); 098. if (m_Ticks.nTicks == (INT_MAX - 2)) 099. { 100. Print("Too much data in tick file.\nIt is not possible to continue..."); 101. return false; 102. } 103. return (!_StopFlag); 104. } 105. //+------------------------------------------------------------------+ 106. int SetSymbolInfos(void) 107. { 108. int iRet; 109. 110. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, iRet = (m_Ticks.ModePlot == PRICE_EXCHANGE ? 4 : 5)); 111. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_TRADE_CALC_MODE, m_Ticks.ModePlot == PRICE_EXCHANGE ? SYMBOL_CALC_MODE_EXCH_STOCKS : SYMBOL_CALC_MODE_FOREX); 112. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_CHART_MODE, m_Ticks.ModePlot == PRICE_EXCHANGE ? SYMBOL_CHART_MODE_LAST : SYMBOL_CHART_MODE_BID); 113. 114. return iRet; 115. } 116. //+------------------------------------------------------------------+ 117. public : 118. //+------------------------------------------------------------------+ 119. C_FileTicks() 120. { 121. ArrayResize(m_Ticks.Rate, def_BarsDiary); 122. m_Ticks.nRate = -1; 123. m_Ticks.nTicks = 0; 124. m_Ticks.Rate[0].time = 0; 125. } 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. for (int c0 = 0; c0 <= iRet; c0++) 153. { 154. ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray); 155. m_Ticks.Info[m_Ticks.nTicks++] = local[c0]; 156. } 157. } 158. ArrayFree(local); 159. delete pFileBars; 160. delete pSimulator; 161. m_Ticks.bTickReal = false; 162. 163. return ((!_StopFlag) && (iMem != m_Ticks.nTicks) && (iRet > 0)); 164. } 165. //+------------------------------------------------------------------+ 166. datetime LoadTicks(const string szFileNameCSV, const bool ToReplay, const int MaxTickVolume) 167. { 168. int MemNRates, 169. MemNTicks, 170. nDigits, 171. nShift; 172. datetime dtRet = TimeCurrent(); 173. MqlRates RatesLocal[], 174. rate; 175. MqlTick TicksLocal[]; 176. bool bNew; 177. 178. MemNRates = (m_Ticks.nRate < 0 ? 0 : m_Ticks.nRate); 179. nShift = MemNTicks = m_Ticks.nTicks; 180. if (!Open(szFileNameCSV)) return 0; 181. if (!ReadAllsTicks()) return 0; 182. rate.time = 0; 183. nDigits = SetSymbolInfos(); 184. m_Ticks.bTickReal = true; 185. for (int c0 = MemNTicks, c1, MemShift = nShift; c0 < m_Ticks.nTicks; c0++, nShift++) 186. { 187. if (nShift != c0) m_Ticks.Info[nShift] = m_Ticks.Info[c0]; 188. if (!BuildBar1Min(c0, rate, bNew)) continue; 189. if (bNew) 190. { 191. if ((m_Ticks.nRate >= 0) && (ToReplay)) if (m_Ticks.Rate[m_Ticks.nRate].tick_volume > MaxTickVolume) 192. { 193. nShift = MemShift; 194. ArrayResize(TicksLocal, def_MaxSizeArray); 195. C_Simulation *pSimulator = new C_Simulation(nDigits); 196. if ((c1 = (*pSimulator).Simulation(m_Ticks.Rate[m_Ticks.nRate], TicksLocal, MaxTickVolume)) > 0) 197. nShift += ArrayCopy(m_Ticks.Info, TicksLocal, nShift, 0, c1); 198. delete pSimulator; 199. ArrayFree(TicksLocal); 200. if (c1 < 0) return 0; 201. } 202. MemShift = nShift; 203. ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 2 : def_BarsDiary), def_BarsDiary); 204. }; 205. m_Ticks.Rate[(m_Ticks.nRate += (bNew ? 1 : 0))] = rate; 206. } 207. if (!ToReplay) 208. { 209. ArrayResize(RatesLocal, (m_Ticks.nRate - MemNRates)); 210. ArrayCopy(RatesLocal, m_Ticks.Rate, 0, 0); 211. CustomRatesUpdate(def_SymbolReplay, RatesLocal, (m_Ticks.nRate - MemNRates)); 212. dtRet = m_Ticks.Rate[m_Ticks.nRate].time; 213. m_Ticks.nRate = (MemNRates == 0 ? -1 : MemNRates); 214. m_Ticks.nTicks = MemNTicks; 215. ArrayFree(RatesLocal); 216. }else m_Ticks.nTicks = nShift; 217. 218. return dtRet; 219. }; 220. //+------------------------------------------------------------------+ 221. inline stInfoTicks GetInfoTicks(void) const 222. { 223. return m_Ticks; 224. } 225. //+------------------------------------------------------------------+ 226. }; 227. //+------------------------------------------------------------------+ 228. #undef def_MaxSizeArray 229. //+------------------------------------------------------------------+
ヘッダーファイルC_FileTicks.mqh
これらの問題が解決したので、次のステップに進みましょう。ここでは、1分足内で許可されるティックの最大数をユーザーが定義できるようにする処理をおこないます。整理のため、この内容は新しいセクションで取り上げます。
ユーザーによる調整を可能にする
この部分は、間違いなく最も簡単でシンプル、かつ実装していて楽しい作業です。なぜなら、開発者としておこなうべき実質的な作業は、調整が必要な値を設定するために使用されるキー名を定義することだけだからです。
このキーとは、ユーザーがアセットの構成ファイルに記述すべき値を意味します。この方法については、これまでの連載記事で既に紹介してきました。ただし、今回の実装は非常にシンプルであるため、コードを細かく分割して解説することはしません。その代わりに、最終的なコードを確認する前に、この新機能がどのように使われるのかを例で見ていきましょう。以下は、リプレイ/シミュレーターアプリケーション用のサンプル構成ファイルです。例は以下のとおりです。
01. [Config] 02. Path = WDO 03. PointsPerTick = 0.5 04. ValuePerPoints = 5.0 05. VolumeMinimal = 1.0 06. Account = NETTING 07. MaxTicksPerBar = 2800 08. 09. [Bars] 10. WDON22_M1_202206140900_202206141759 11. 12. [ Ticks -> Bars] 13. 14. [ Bars -> Ticks ] 15. 16. [Ticks] 17. WDON22_202206150900_202206151759
構成ファイルの例
7行目に導入された新しい構成設定に注目してください。この構成ファイルを、MetaTrader 5のリプレイ/シミュレーションアプリケーションの旧バージョンで使用しようとすると、7行目に認識されないパラメータが含まれているというエラーメッセージが表示されます。しかし、ここで紹介している新しいバージョンでは、アプリケーションが7行目の内容を正しく解釈できるようになっています。
ここで重要な注意点があります。それは、開発者・エンドユーザーのどちらも、必ずしも7行目の新しい設定を記述する必要はないということです。もしこの設定が記述されていれば、その構成値がコンパイル済みアプリケーションに埋め込まれたデフォルト値を上書きします。一方、省略された場合は、リプレイ/シミュレーターサービスがコンパイル時に内部的に定義されたデフォルト値を使用します。
コードを紹介する前にこの点を強調しておきたいのは、この構成設定が完全にオプションであるという事実を明確にするためです。ただし、指定された場合は、プリコンパイル済みの値よりも優先されて使用されます。もうひとつ覚えておいてほしいのは、各構成ファイルはそれぞれ独立しているという点です。つまり、ファイルごとに異なる最大ティック数を定義することが可能です。MetaTrader 5で快適なパフォーマンスを維持しつつ、チャートがスムーズに描画されるよう、さまざまな設定を自由に試してみてください。
それでは、構成ファイルからこの値を読み取り、適用する役割を担う更新済みのヘッダーファイルを確認しましょう。ここで取り上げるのはC_ConfigService.mqhです。以下に、修正後のコード全文を掲載します。
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. //+------------------------------------------------------------------+ 004. #include "Support\C_FileBars.mqh" 005. #include "Support\C_FileTicks.mqh" 006. #include "Support\C_Array.mqh" 007. //+------------------------------------------------------------------+ 008. class C_ConfigService : protected C_FileTicks 009. { 010. protected: 011. //+------------------------------------------------------------------+ 012. private : 013. enum eWhatExec {eTickReplay, eBarToTick, eTickToBar, eBarPrev}; 014. enum eTranscriptionDefine {Transcription_INFO, Transcription_DEFINE}; 015. struct st001 016. { 017. C_Array *pTicksToReplay, *pBarsToTicks, *pTicksToBars, *pBarsToPrev; 018. int Line, 019. MaxTickVolume; 020. bool AccountHedging; 021. char ModelLoading; 022. string szPath; 023. }m_GlPrivate; 024. //+------------------------------------------------------------------+ 025. inline void FirstBarNULL(void) 026. { 027. MqlRates rate[1]; 028. int c0 = 0; 029. 030. for(; (GetInfoTicks().ModePlot == PRICE_EXCHANGE) && (GetInfoTicks().Info[c0].volume_real == 0); c0++); 031. rate[0].close = (GetInfoTicks().ModePlot == PRICE_EXCHANGE ? GetInfoTicks().Info[c0].last : GetInfoTicks().Info[c0].bid); 032. rate[0].open = rate[0].high = rate[0].low = rate[0].close; 033. rate[0].tick_volume = 0; 034. rate[0].real_volume = 0; 035. rate[0].time = macroRemoveSec(GetInfoTicks().Info[c0].time) - 86400; 036. CustomRatesUpdate(def_SymbolReplay, rate); 037. } 038. //+------------------------------------------------------------------+ 039. inline eTranscriptionDefine GetDefinition(const string &In, string &Out) 040. { 041. string szInfo; 042. 043. szInfo = In; 044. Out = ""; 045. StringToUpper(szInfo); 046. StringTrimLeft(szInfo); 047. StringTrimRight(szInfo); 048. if (StringSubstr(szInfo, 0, 1) == "#") return Transcription_INFO; 049. if (StringSubstr(szInfo, 0, 1) != "[") 050. { 051. Out = szInfo; 052. return Transcription_INFO; 053. } 054. for (int c0 = 0; c0 < StringLen(szInfo); c0++) 055. if (StringGetCharacter(szInfo, c0) > ' ') 056. StringAdd(Out, StringSubstr(szInfo, c0, 1)); 057. 058. return Transcription_DEFINE; 059. } 060. //+------------------------------------------------------------------+ 061. inline bool Configs(const string szInfo) 062. { 063. const string szList[] = { 064. "PATH", 065. "POINTSPERTICK", 066. "VALUEPERPOINTS", 067. "VOLUMEMINIMAL", 068. "LOADMODEL", 069. "ACCOUNT", 070. "MAXTICKSPERBAR" 071. }; 072. string szRet[]; 073. char cWho; 074. 075. if (StringSplit(szInfo, '=', szRet) == 2) 076. { 077. StringTrimRight(szRet[0]); 078. StringTrimLeft(szRet[1]); 079. for (cWho = 0; cWho < ArraySize(szList); cWho++) if (szList[cWho] == szRet[0]) break; 080. switch (cWho) 081. { 082. case 0: 083. m_GlPrivate.szPath = szRet[1]; 084. return true; 085. case 1: 086. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, StringToDouble(szRet[1])); 087. return true; 088. case 2: 089. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, StringToDouble(szRet[1])); 090. return true; 091. case 3: 092. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, StringToDouble(szRet[1])); 093. return true; 094. case 4: 095. m_GlPrivate.ModelLoading = StringInit(szRet[1]); 096. m_GlPrivate.ModelLoading = ((m_GlPrivate.ModelLoading < 1) && (m_GlPrivate.ModelLoading > 4) ? 1 : m_GlPrivate.ModelLoading); 097. return true; 098. case 5: 099. if (szRet[1] == "HEDGING") m_GlPrivate.AccountHedging = true; 100. else if (szRet[1] == "NETTING") m_GlPrivate.AccountHedging = false; 101. else 102. { 103. Print("Entered account type is not invalid."); 104. return false; 105. } 106. return true; 107. case 6: 108. m_GlPrivate.MaxTickVolume = (int) MathAbs(StringToInteger(szRet[1])); 109. return true; 110. } 111. Print("Variable >>", szRet[0], "<< not defined."); 112. }else 113. Print("Configuration definition >>", szInfo, "<< invalidates."); 114. 115. return false; 116. } 117. //+------------------------------------------------------------------+ 118. inline bool WhatDefine(const string szArg, char &cStage) 119. { 120. const string szList[] = { 121. "[BARS]", 122. "[TICKS]", 123. "[TICKS->BARS]", 124. "[BARS->TICKS]", 125. "[CONFIG]" 126. }; 127. 128. cStage = 1; 129. for (char c0 = 0; c0 < ArraySize(szList); c0++, cStage++) 130. if (szList[c0] == szArg) return true; 131. 132. return false; 133. } 134. //+------------------------------------------------------------------+ 135. inline bool CMD_Array(char &cError, eWhatExec e1) 136. { 137. bool bBarsPrev = false; 138. string szInfo; 139. C_FileBars *pFileBars; 140. C_Array *ptr = NULL; 141. 142. switch (e1) 143. { 144. case eTickReplay : ptr = m_GlPrivate.pTicksToReplay; break; 145. case eTickToBar : ptr = m_GlPrivate.pTicksToBars; break; 146. case eBarToTick : ptr = m_GlPrivate.pBarsToTicks; break; 147. case eBarPrev : ptr = m_GlPrivate.pBarsToPrev; break; 148. } 149. if (ptr != NULL) 150. { 151. for (int c0 = 0; (c0 < INT_MAX) && (cError == 0); c0++) 152. { 153. if ((szInfo = ptr.At(c0, m_GlPrivate.Line)) == "") break; 154. switch (e1) 155. { 156. case eTickReplay: 157. if (LoadTicks(szInfo, true, m_GlPrivate.MaxTickVolume) == 0) cError = 4; 158. break; 159. case eTickToBar : 160. if (LoadTicks(szInfo, false, m_GlPrivate.MaxTickVolume) == 0) cError = 5; else bBarsPrev = true; 161. break; 162. case eBarToTick : 163. if (!BarsToTicks(szInfo, m_GlPrivate.MaxTickVolume)) cError = 6; 164. break; 165. case eBarPrev : 166. pFileBars = new C_FileBars(szInfo); 167. if ((*pFileBars).LoadPreView() == 0) cError = 3; else bBarsPrev = true; 168. delete pFileBars; 169. break; 170. } 171. } 172. delete ptr; 173. } 174. 175. return bBarsPrev; 176. } 177. //+------------------------------------------------------------------+ 178. public : 179. //+------------------------------------------------------------------+ 180. C_ConfigService() 181. :C_FileTicks() 182. { 183. m_GlPrivate.AccountHedging = false; 184. m_GlPrivate.ModelLoading = 1; 185. m_GlPrivate.MaxTickVolume = 2000; 186. } 187. //+------------------------------------------------------------------+ 188. inline const bool TypeAccountIsHedging(void) const 189. { 190. return m_GlPrivate.AccountHedging; 191. } 192. //+------------------------------------------------------------------+ 193. bool SetSymbolReplay(const string szFileConfig) 194. { 195. #define macroFileName ((m_GlPrivate.szPath != NULL ? m_GlPrivate.szPath + "\\" : "") + szInfo) 196. int file; 197. char cError, 198. cStage; 199. string szInfo; 200. bool bBarsPrev; 201. 202. if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE) 203. { 204. Print("Failed to open configuration file [", szFileConfig, "]. Service being terminated..."); 205. return false; 206. } 207. Print("Loading data for playback. Please wait...."); 208. cError = cStage = 0; 209. bBarsPrev = false; 210. m_GlPrivate.Line = 1; 211. m_GlPrivate.pTicksToReplay = m_GlPrivate.pTicksToBars = m_GlPrivate.pBarsToTicks = m_GlPrivate.pBarsToPrev = NULL; 212. while ((!FileIsEnding(file)) && (!_StopFlag) && (cError == 0)) 213. { 214. switch (GetDefinition(FileReadString(file), szInfo)) 215. { 216. case Transcription_DEFINE: 217. cError = (WhatDefine(szInfo, cStage) ? 0 : 1); 218. break; 219. case Transcription_INFO: 220. if (szInfo != "") switch (cStage) 221. { 222. case 0: 223. cError = 2; 224. break; 225. case 1: 226. if (m_GlPrivate.pBarsToPrev == NULL) m_GlPrivate.pBarsToPrev = new C_Array(); 227. (*m_GlPrivate.pBarsToPrev).Add(macroFileName, m_GlPrivate.Line); 228. break; 229. case 2: 230. if (m_GlPrivate.pTicksToReplay == NULL) m_GlPrivate.pTicksToReplay = new C_Array(); 231. (*m_GlPrivate.pTicksToReplay).Add(macroFileName, m_GlPrivate.Line); 232. break; 233. case 3: 234. if (m_GlPrivate.pTicksToBars == NULL) m_GlPrivate.pTicksToBars = new C_Array(); 235. (*m_GlPrivate.pTicksToBars).Add(macroFileName, m_GlPrivate.Line); 236. break; 237. case 4: 238. if (m_GlPrivate.pBarsToTicks == NULL) m_GlPrivate.pBarsToTicks = new C_Array(); 239. (*m_GlPrivate.pBarsToTicks).Add(macroFileName, m_GlPrivate.Line); 240. break; 241. case 5: 242. if (!Configs(szInfo)) cError = 7; 243. break; 244. } 245. break; 246. }; 247. m_GlPrivate.Line += (cError > 0 ? 0 : 1); 248. } 249. FileClose(file); 250. CMD_Array(cError, (m_GlPrivate.ModelLoading <= 2 ? eTickReplay : eBarToTick)); 251. CMD_Array(cError, (m_GlPrivate.ModelLoading <= 2 ? eBarToTick : eTickReplay)); 252. bBarsPrev = (CMD_Array(cError, ((m_GlPrivate.ModelLoading & 1) == 1 ? eTickToBar : eBarPrev)) ? true : bBarsPrev); 253. bBarsPrev = (CMD_Array(cError, ((m_GlPrivate.ModelLoading & 1) == 1 ? eBarPrev : eTickToBar)) ? true : bBarsPrev); 254. switch(cError) 255. { 256. case 0: 257. if (GetInfoTicks().nTicks <= 0) 258. { 259. Print("There are no ticks to use. Service is being terminated..."); 260. cError = -1; 261. }else if (!bBarsPrev) FirstBarNULL(); 262. break; 263. case 1 : Print("The command on the line ", m_GlPrivate.Line, " not recognized by the system..."); break; 264. case 2 : Print("The system did not expect the contents of the line: ", m_GlPrivate.Line); break; 265. default : Print("Error accessing the file indicated in the line: ", m_GlPrivate.Line); 266. } 267. 268. return (cError == 0 ? !_StopFlag : false); 269. #undef macroFileName 270. } 271. //+------------------------------------------------------------------+ 272. }; 273. //+------------------------------------------------------------------+
ヘッダーファイルC_ConfigService.mqhのソースコード
C_ConfigServiceクラスのコードは、本当に扱いやすく、開発者にとって嬉しい存在です。なぜなら、最小限の変更で多くの成果を得られる、非常に効率的な設計になっているからです。こうしたコードに出会えるのは稀なことです。さて、本題に入りましょう。このクラスは、リプレイやシミュレーションに必要なデータを読み込むために、基盤となる複数のレイヤーが連携して動作する中心的な役割を担っています。すべての処理は、先ほど紹介した構成ファイルの内容に従って適切に制御されます。
このコードで最初におこなったのは、新しい変数の定義です。19行目にその定義があり、名前からも今後の処理内容が直感的に理解できるようになっています。この変数は、185行目にあるクラスのコンストラクタ内で初期化されています。そのため、構成ファイルで別の値が指定されていない場合は、ここで設定されたデフォルト値が使用される仕組みです。
それなら、なぜDefines.mqhにあったdef_MaxTicksVolumeを使わないのかと思われるかもしれません。その理由は単純で、その定義はもはや存在しないからです。構成ファイルベースの設定に移行したことで、特定のハードコード値に依存する必要がなくなりました。そのためdef_MaxTicksVolumeは不要と判断され、Defines.mqhから削除されました。もちろん、それを手元に残しておきたいのであれば問題はありませんが、今後の記事でDefines.mqhを再確認する際にその定義が存在しなくても驚かないでください。
では、なぜこの定義が不要になったのかを詳しく見ていきましょう。1分足内でティック数がオーバーフローした際に、それを再現・シミュレートできるようにする機能を開発していた当初、どこからその情報を取得するかを決めていませんでした。コード内に複数の値が散らばるのを避けるため、一時的に汎用の定義としてdef_MaxTicksVolumeを用意し、Defines.mqhに配置していたのです。これは、後の機能拡張や調整を見据えた設計判断でした。しかし、C_ConfigServiceクラスの設計に進んだ段階で、この値をユーザーが自由に変更できるようにし、プロジェクト全体を再コンパイルせずに済ませたいというアイデアが浮かびました。
幸い、難しい部分を担うクラス群はすでに実装されていたため、必要だったのはこの最大ティック数をそれらのクラスに渡す処理を追加することだけです。コードのごく一部に軽微な修正を加えるだけで、これらのクラスがアプリケーション全体の設定値を受け取り、適用できるようになりました。その処理は157行目、160行目、163行目でおこなわれています。
新たなロジックを追加したわけではなく、既存機能を補完するかたちで小さな調整を加えただけです。それでも、これにより1分足内で許容される最大ティック数をユーザー側で柔軟に制御できるようになります。
さて、そもそもすべてをこのクラスに集約した本当の理由は、ユーザーが1分足に含めることができるティックの最大数を定義できるようにすることです。コンテキストがない状態でこのコードだけを見ると、複雑で大がかりな機能実装だと思われるかもしれません。しかし実際には、シンプルで実装も非常に容易です。
まず最初のステップは、構成用のキーを追加することです。これは、設定パラメータ用のキーを格納している文字列配列に1つ項目を加えるだけで完了します。この配列は63行目で定義されています。ここで注意が必要なのは、これまでにも説明しましたが、繰り返しておく価値があります。新しいキーを追加する際には、必ず大文字で定義してください。キーの内容が何であっても、大文字であることが重要です。今回追加した新しいキーは、70行目で定義されています。そしてもう一つのポイントは、75行目です。ここでは、MQL5ライブラリの関数を使って、キーと値のペアを分割しています。区切り文字として等号(=)が使用されており、等号の前にある文字列がキー、後ろにある文字列がそのキーに対する値として扱われます。
次に重要なのは79行目です。ここでは、配列内からキーを検索し、そのインデックスを取得しています。この仕組みにより、もしキーの配列内での順序を変更した場合には、それに対応して後続のコードで使われるインデックス値も更新する必要があります。難しい作業ではありませんが、注意が必要です。
今回の場合、新しい設定のインデックスは6です。したがって、107行目でこの設定がどのように適用されるかを定義しています。ここでは、整数の値を想定しているため、別のMQL5ライブラリ関数を使って文字列を整数に変換しています。これにより、ユーザーは1分足に対して、最大ティック数として使用する値を設定できるようになります。
重要な注意点があります。108行目で構成ファイルの値を整数に変換していますが、その値が許容されるパラメータ範囲内であるかどうかの検証チェックはおこなっていません。唯一の安全策は、値が正の数であることを確認することです。値が矛盾していたり無効であったりした場合、シミュレーション処理が失敗する可能性があります。
最終的な結論
この記事を締めくくる前に、前回の記事で触れた内容をもう一度思い出していただきたいと思います。それは、そこに含まれていたビデオについてです。今回の記事では、リプレイ機能のティック上限システムの実装に焦点を当ててきましたが、あのビデオで示されていたバグについても、決して忘れたわけではありません。
そのバグは重大というわけではありません。というのも、それによってアプリケーションが不安定になったり、プラットフォームがクラッシュしたりすることはないからです。ただ、確かに存在しています。特に、チャートを閉じた際に、いくつかのグラフィカルオブジェクトが正しく削除されないという問題があります。これはごく限られた特定の状況下でのみ発生するものですが、私たちはすでにその修正に取り組んでいます。問題が解決し次第、それに特化したフォローアップ記事を公開する予定です。
さて、この記事の締めくくりとして、以下に埋め込んだ短いビデオをご覧いただくことをお勧めします。これは、システムが現在どのように動作しているか、特に「構成ファイルにティックの上限が定義されているかどうか」によって挙動がどう変化するのかを示しています。
短いですが、シミュレートされたデータと実際のティックデータとでは動きに明確な違いがあることを、効果的に伝えています。たとえば、ランダムウォークによるシミュレーションを使ってティックを補完すると、その動きは実際の取引で記録されたティックとはかなり異なる印象になります。
最後に、もうひとつだけ別の問題について触れておきたいと思います。これは深刻なものではありませんが、やや煩わしい現象です。ビデオでも確認できますが、まれにリプレイ/シミュレーションサービスが突然一時停止してしまい、ユーザーが再び「再生」ボタンを押す必要が生じることがあります。
この問題は実害こそないものの、早期に修正すべき課題です。次回の記事ではこのバグへの対処を行い、リプレイ/シミュレータが予期せず一時停止状態にならないようにします。また、現在一時的に無効化されているいくつかの機能の再有効化にも取りかかっていく予定です。
デモビデオ
MetaQuotes Ltdによりポルトガル語から翻訳されました。
元の記事: https://www.mql5.com/pt/articles/12240




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