
リプレイシステムの開発(第66回)サービスの再生(VII)
はじめに
この記事では、これまでとは少し異なるアプローチから始めます。とはいえ、まずは前回の「リプレイシステムの開発(第65回):サービスの再生(VI)」の内容を簡単に振り返っておきましょう。 前回は、マウスポインタに表示されるパーセンテージインジケーターに関する問題を取り上げました。再生やシミュレーション中、このインジケーターが誤った値を示していたため、それを修正しました。また、任意のタイミングへ一気に移動できる「早送り機能」を実装し、場合によっては数分から数時間かかるような場面をスキップできるようになりました。ただし、この記事の本題に入る前に、ぜひ以下のビデオをご覧ください。冒頭で説明する内容の理解を深めるのに役立つはずです。
見るべきもの...
このビデオが状況をよく表していると思います。それでも、何が起こっているのかを少し説明させてください。このリプレイ/シミュレーションアプリケーションは、カスタマイズされたアセットの使用に大きく依存しています。これは明らかであり、多くの方が同意されることでしょう。しかし、私たちがおこなっているプログラミングが原因ではない障害も存在します。これらの障害はさまざまな原因によって発生し、珍しいケースもあれば、ほぼランダムに発生することもあります。問題の原因が特定できる場合は対応が可能ですが、ランダムに起こる問題については状況が異なり、回避策を学ぶ必要があります。
この記事執筆時点でのMetaTrader 5プラットフォームの最新バージョンは、ビデオで示されたものと全く同じです。問題の所在は明確ではありませんが、確実に存在しており、すでに経験されているか、今後遭遇する可能性があります。ビデオで示された問題が確実に解決されるまでは、適応する必要があります。具体的には、リプレイまたはシミュレーションアプリケーションを起動する前に、セッション中に使用する設定をあらかじめ決めておくことが重要です。こうすることで、チャートの時間軸を変更する必要がなくなります。時間軸を変更すると、MetaTrader 5がカスタマイズされたアセットのティックやバー情報との接続を不自然に失うことがあるためです。おそらくこの記事をお読みの時点で、ビデオに示された問題はすでに修正されているかもしれません。もしそうであれば幸いです。そうでない場合は、不快な体験を避けるために、上記のアドバイスに従ってください。
それでは、この記事の本題に進みましょう。前回の記事では、未解決の問題が1つ残っていました。より緊急性の高い課題ですので、まずはそこから取り組みます。
次のバーまでの残り時間の実装
多くの金融市場のトレーダーが重視し、頻繁にチェックしている機能のひとつに「次のバーが始まるまでの残り時間」があります。最初は些細なことや無駄に感じるかもしれませんが、トレーダーは次のバーが始まる直前の数秒に注文を出すことが多いため、この情報は非常に重要になる運用モデルも存在します。
特定の時間枠やトレーダー自身の経験によって、残り時間を直感的に把握できるようになる場合もあります。しかし、初心者トレーダーにはまだそのスキルが備わっていません。そのため、残り時間を知らせる仕組みを提供する必要があります。ライブ市場であれば、常に取引サーバー(デモ口座や実口座)に接続されているため、この機能を実装するのは比較的簡単です。さらに、取引が常に進行しているため、時間が止まらず指標の表示もスムーズです。しかし、リプレイやシミュレーション環境においてはこの点が複雑になります。ユーザーがアプリケーションを無期限に一時停止できることや、バーの位置が開始直後やほぼ終了地点にまで動かせることが、事態を難しくしています。このため、単純にバーの残り時間を表示しようとすると、複雑で扱いにくい「フランケンシュタイン的」な解決策になりかねず、うまくバランスを取ることが求められます。
それでは、先に進む前に、すでに実装している部分を確認しましょう。前回の記事では、マウスインジケーターのコードを修正し、OnCalculateイベントハンドラの別バージョンを使用するように変更しました。この変更は今回の実装で非常に役立つことがわかっています。なぜなら、このイベントハンドラが時間値を格納する配列を提供してくれるためです。少し手を加えるだけで、以下のようなコードスニペットが得られます。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. { 49. Print(TimeToString(time[rates_total - 1], TIME_DATE | TIME_SECONDS)); // To Testing ... 50. GL_PriceClose = close[rates_total - 1]; 51. m_posBuff = rates_total; 52. (*Study).Update(m_Status); 53. 54. return rates_total; 55. } 56. //+------------------------------------------------------------------+
Mouse Study.mq5のソースコードの一部
新しい行が追加されたことにご注意ください。49行目では、時間配列に格納されている最新の値を表示できるようになっています。ここで、重要な点に注目してください。この配列の値は、1分足バーが正しく構築されるよう、つまり1分間のウィンドウ内で正確に形成されるよう調整されています。
この新しいコードがマウスインジケーターに組み込まれた状態で、リプレイ/シミュレーションアプリケーションを実行すると、以下のアニメーションに示されているものと非常に似た情報がツールボックスに表示されます。
OnCalculate関数が呼び出されるたびに、マウスインジケーターが情報を出力していることはわかりますが、秒単位の値は変化していません。そこで、よく考えてみると、この情報に秒単位の値を加えたらどうなるか、そうすれば、次のバーが表示されるまでに残っている時間を計算できるのではないのではないかといった疑問が湧くかもしれません。もしこのように考えたのであれば、この機能を実装するために何をすべきかを理解できているということです。それでは、このアイデアを試してみましょう。そのために、ヘッダーファイルC_Replay.mqhに少し修正を加える必要があります。以下に関連するコードスニペットを示します。
69. //+------------------------------------------------------------------+ 70. inline void CreateBarInReplay(bool bViewTick) 71. { 72. bool bNew; 73. double dSpread; 74. int iRand = rand(); 75. 76. if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[0], bNew)) 77. { 78. m_Infos.tick[0] = m_MemoryData.Info[m_Infos.CountReplay]; 79. if (m_MemoryData.ModePlot == PRICE_EXCHANGE) 80. { 81. dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 ); 82. if (m_Infos.tick[0].last > m_Infos.tick[0].ask) 83. { 84. m_Infos.tick[0].ask = m_Infos.tick[0].last; 85. m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread; 86. }else if (m_Infos.tick[0].last < m_Infos.tick[0].bid) 87. { 88. m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread; 89. m_Infos.tick[0].bid = m_Infos.tick[0].last; 90. } 91. } 92. if (bViewTick) CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 93. m_Infos.Rate[0].time = m_MemoryData.Info[m_Infos.CountReplay].time; //< To Testing... 94. CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate); 95. } 96. m_Infos.CountReplay++; 97. } 98. //+------------------------------------------------------------------+
C_Replay.mqhソースコードスニペット
実装がいかに面白くて簡単か、ご覧ください。秒単位の値を含められるようにするには、コードスニペットに93行目を追加するだけで済みました。これ以上に簡単なことはありません。しかし、これが本当に機能するのでしょうか。CustomRatesUpdate関数のドキュメントには、レートを作成する時間値が1分間のウィンドウ内に収まっている必要があると記載されています。ウィンドウ自体は変更していないので問題ないと思うかもしれません。秒単位の値を追加しているだけで、おそらく関数側では無視され、OnCalculate関数で認識できるようマウスインジケーターに渡されるだけでしょう。ある意味、この理論には私も同意しますが、実際に検証してみるまではあくまで仮説にすぎません。そこでコードをコンパイルし、結果が下の動画に示されています。
簡単なデモンストレーション
しかし、あれは何だったのでしょうか。いったい何が起こったのでしょう。正直に言うと、こんな結果になるとは思っていませんでした。うまくいくと考えていたのです。実際、このアイデア自体は完全に間違っているわけではありません。マウスインジケーターが私たちの欲しかった情報を返してくれたことに気づいたはずです。しかし、チャートの内容は…別のアプローチを探したほうがよさそうです。ただ、それでもまだ興味深いことができる可能性があることも理解してほしいです。実は私も以前、このアイデアを使ってインジケーターを作ったことがあります。そのインジケーターの少なくともオープン版は、「一からの取引エキスパートアドバイザーの開発(第13部):時間と取引(II)」で確認できます。あのバージョンは今はもう古くなっていますが、ここで起こったことを理解する手助けになるでしょう。ちなみに、よく聞かれますが、いいえ、現在のそのインジケーターのバージョンを販売したり譲渡したり貸したり、見せたりすることはありません。あくまで個人利用のものです。
さて、これで仕組みがどうなっているか、そして一見シンプルに見えるものが実際にはもっと複雑な処理を必要とする理由が分かっていただけたと思います。
新しい方法を考えなければと思うかもしれませんが、実はターミナルのグローバル変数を使っていた頃にはすでに解決策はありました。ただ、今回はその方法は使いません。私たちは創意工夫を凝らし、プログラム同士がどう情報をやり取りするかをしっかり理解しなければなりません。今も、コントロールインジケーターがサービスを管理し、サービスが私たちのポジションをインジケーターに返す、という形で情報のやり取りを行っています。同様の方法をここでも応用しますが、一点だけ重要な違いがあります。マウスインジケーターに毎秒、あるいはティックごとにカスタムイベントを送って現在時刻を知らせることはできない、ということです。そうすると、アプリのパフォーマンスが著しく低下し、1分あたりの最大ティック数をさらに減らさざるを得なくなります。これは何としても避けたい事態です。とはいえ、ティックの情報と現在のバーの時間に関する情報を同期させる必要があるのは事実です。これが最大の課題です。というわけで、先ほどのコード変更は元に戻して、実際に問題を解決できる別の方法の検討に入りましょう。その内容は次のセクションで説明します。
残り時間を表示する別の方法を考える
ライブ取引サーバーに接続している場合とは異なり、リプレイ/シミュレーター環境でこれを扱うのは、特に私が目指す方法を考慮すると、ずっと複雑になります。端末のグローバル変数を使って情報を渡すつもりはありません。実際のところ、ライブサーバーに接続しているときは、すべてのクロックが少なくとも秒単位まで同期している必要があります。この同期がどれほど重要かは完全には理解しづらいかもしれませんが、残り秒数を基に処理をするためには、ある時刻まであと何秒かを知るだけで十分です。ライブサーバーならシステムの内部時計を使って簡単に計算でき、残り時間を得られます。
しかし、リプレイやシミュレーション環境では話が一変します。この複雑さの原因は、まさに「再生」や「シミュレーション」という性質にあります。どうして単に再生・シミュレーションしているだけで、次のバーまでの残り時間を知るのが難しくなるのか、これは一見、理解しづらいことです。表面的に見れば確かにそうですが、問題の核心は「時計を正しく読み取れるかどうか」に尽きます。
例えば、チャートに表示される最初の1分足が完全に描画されるのに丸1分かかるとします。さて、これが問題を理解する上での最初の条件です。バーが1分かけて描かれるわけです。ここで、アプリケーションをちょうど0秒に起動してプロットを開始すれば、システムの1分単位の刻みで新しいバーが生成されます。完璧です。問題は解決しました。同期が完璧だからです。したがって、システムクロックをチェックするだけで、次のバーが始まるまでの残り時間が正確にわかります。
ところが、最初のバーがちょうど1分で終わることはほとんどありませんし、そもそもシステム時計の0秒に合わせて起動することも非常に難しいです。とはいえ、この場合、リプレイやシミュレーターをシステムクロックに同期させるように強制的に起動させる、小さな調整やテストを実装する可能性があります。この解決策は実行可能で妥当です。ただし、アプリケーションが準備完了を通知し、バーのプロットを開始できる状態になった後、実際に同期が取れるまで一定時間待つ必要があります。その待ち時間は最悪の場合で最大59秒です。個人用アプリケーションでは許容範囲ですが、何度も再生ボタンを押すたびに最大59秒待つのは、遅かれ早かれストレスになるでしょう。
システムクロックと強制的に同期することでバーの残り時間の追跡は簡単になりますが、ユーザー体験は悪化します。特にシミュレーションやリプレイを途中で一時停止・再開した場合、再び最大59秒待つ必要があり、チャートのプロットが再開されるまで時間がかかります。したがって、実用的には最善の解決策とは言えません。
しかし、妥協点を見つけることはできるかもしれません。システムクロックがバーのプロットを「ロック解除」するまで最大59秒待つ必要がなく、システムクロックとの同期を保つためのわずかな調整を実行できる方法です。そうすれば、バーに残っている時間を把握できます。ここから少し面白くなってきます。
この詳細な説明は、物事を過度に複雑にしたり、読者の皆さんを混乱させたりするためではなく、コードを書く前にまず考え、可能性を分析し、実装の難易度を検討することの重要性を示すためのものです。多くの人はプログラミングとは単にコードを書くだけだと考えがちですが、実際にはそれはプロセスの一部にすぎません。最も重要なのは、ソリューションの計画と分析です。それがこれまでやってきたことです。この思考プロセスによって、何をすべきかの確かなイメージを形成できました。ただし、組織面でいくつか改善が必要です。これから何をするのか本当に理解するために、新しいセクションに進みましょう。
残り時間を表示するための基本バージョンの実装
このセクションでおこなうことは、私の見解では比較的シンプルでありながら、かなり大胆な内容です。現在のバーの時間情報をマウスインジケーターに素早く伝達する仕組みを実装します。まずはマウスインジケーターのコードにいくつか変更を加えることから始めます。これらの変更には、以下のコードスニペットに示すように、多少のコード追加が含まれます。
12. //+------------------------------------------------------------------+ 13. double GL_PriceClose; 14. datetime GL_TimeAdjust; 15. //+------------------------------------------------------------------+ 16. #include <Market Replay\Auxiliar\Study\C_Study.mqh> 17. //+------------------------------------------------------------------+ 18. C_Study *Study = NULL; 19. //+------------------------------------------------------------------+ 20. input color user02 = clrBlack; //Price Line 21. input color user03 = clrPaleGreen; //Positive Study 22. input color user04 = clrLightCoral; //Negative Study 23. //+------------------------------------------------------------------+ 24. C_Study::eStatusMarket m_Status; 25. int m_posBuff = 0; 26. double m_Buff[]; 27. //+------------------------------------------------------------------+ 28. int OnInit() 29. { 30. ResetLastError(); 31. Study = new C_Study(0, "Indicator Mouse Study", user02, user03, user04); 32. if (_LastError != ERR_SUCCESS) return INIT_FAILED; 33. if ((*Study).GetInfoTerminal().szSymbol != def_SymbolReplay) 34. { 35. MarketBookAdd((*Study).GetInfoTerminal().szSymbol); 36. OnBookEvent((*Study).GetInfoTerminal().szSymbol); 37. m_Status = C_Study::eCloseMarket; 38. }else 39. m_Status = C_Study::eInReplay; 40. SetIndexBuffer(0, m_Buff, INDICATOR_DATA); 41. ArrayInitialize(m_Buff, EMPTY_VALUE); 42. 43. return INIT_SUCCEEDED; 44. } 45. //+------------------------------------------------------------------+ 46. int OnCalculate(const int rates_total, const int prev_calculated, const datetime& time[], const double& open[], 47. const double& high[], const double& low[], const double& close[], const long& tick_volume[], 48. const long& volume[], const int& spread[]) 49. { 50. GL_PriceClose = close[rates_total - 1]; 51. GL_TimeAdjust = (spread[rates_total - 1] < 60 ? spread[rates_total - 1] : 0); 52. m_posBuff = rates_total; 53. (*Study).Update(m_Status); 54. 55. return rates_total; 56. } 57. //+------------------------------------------------------------------+
Mouse Study.mq5のソースコードの一部
上記のコードスニペットをマウスインジケーターの最新ソースコードと比較すると、14行目が新たに追加されていることがわかります。つまり、インジケーターモジュール内に新しいグローバル変数が増えたということです。このマウスインジケーターモジュールに新しいグローバル変数を追加するつもりはありませんが、これは特別な変数で、その理由はすぐにご理解いただけるでしょう。いずれにせよ、この変数には51行目で値が代入されます。この値はスプレッドを介して渡されます。ここには潜在的なリスクがありますが、同時に非常に役立つ可能性も示唆しています。とはいえ、このグローバル変数は非常に限定的な方法でのみ使用されます。より理解を深めるために、以下に示す更新されたC_Study.mqhファイルを見てみましょう。
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. //+------------------------------------------------------------------+ 004. #include "..\C_Mouse.mqh" 005. //+------------------------------------------------------------------+ 006. #define def_ExpansionPrefix def_MousePrefixName + "Expansion_" 007. //+------------------------------------------------------------------+ 008. class C_Study : public C_Mouse 009. { 010. private : 011. //+------------------------------------------------------------------+ 012. struct st00 013. { 014. eStatusMarket Status; 015. MqlRates Rate; 016. string szInfo, 017. szBtn1, 018. szBtn2, 019. szBtn3; 020. color corP, 021. corN; 022. int HeightText; 023. bool bvT, bvD, bvP; 024. datetime TimeDevice; 025. }m_Info; 026. //+------------------------------------------------------------------+ 027. void Draw(void) 028. { 029. double v1; 030. 031. if (m_Info.bvT) 032. { 033. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn1, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - 18); 034. ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn1, OBJPROP_TEXT, m_Info.szInfo); 035. } 036. if (m_Info.bvD) 037. { 038. v1 = NormalizeDouble((((GetInfoMouse().Position.Price - m_Info.Rate.close) / m_Info.Rate.close) * 100.0), 2); 039. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - 1); 040. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_BGCOLOR, (v1 < 0 ? m_Info.corN : m_Info.corP)); 041. ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_TEXT, StringFormat("%.2f%%", MathAbs(v1))); 042. } 043. if (m_Info.bvP) 044. { 045. v1 = NormalizeDouble((((GL_PriceClose - m_Info.Rate.close) / m_Info.Rate.close) * 100.0), 2); 046. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - 1); 047. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_BGCOLOR, (v1 < 0 ? m_Info.corN : m_Info.corP)); 048. ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_TEXT, StringFormat("%.2f%%", MathAbs(v1))); 049. } 050. } 051. //+------------------------------------------------------------------+ 052. inline void CreateObjInfo(EnumEvents arg) 053. { 054. switch (arg) 055. { 056. case evShowBarTime: 057. C_Mouse::CreateObjToStudy(2, 110, m_Info.szBtn1 = (def_ExpansionPrefix + (string)ObjectsTotal(0)), clrPaleTurquoise); 058. m_Info.bvT = true; 059. break; 060. case evShowDailyVar: 061. C_Mouse::CreateObjToStudy(2, 53, m_Info.szBtn2 = (def_ExpansionPrefix + (string)ObjectsTotal(0))); 062. m_Info.bvD = true; 063. break; 064. case evShowPriceVar: 065. C_Mouse::CreateObjToStudy(58, 53, m_Info.szBtn3 = (def_ExpansionPrefix + (string)ObjectsTotal(0))); 066. m_Info.bvP = true; 067. break; 068. } 069. } 070. //+------------------------------------------------------------------+ 071. inline void RemoveObjInfo(EnumEvents arg) 072. { 073. string sz; 074. 075. switch (arg) 076. { 077. case evHideBarTime: 078. sz = m_Info.szBtn1; 079. m_Info.bvT = false; 080. break; 081. case evHideDailyVar: 082. sz = m_Info.szBtn2; 083. m_Info.bvD = false; 084. break; 085. case evHidePriceVar: 086. sz = m_Info.szBtn3; 087. m_Info.bvP = false; 088. break; 089. } 090. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, false); 091. ObjectDelete(GetInfoTerminal().ID, sz); 092. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, true); 093. } 094. //+------------------------------------------------------------------+ 095. public : 096. //+------------------------------------------------------------------+ 097. C_Study(long IdParam, string szShortName, color corH, color corP, color corN) 098. :C_Mouse(IdParam, szShortName, corH, corP, corN) 099. { 100. if (_LastError != ERR_SUCCESS) return; 101. ZeroMemory(m_Info); 102. m_Info.Status = eCloseMarket; 103. m_Info.Rate.close = iClose(GetInfoTerminal().szSymbol, PERIOD_D1, ((GetInfoTerminal().szSymbol == def_SymbolReplay) || (macroGetDate(TimeCurrent()) != macroGetDate(iTime(GetInfoTerminal().szSymbol, PERIOD_D1, 0))) ? 0 : 1)); 104. m_Info.corP = corP; 105. m_Info.corN = corN; 106. CreateObjInfo(evShowBarTime); 107. CreateObjInfo(evShowDailyVar); 108. CreateObjInfo(evShowPriceVar); 109. } 110. //+------------------------------------------------------------------+ 111. void Update(const eStatusMarket arg) 112. { 113. int i0; 114. datetime dt; 115. 116. switch (m_Info.Status = (m_Info.Status != arg ? arg : m_Info.Status)) 117. { 118. case eCloseMarket : 119. m_Info.szInfo = "Closed Market"; 120. break; 121. case eInReplay : 122. case eInTrading : 123. i0 = PeriodSeconds(); 124. dt = (m_Info.Status == eInReplay ? m_Info.TimeDevice + GL_TimeAdjust : TimeCurrent()); 125. m_Info.Rate.time = (m_Info.Rate.time <= dt ? (datetime)(((ulong) dt / i0) * i0) + i0 : m_Info.Rate.time); 126. m_Info.szInfo = TimeToString((datetime)m_Info.Rate.time - dt, TIME_SECONDS); 127. break; 128. case eAuction : 129. m_Info.szInfo = "Auction"; 130. break; 131. default : 132. m_Info.szInfo = "ERROR"; 133. } 134. Draw(); 135. } 136. //+------------------------------------------------------------------+ 137. virtual void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) 138. { 139. C_Mouse::DispatchMessage(id, lparam, dparam, sparam); 140. switch (id) 141. { 142. case CHARTEVENT_CUSTOM + evHideBarTime: 143. RemoveObjInfo(evHideBarTime); 144. break; 145. case CHARTEVENT_CUSTOM + evShowBarTime: 146. CreateObjInfo(evShowBarTime); 147. break; 148. case CHARTEVENT_CUSTOM + evHideDailyVar: 149. RemoveObjInfo(evHideDailyVar); 150. break; 151. case CHARTEVENT_CUSTOM + evShowDailyVar: 152. CreateObjInfo(evShowDailyVar); 153. break; 154. case CHARTEVENT_CUSTOM + evHidePriceVar: 155. RemoveObjInfo(evHidePriceVar); 156. break; 157. case CHARTEVENT_CUSTOM + evShowPriceVar: 158. CreateObjInfo(evShowPriceVar); 159. break; 160. case (CHARTEVENT_CUSTOM + evSetServerTime): 161. m_Info.TimeDevice = (datetime)lparam; 162. break; 163. case CHARTEVENT_MOUSE_MOVE: 164. Draw(); 165. break; 166. } 167. ChartRedraw(GetInfoTerminal().ID); 168. } 169. //+------------------------------------------------------------------+ 170. }; 171. //+------------------------------------------------------------------+ 172. #undef def_ExpansionPrefix 173. #undef def_MousePrefixName 174. //+------------------------------------------------------------------+
C_Study.mqhソースコード
さて、次に、これから説明する内容は、全体の動作を理解する上で非常に重要なので、よく注意してください。ヘッダーファイルのコードがこれまでと違って見えるかもしれませんが、ここで本当に重要な部分は2つだけです。まず160行目で、カスタムイベントを処理しています。161行目では、このイベントによって渡された値をクラスのprivate変数に格納していることに注意してください。これが1つ目です。さて、本当の「魔法」が起こる場所を見てみましょう。Updateプロシージャが定義されている111行目まで少しスクロールします。このプロシージャは、後で表示する情報を生成する役割を担います。注目してください。123行目では、カスタム銘柄チャートの現在の時間枠に含まれる秒数を取得しています。次に124行目で、小さな計算を実行するか、あるいはMetaTrader 5から提供された値を使います。計算をおこなうか提供された値を使うかは、インジケーターの状態によります。マウスインジケーターがリプレイ銘柄上で使われている場合は計算をおこない、それ以外の場合はMetaTrader 5の値を使います。
この計算には、マウスインジケーターのコードスニペットから取得した値と、リプレイ/シミュレーターアプリケーションから提供されるもう一つの値が考慮されています。この2つ目の値がマウスインジケーターにどのように渡されるかは後ほど説明します。
いずれにせよ、ここで扱っているのは時間の経過とともに変化するデータポイントです。しかし、さらに別の値も必要で、それは125行目で計算されています。この行は、チャート上で新しいバーが生成される正確なタイミングを決定します。続く126行目では、現在のバーが閉じるまでの残り時間を最終的に計算し、ユーザーに表示します。
このシステム全体が適切に機能するのは、新しいバーが形成されるまでの残り時間を教えてくれる存在が取引サーバーだからです。あるいは、リプレイやシミュレーションの場合は、バーを更新してMetaTrader 5に描画させるサービスがそれに当たります。
ここで、上記の説明からサービスコードを修正する必要があることに気づいたかもしれません。しかし、実際に更新すべきなのはC_Replay.mqhファイル内のコードです。ただし、その前にサービスコードの2か所に小さな変更を加える必要があります。まずはMacros.mqhヘッダーファイルにいくつか追加します。手順は次の通りです。
1. //+------------------------------------------------------------------+ 2. #property copyright "Daniel Jose" 3. //+------------------------------------------------------------------+ 4. #define macroRemoveSec(A) (A - (A % 60)) 5. #define macroGetDate(A) (A - (A % 86400)) 6. #define macroGetSec(A) (A - (A - (A % 60))) 7. //+------------------------------------------------------------------+
Macros.mqhソースコード
すべてが非常に単純なので、詳細には触れません。これが完了したら、C_FilesTick.mqhヘッダーファイルを変更します。変更は特殊な性質のものであり、次の部分に示されています。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #include "C_FileBars.mqh" 05. #include "C_Simulation.mqh" 06. #include "..\..\Auxiliar\Macros.mqh" 07. //+------------------------------------------------------------------+ 08. //#define macroRemoveSec(A) (A - (A % 60)) 09. #define def_MaxSizeArray 16777216 // 16 Mbytes 10. //+------------------------------------------------------------------+ 11. class C_FileTicks 12. { 13. protected: 14. enum ePlotType {PRICE_EXCHANGE, PRICE_FOREX};
C_FilesTick.mqhソースコードスニペット
この場合、6行目が追加されていることに注意してください。また、強調した8行目はコードから削除する必要があります。これは、以前8行目にあったコードが、現在ではMacros.mqhヘッダーファイルに含まれているためです。これで、これらの変更を加えたら、次は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) 092. { 093. CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 094. if (bNew) EventChartCustom(m_Infos.IdReplay, evSetServerTime, (long)m_Infos.Rate[0].time, 0, ""); 095. } 096. m_Infos.Rate[0].spread = (int)macroGetSec(m_MemoryData.Info[m_Infos.CountReplay].time); 097. CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate); 098. } 099. m_Infos.CountReplay++; 100. } 101. //+------------------------------------------------------------------+ 102. void AdjustViewDetails(void) 103. { 104. MqlRates rate[1]; 105. 106. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_ASK_LINE, GetInfoTicks().ModePlot == PRICE_FOREX); 107. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_BID_LINE, GetInfoTicks().ModePlot == PRICE_FOREX); 108. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_LAST_LINE, GetInfoTicks().ModePlot == PRICE_EXCHANGE); 109. m_Infos.PointsPerTick = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE); 110. CopyRates(def_SymbolReplay, PERIOD_M1, 0, 1, rate); 111. if ((m_Infos.CountReplay == 0) && (GetInfoTicks().ModePlot == PRICE_EXCHANGE)) 112. for (; GetInfoTicks().Info[m_Infos.CountReplay].volume_real == 0; m_Infos.CountReplay++); 113. if (rate[0].close > 0) 114. { 115. if (GetInfoTicks().ModePlot == PRICE_EXCHANGE) 116. m_Infos.tick[0].last = rate[0].close; 117. else 118. { 119. m_Infos.tick[0].bid = rate[0].close; 120. m_Infos.tick[0].ask = rate[0].close + (rate[0].spread * m_Infos.PointsPerTick); 121. } 122. m_Infos.tick[0].time = rate[0].time; 123. m_Infos.tick[0].time_msc = rate[0].time * 1000; 124. }else 125. m_Infos.tick[0] = GetInfoTicks().Info[m_Infos.CountReplay]; 126. CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 127. } 128. //+------------------------------------------------------------------+ 129. void AdjustPositionToReplay(void) 130. { 131. int nPos, nCount; 132. 133. if (m_IndControl.Position == (int)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks)) return; 134. nPos = (int)((m_MemoryData.nTicks * m_IndControl.Position) / def_MaxSlider); 135. for (nCount = 0; m_MemoryData.Rate[nCount].spread < nPos; m_Infos.CountReplay = m_MemoryData.Rate[nCount++].spread); 136. if (nCount > 0) CustomRatesUpdate(def_SymbolReplay, m_MemoryData.Rate, nCount - 1); 137. while ((nPos > m_Infos.CountReplay) && def_CheckLoopService) 138. CreateBarInReplay(false); 139. } 140. //+------------------------------------------------------------------+ 141. public : 142. //+------------------------------------------------------------------+ 143. C_Replay() 144. :C_ConfigService() 145. { 146. Print("************** Market Replay Service **************"); 147. srand(GetTickCount()); 148. SymbolSelect(def_SymbolReplay, false); 149. CustomSymbolDelete(def_SymbolReplay); 150. CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay)); 151. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0); 152. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, 0); 153. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, 0); 154. CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation"); 155. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, 8); 156. SymbolSelect(def_SymbolReplay, true); 157. m_Infos.CountReplay = 0; 158. m_IndControl.Handle = INVALID_HANDLE; 159. m_IndControl.Mode = C_Controls::ePause; 160. m_IndControl.Position = 0; 161. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = C_Controls::eTriState; 162. } 163. //+------------------------------------------------------------------+ 164. ~C_Replay() 165. { 166. SweepAndCloseChart(); 167. IndicatorRelease(m_IndControl.Handle); 168. SymbolSelect(def_SymbolReplay, false); 169. CustomSymbolDelete(def_SymbolReplay); 170. Print("Finished replay service..."); 171. } 172. //+------------------------------------------------------------------+ 173. bool OpenChartReplay(const ENUM_TIMEFRAMES arg1, const string szNameTemplate) 174. { 175. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == 0) 176. return MsgError("Asset configuration is not complete, it remains to declare the size of the ticket."); 177. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == 0) 178. return MsgError("Asset configuration is not complete, need to declare the ticket value."); 179. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == 0) 180. return MsgError("Asset configuration not complete, need to declare the minimum volume."); 181. SweepAndCloseChart(); 182. m_Infos.IdReplay = ChartOpen(def_SymbolReplay, arg1); 183. if (!ChartApplyTemplate(m_Infos.IdReplay, szNameTemplate + ".tpl")) 184. Print("Failed apply template: ", szNameTemplate, ".tpl Using template default.tpl"); 185. else 186. Print("Apply template: ", szNameTemplate, ".tpl"); 187. 188. return true; 189. } 190. //+------------------------------------------------------------------+ 191. bool InitBaseControl(const ushort wait = 1000) 192. { 193. Print("Waiting for Mouse Indicator..."); 194. Sleep(wait); 195. while ((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, 0, "Indicator Mouse Study") == INVALID_HANDLE)) Sleep(200); 196. if (def_CheckLoopService) 197. { 198. AdjustViewDetails(); 199. Print("Waiting for Control Indicator..."); 200. if ((m_IndControl.Handle = iCustom(ChartSymbol(m_Infos.IdReplay), ChartPeriod(m_Infos.IdReplay), "::" + def_IndicatorControl, m_Infos.IdReplay)) == INVALID_HANDLE) return false; 201. ChartIndicatorAdd(m_Infos.IdReplay, 0, m_IndControl.Handle); 202. UpdateIndicatorControl(); 203. } 204. 205. return def_CheckLoopService; 206. } 207. //+------------------------------------------------------------------+ 208. bool LoopEventOnTime(void) 209. { 210. int iPos; 211. 212. while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay)) 213. { 214. UpdateIndicatorControl(); 215. Sleep(200); 216. } 217. m_MemoryData = GetInfoTicks(); 218. AdjustPositionToReplay(); 219. EventChartCustom(m_Infos.IdReplay, evSetServerTime, (long)macroRemoveSec(m_MemoryData.Info[m_Infos.CountReplay].time), 0, ""); 220. iPos = 0; 221. while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService)) 222. { 223. if (m_IndControl.Mode == C_Controls::ePause) return true; 224. 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); 225. CreateBarInReplay(true); 226. while ((iPos > 200) && (def_CheckLoopService)) 227. { 228. Sleep(195); 229. iPos -= 200; 230. m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks); 231. UpdateIndicatorControl(); 232. } 233. } 234. 235. return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService)); 236. } 237. }; 238. //+------------------------------------------------------------------+ 239. #undef def_SymbolReplay 240. #undef def_CheckLoopService 241. #undef def_MaxSlider 242. //+------------------------------------------------------------------+
C_Replay.mqhソースコード
ここでの問題は、一見単純に思えるかもしれませんが、実はもう少し複雑です。というのも、シミュレーションやリプレイにおいて、現在の進行位置をマウスインジケーターに直接伝える必要があるためです。一見すると、それ自体は大した問題ではないように思えるかもしれません。この記事の前半でも、その方法を紹介しました。しかし、そこで何かおかしな挙動が起きていることに気づき、その処理には別の方法が必要だということが明らかになったのです。
その新しい方法とは、3行のコードを追加することで実現されました。ただし、これが「完全な解決策」だと勘違いしないでください。そうではありません。これはあくまで、問題の一側面を解決したにすぎません。この方法ではカバーできない、もう一つの側面があるのです(少なくとも現時点では)。ただし、その話に入る前に、まずはこのコードが何をしているのか見ていきましょう。
最初に見ておきたいのは、最もシンプルな96行目です。マウスインジケーターのソースコードの一部を見た方なら、スプレッド値を使って秒単位で細かい調整をおこなっているのを覚えているはずです。この値をRatesを通して直接渡せないのであれば、別の方法でできるだけ速く送る必要があります。そこで、スプレッド値がシミュレーション中には特に使われていないことを活かして、それを転用することにしました。つまり、スプレッドを使って意味のある情報を渡すというわけです。もちろん、ここには小さな問題がありますが、それについてはまだ深く考える必要はありません。今のところはこの方法で問題ありません。
次に他の行の説明に移ります。少し上の94行目を見ると、リプレイ/シミュレーションサービスが新しいバーを検出するたびにカスタムイベントを発生させ、インジケーターに新しい値を通知していることが分かります。同様に、219行目でも再度インジケーターに値を通知しています。これらはいずれもカスタムイベントを使って処理されています。
では、なぜこのような方法を取っているのでしょうか。他に同じ結果を得る手段はないのでしょうか。もちろんあります。ですが、少なくとも今の目的に対しては、十分な方法とは言えませんでした。事実として、これらのカスタムイベントは、新しい1分足が確定したことが明らかになったときにだけトリガーされるようになっています。これをわざわざカスタムイベントとしてチャートに送らずに、もっと違うやり方で処理することも可能です。たとえば、iTimeライブラリ関数を使って、1分足がいつ新たに形成されたかを検出する方法があります。ここで大事な点に注意してください。私たちに重要なのは、現在のチャートの時間足におけるバーの時間ではなく、「1分足」のタイムスタンプです。少し混乱するかもしれませんが、以下のコードスニペットをご覧ください。
110. //+------------------------------------------------------------------+ 111. void Update(const eStatusMarket arg) 112. { 113. int i0; 114. datetime dt; 115. 116. switch (m_Info.Status = (m_Info.Status != arg ? arg : m_Info.Status)) 117. { 118. case eCloseMarket : 119. m_Info.szInfo = "Closed Market"; 120. break; 121. case eInReplay : 122. case eInTrading : 123. i0 = PeriodSeconds(); 124. dt = (m_Info.Status == eInReplay ? iTime(NULL, 0, 0) + GL_TimeAdjust : TimeCurrent()); 125. m_Info.Rate.time = (m_Info.Rate.time <= dt ? (datetime)(((ulong) dt / i0) * i0) + i0 : m_Info.Rate.time); 126. m_Info.szInfo = TimeToString((datetime)m_Info.Rate.time - dt, TIME_SECONDS); 127. break; 128. case eAuction : 129. m_Info.szInfo = "Auction"; 130. break; 131. default : 132. m_Info.szInfo = "ERROR"; 133. } 134. Draw(); 135. } 136. //+------------------------------------------------------------------+
ファイルC_Study.mqhからのコードスニペット
変更は124行目にあります。この関数を使用してバーの時間を決定するのは時間の無駄です。結果の値はOnCalculate関数の値と同じになるためです。ただし、同じフラグメントを以下のように変更すると、すべてが完全に異なります。
110. //+------------------------------------------------------------------+ 111. void Update(const eStatusMarket arg) 112. { 113. int i0; 114. datetime dt; 115. 116. switch (m_Info.Status = (m_Info.Status != arg ? arg : m_Info.Status)) 117. { 118. case eCloseMarket : 119. m_Info.szInfo = "Closed Market"; 120. break; 121. case eInReplay : 122. case eInTrading : 123. i0 = PeriodSeconds(); 124. dt = (m_Info.Status == eInReplay ? iTime(NULL, PERIOD_M1, 0) + GL_TimeAdjust : TimeCurrent()); 125. m_Info.Rate.time = (m_Info.Rate.time <= dt ? (datetime)(((ulong) dt / i0) * i0) + i0 : m_Info.Rate.time); 126. m_Info.szInfo = TimeToString((datetime)m_Info.Rate.time - dt, TIME_SECONDS); 127. break; 128. case eAuction : 129. m_Info.szInfo = "Auction"; 130. break; 131. default : 132. m_Info.szInfo = "ERROR"; 133. } 134. Draw(); 135. } 136. //+------------------------------------------------------------------+
ファイルC_Study.mqhからのコードスニペット
ここでの変更点はただひとつ、MetaTrader 5が「1分足バーがいつ始まったのか」を教えるようにしたことです。そして、このたった一つの変更によって、サービス側でカスタムイベントをトリガーして同じ情報を渡す必要がなくなったのです。これではよくわからなかった方もいるかもしれません。ここでのポイントは、チャートの時間足が何であれ、MetaTrader 5が返してくるのは常に「1分足バーが始まったタイムスタンプ」だということです。そして実は、それこそがカスタムイベントを通じて伝えようとしていた内容とまったく同じ情報なのです。ですが、この処理をサービス側で制御することで、「適切なタイミングでのみ」この情報が渡されるようになり、インジケーター側から何度も繰り返し取得することでシステムに負荷がかかるのを防ぐことができます。
最終的な考察
ここで紹介した内容は、まだ問題に対する最終的・決定的な解決策とは言えません。しかし、現段階では非常に有効であることが実証されています。したがって、今のところこの方法を採用しない理由は特にありません。とはいえ、近いうちに、より洗練され、堅牢な仕組みが必要になるという点は、しっかり認識しておくべきです。それでも、それまでは新しいバーが生成されるタイミングを確実に検出できる、信頼性のある方法が手に入りました。
以下のビデオでは、現在のシステムがどのように動作しているかをご覧いただけます。
デモ版を実行する
MetaQuotes Ltdによりポルトガル語から翻訳されました。
元の記事: https://www.mql5.com/pt/articles/12286
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。





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