English Русский Español Deutsch Português
preview
リプレイシステムの開発(第26回):エキスパートアドバイザープロジェクト-C_Terminalクラス

リプレイシステムの開発(第26回):エキスパートアドバイザープロジェクト-C_Terminalクラス

MetaTrader 5テスター | 30 3月 2024, 15:08
74 0
Daniel Jose
Daniel Jose

はじめに

前回の「リプレイシステムの開発 - 市場シミュレーション(第25回):次の段階への準備」稿では、基本的な使用のためにリプレイ/シミュレーションシステムを準備しました。しかし、必要なのは過去の動きや潜在的な行動を分析するだけではありません。つまり、あたかも実際の市場に取り組んでいるかのように、ターゲットを絞った調査をおこなうことができるツールが必要です。そのためには、エキスパートアドバイザー(EA)を作成して、より詳細な調査をおこなう必要があります。さらに、様々な市場(株式や為替)に適用可能で、当社のリプレイ/シミュレーションシステムにも対応する汎用なEAを開発する予定です。

プロジェクトの規模を考えれば、この仕事は非常に深刻なものになるでしょう。しかし、以前の「一からの取引エキスパートアドバイザーの開発(第31部):未来に向けて(IV)」稿および「自動で動くEAを作る(第15回):オートメーション(VII)」稿ですでにそのプロセスのほとんどをカバーしているので、開発は見かけほど複雑ではありません。これらの記事では、完全に自動化されたEAの作成方法について詳しく説明しました。これらの資料にもかかわらず、ここでは固有でさらに難しい課題があります。MetaTrader 5プラットフォームに取引サーバーへの接続をシミュレーションさせ、現実的な公開市場シミュレーションを促進することです。この仕事はかなり複雑であることは間違いません。

にもかかわらず、最初の複雑さに怯んではなりません。どこかから始めなければなりません。そうしなければ、その課題を克服しようともせずに、その難しさを反芻してしまうことになります。それこそがプログラミングの醍醐味であり、学習、テスト、徹底的な研究を通じて障害を克服することです。始める前に言っておきたいのは、私は物事が実際にどのようにして存在するようになるのかを概説し、説明するのがとても好きだということです。この連載で多くの人が、通常の基本を超え、テストすることで、可能だと思っている以上のことを達成できることを学んだと思います。


エキスパートアドバイザー(EA)の実装概念

すでにお気づきかもしれませんが、私はオブジェクト指向プログラミング(OOP)の大ファンです。これは、OOPが提供する豊富な機能によるものです。また、ロバストで安全かつ信頼性の高いコードを最初から作成する方法も提供します。手始めに、プロジェクトの構成を整理することで、必要なものの予備知識を得る必要があります。ユーザーとしてもプログラマーとしても経験を積んできたため、EAが真に効果的であるためには、キーボードとマウスといういつでも利用できるリソースを使わなければならないことに気づきました。MetaTrader 5プラットフォームはチャートに基づいているため、マウスを使用してグラフィック要素を操作することは不可欠ですが、キーボードもまた、さまざまな面で重要な役割を果たしています。しかし、この議論はマウスとキーボードの使い方にとどまらず、オートメーションの連載でも取り上げられます。場合によっては、これらのツールを使わなくても完全な自動化が達成できることもありますが、ツールを使用する場合は、実行される操作の性質を考慮することが重要です。したがって、すべてのEAがすべての種類の資産に適しているわけではありません。

一部の資産の値動きが0.01であるためです。0.5という人もいれば、5という人もいます。FOREXの場合、これらの値は前述の例とは大きく異なります。このように多様な価値観があるため、特定の資産に特化したEAを開発するプログラマーもいます。取引サーバーは任意の値を受け付けないため、サーバーが設定したルールに従わなければならないからです。同じ原理がリプレイ/シミュレーションシステムにも当てはまります。EAに無作為な値で注文を実行させることはできません。

この制限の導入は必要であるだけでなく、非常に必要です。実際の口座で取引するときに、システムの挙動がまったく異なるのであれば、訓練のために機能的なリプレイ/シミュレーションをおこなう意味はありません。したがって、システムが一定の標準化を維持し、実際の口座の実態にできるだけ近い形で適応することが重要です。どのような状況であっても、あたかも取引サーバーと直接やりとりしているかのように動作するEAを開発する必要があります。


最初のC_Terminalクラスから始める

具体的な指針なしにすべてのコードを書くことはしばしば可能ですが、非常に大規模で複雑になる可能性のあるプロジェクトでは、この方法は推奨されません。プロジェクトがどのように発展していくのか、まだ正確なところは1つもわかりませんが、常にベストプログラミングプラクティスに焦点を当てることから始めることが重要です。そうでなければ、適切なプロジェクト計画を立てなければ、厄介なコードが大量にできてしまいます。したがって、たとえプロジェクトがそれほど壮大で複雑なものでなくても、最初から大きく考えることが重要です。ベストプラクティスを実践することで、小さなプロジェクトでもより組織的になり、安定した方法論に従うことを学ぶことができます。まずは最初のクラスを開発することから始めましょう。そのために、C_Terminal.mqhという新しいヘッダーファイルを作成します。ファイルをクラスと同じ名前にするのは良い習慣です。作業する必要があるときに見つけやすくなります。コードは次のように始まります。

class C_Terminal
{

       protected:
   private  :
   public   :
};

自動で動くEAを作る(第05回):手動トリガー(II)」稿では、クラスと予約語についての基本的な概念について考えました。自動化されたEAを作成する連載について知らない方は一見の価値があります。ここで使用する要素の多くが含まれているからです。しかし、そのコードは最新ではなく、完全に陳腐化しています。ここでは、ある問題に対処し、システムをより堅牢で、信頼性が高く、効率的なものにする必要があるため、新しいインタラクションの形を提供するコードを見ることになります。コーディングを始めてから、実際にクラスコードに現れる最初のものは構造体です。これについては以下で詳しく説明します。

class C_Terminal
{
   protected:
//+------------------------------------------------------------------+
      struct st_Terminal
      {
         long    ID;
         string  szSymbol;
         int     Width,
                 Height,
                 nDigits;
         double  PointPerTick,
                 ValuePerPoint,
                 VolumeMinimal,
                 AdjustToTrade;
      };
//+------------------------------------------------------------------+

この構造体はコードのprivate部分で宣言されていることに注意する必要があります。これは私たちがこれからやろうとしていることにとって非常に重要です。ここで変数を宣言していないのは興味深いことです。実際、クラス内のグローバル変数はすべて、コードのprivate部分で宣言されなければなりません。これにより、最高レベルのセキュリティと情報のカプセル化が実現します。このトピックについては、より深く理解するために、実装期間中にまた触れることになるでしょう。プログラミングのベストプラクティスとして、クラスの内部変数に、そのクラスに属さない他のコード部分からアクセスすることは決して許さないことです。

クラス変数がどのように宣言されるかを見てみましょう。

   private :
      st_Terminal m_Infos;

今のところ、C_Terminalクラスにはグローバル変数とprivate変数が1つだけあり、そこに対応するデータを格納します。クラスの外からこのデータにアクセスする方法については後ほど説明します。このとき、許可なく情報が漏れたり、クラスに入ったりしないようにすることが非常に重要です。この概念に従うことが重要です。多くの新米プログラマーは、コンパイラがエラーとして示さなくても、クラス外のコードが内部変数の値を変更することを許可してしまいます。この方法ではカプセル化が損なわれ、クラスを認識せずに値を変更すると、検出と修正が困難なエラーやクラッシュが発生する可能性があるため、コードの安全性と管理性が大幅に低下します。

この後、構造体を整理しておくために新しいヘッダーファイルを作成する必要があります。このファイルはMacros.mqhと呼ばれ、最初は1行しかありません。

#define macroGetDate(A) (A - (A % 86400))

この行は日付情報を強調するために使用されます。関数の代わりにマクロを選ぶのは珍しいように思えるかもしれませんが、多くの状況ではマクロを使用する方が理にかなっています。これは、マクロがインライン関数としてコードに挿入され、できるだけ早く実行できるようにするためです。マクロの使用は、特に1つまたは別のリファクタリングをコード内で何度も繰り返す必要がある場合に、重大なプログラミングミスを犯す可能性を減らすという点でも正当化されます。

注意:このシステムでは、特にプログラミングを学び始めた方が読みやすく、理解しやすいように、ある箇所では高水準のプログラミング言語を使用するようにしています。高水準言語を使用するからといって、コードが遅くなるわけではなく、むしろ読みやすくなります。これをコードにどのように適用できるかをお見せしましょう。

可能な限り、高水準言語でコードを書くようにしてください。そうすれば、デバッグや改良がずっと簡単になるからです。また、コードはマシンのためだけに書かれるのではなく、他のプログラマーも理解する必要があることを忘れてはなりません。

すべてのグローバルマクロを定義するMacros.mqhヘッダーファイルを作成した後、このファイルをC_Terminal.mqhヘッダーファイルにインクルードします。その内容は以下の通りです。

#include "Macros.mqh"

ヘッダーファイル名は二重引用符で囲むことにご注意ください。なぜこのように表示され、小なり記号と大なり記号(< >)の間ではないのでしょうか。何か特別な理由があるのでしょうか。あります。二重引用符を使用することで、ヘッダーファイルへのパスは現在のヘッダーファイルがあるディレクトリ(この場合はC_Terminal.mqh)から始まるようにコンパイラに指示します。特定のパスが指定されていないため、コンパイラはC_Terminal.mqhファイルと同じディレクトリにあるMacros.mqhファイルを探します。したがって、プロジェクトのディレクトリ構造が変更されても、Macros.mqhファイルをC_Terminal.mqhファイルと同じディレクトリに保存すれば、コンパイラに新しいパスを伝える必要はありません。

小なり記号と大なり記号(< >)で囲まれた名前を使用して、コンパイラに、ビルドシステム上のあらかじめ定義されたディレクトリからファイルを探し始めるように指示します。MQL5の場合、このディレクトリはINCLUDEです。したがって、Macros.mqhファイルへのパスは、MQL5フォルダにあるこのディレクトリINCLUDEから指定する必要があります。もしプロジェクトのディレクトリ構造が変わったら、コンパイラがヘッダーファイルを見つけられるように、すべてのパスを再定義する必要があります。これは些細なことに思えるかもしれませんが、特定の方法を選択するかどうかで大きな違いが生まれます。

この違いを理解したところで、C_Terminalクラスの最初のコードを見てみましょう。このコードはクラスのprivateなので、外部からアクセスすることはできません。

void CurrentSymbol(void)
   {
      MqlDateTime mdt1;
      string sz0, sz1;
      datetime dt = macroGetDate(TimeCurrent(mdt1));
      enum eTypeSymbol {WIN, IND, WDO, DOL, OTHER} eTS = OTHER;
                
      sz0 = StringSubstr(m_Infos.szSymbol = _Symbol, 0, 3);
      for (eTypeSymbol c0 = 0; (c0 < OTHER) && (eTS == OTHER); c0++) eTS = (EnumToString(c0) == sz0 ? c0 : eTS);
      switch (eTS)
      {
         case DOL:
         case WDO: sz1 = "FGHJKMNQUVXZ"; break;
         case IND:
         case WIN: sz1 = "GJMQVZ";       break;
         default : return;
      }
      for (int i0 = 0, i1 = mdt1.year - 2000, imax = StringLen(sz1);; i0 = ((++i0) < imax ? i0 : 0), i1 += (i0 == 0 ? 1 : 0))
      if (dt < macroGetDate(SymbolInfoInteger(m_Infos.szSymbol = StringFormat("%s%s%d", sz0, StringSubstr(sz1, i0, 1), i1), SYMBOL_EXPIRATION_TIME))) break;
   }

このコードは複雑でわかりにくいかもしれませんが、かなり特殊な機能を持ちます。両建て注文システムで使用できるように資産名を生成することです。その方法を理解するために、プロセスを注意深く分析してみましょう。現在の焦点は、B3(ブラジル証券取引所)に従って指数先物取引とドル先物取引の名称を作ることです。これらの名前の生成の背後にあるロジックを理解することで、コードは将来の任意の契約の名前を生成するように適応させることができ、これらの契約は、以前の「一からの取引エキスパートアドバイザーの開発(第11部):両建て注文システム」で説明したように、両建て注文システムを介して動作することができます。しかし、ここでの目標は、EAが異なる条件、シナリオ、資産または市場に適応できるように、この機能を拡張することです。これは、EAがどのような種類の資産を扱うかを判断できるようにする必要があり、その結果、より多くの異なる種類の資産をコードに含める必要が生じるかもしれません。よりよく説明するために、それを細かく分けてみましょう。

MqlDateTime mdt1;
string sz0, sz1;
datetime dt = macroGetDate(TimeCurrent(mdt1));

この3行が、コード中で使用される変数です。ここでの主な問題は、これらの変数の初期化方法に関連していると思われます。TimeCurrent関数は、2つの異なる変数を1手順で初期化するために使用されます。最初の変数mdt1MqlDateTime型の構造体で、詳細な日付と時刻の情報をmdt1変数に格納し、TimeCurrentもmdt1に格納された値を返します。2つ目の変数dtは、マクロを使用して日付の値を取り出し、それを格納することで、1行のコードで2つの変数を完全に初期化することができます。

enum eTypeSymbol {WIN, IND, WDO, DOL, OTHER} eTS = OTHER;

この行は、列挙の作成、変数の宣言、初期値の代入を同時におこなっているので、異例に見えるかもしれません。この行を理解することが、関数の他の部分を理解する鍵となります。以下の点にご注意ください。MQL5では、列挙は名前がないと作成できないので、一番最初に名前を示します。列挙の中には、デフォルトでヌル値で始まる要素があります。これは変更可能ですが、後で対処しましょう。デフォルトでは、列挙はゼロから始まります。つまり、WINの値は0、INDの値は1、WDOの値は2という具合です。後で説明する理由により、入れたい要素の数に関係なく、最後の要素はOTHERでなければなりません。列挙を定義した後、この列挙のデータを使用する変数を宣言し、この変数の値を最後の要素、すなわちOTHERの値として開始します。

重要な注意事項列挙の宣言を見てみましょう。見覚えがあると思われませんか。名前も大文字で宣言することにご注意ください。何が起こるかというと、将来の契約で使用する資産を追加したい場合は、OTHER要素の前に契約名の最初の3文字を追加して、関数が現在の契約名を正しく生成できるようにする必要があります。例えば、雄牛の契約を追加したい場合、BGI値を列挙に挿入しなければなりません。これが最初の手順です。もう1つ、後で説明する手順があります。別の例:トウモロコシの先物契約を追加したい場合、OTHERの前に常にCCM値などを追加します。そうでなければ、列挙は期待通りに機能しません。

次のコードを考えてみましょう。上記の列挙と合わせて、仕事の最初のサイクルが完了します。

   sz0 = StringSubstr(m_Infos.szSymbol = _Symbol, 0, 3);
   for (eTypeSymbol c0 = 0; (c0 < OTHER) && (eTS == OTHER); c0++) eTS = (EnumToString(c0) == sz0 ? c0 : eTS);
   switch (eTS)
   {
      case DOL:
      case WDO: sz1 = "FGHJKMNQUVXZ"; break;
      case IND:
      case WIN: sz1 = "GJMQVZ";       break;
      default : return;
   }

最初のアクションは、資産名をprivateなグローバルクラス変数に保存することです。処理を簡単にするため、StringSubstr関数を使用して、クラスコードが実行される資産名の最初の3文字を取得し、変数sz0に保存します。これは最も簡単な段階です。ここで、非常に珍しい、しかし可能なことをしてみましょう。列挙を使用して、どの命名規則を契約に適用するかを決めるのです。そのためにforループを使用します。このループで使用されている式はかなり奇妙に見えるかもしれませんが、やっていることは、上で説明したように、列挙の中で元々定義されていた契約名を探しながら、列挙を反復していることです。デフォルトの列挙はゼロから始まるので、ローカルループ変数もゼロから始まります。どの要素が最初であろうと、ループはそこから始まり、OTHER要素が見つかるか、eTS変数がOTHERと異なるまで続きます。各反復で、列挙内の位置を上げていきます。さて、興味深い部分は次です。MQL5では、EnumToString関数を使用して、ループの各反復で列挙値を文字列に変換し、変数sz0に存在する値と比較します。これらの値が一致すると、eTS変数にポジションが保存され、その結果、OTHERとは異なるポジションになります。この関数は非常に興味深く、MQL5における列挙は他のプログラミング言語と同じように扱うべきではないことを示しています。ここでは、列挙を文字列の配列と考えることができ、他の言語よりもはるかに多くの機能と実用性を提供します。

eTS変数に希望する値を定義した後、次の手順は、各契約に特定の命名規則を定義することであり、これには変数sz1の適切な初期化が必要です。sz1の次の文字を選択するかどうかは、ここで紹介する方法論に従って、命名ルールに追加したい特定の契約を調査することによります。

資産が列挙に含まれておらず、対応するルールが見つからない場合、この関数は終了します。リプレイ/シミュレーションモードで資産を使用する場合は特にそうで、この種の資産というのはもともとパーソナライズされた特別なものだからです。このような場合、関数はここで終了します。

今度は別のループを調査します。これは「すべてがより複雑になっていく」段階です。このループの複雑さは多くのプログラマーを混乱させ、その機能を理解することを難しくしています。したがって、以下の説明にはさらに注意を払う必要があります。以下はこのループのコードです。

for (int i0 = 0, i1 = mdt1.year - 2000, imax = StringLen(sz1);; i0 = ((++i0) < imax ? i0 : 0), i1 += (i0 == 0 ? 1 : 0))
	if (dt < macroGetDate(SymbolInfoInteger(m_Infos.szSymbol = StringFormat("%s%s%d", sz0, StringSubstr(sz1, i0, 1), i1), SYMBOL_EXPIRATION_TIME))) break;

このコードは一見、わかりにくく複雑に見えるかもしれませんが、実はシンプルです。より効率的にするためにコードは短縮されています。物事を単純にし、不必要な複雑さを避けるために、厳密には必要ではありませんが、IFコマンドを使用します。理論的には、コマンド全体をFORループに含めることもできますが、そうすると説明が少し複雑になります。そのため、このループではIFを使用して、生成された契約の名前と取引サーバーに存在する名前の一致を確認し、将来の契約のうちどれが最も関連性があるかを判断しています。このプロセスを理解するためには、契約名を作成する際に使用される命名規則を知ることが重要です。例として、ブラジル証券取引所で取引されているミニドル先物契約が、ある命名規則に従っている場合、どうなるかを見てみましょう。

  • 契約名の最初の3文字はWDOとなる(有効期限や歴史的な契約かどうかに関係なく)
  • 満期月を示す記号が続く
  • 有効期限切れの年を示す2桁の値が続く

このように、契約名を数学的に構築するのが、このループの役割です。簡単な計算規則とループを使用して、契約名を作成し、その有効性を確認します。そのため、どのようにおこなわれるかを理解するためには、説明に従うことが重要です。

まず、必要な会計単位となる3つのローカル変数をループ内で初期化します。ループは最初の反復を実行しますが、特に興味深いのは、これがループ本体内ではなくifコマンド内で発生することです。ただし、ifコマンドと同じコードをforループ内のコロン(;)の間に置くことができ、ループは同様に動作します。この相互作用で何が起こるかを理解しましょう。まず、契約成立のための特定のルールに従って、契約名を作成します。StringFormat関数を使用して、必要な名前を取得し、後でアクセスできる銘柄名として保存します。すでに契約名がわかっている場合は、取引サーバーに資産のプロパティの1つである、SYMBOL_EXPIRATION_TIME列挙を使用した契約の有効期限を要求します。SymbolInfoInteger関数は値を返しますが、私たちは日付にしか興味がありません。この値を正確に抽出するために、有効期限と現在の日付を比較できるマクロを使用します。戻り値が未来の日付の場合、直近の契約を変数に定義したので、ループは終了します。というのも、2000年から始まる年はすでに過去の年であり、新たな反復が必要となるからです。すべてのプロセスを繰り返す前に、ポジションを増やして新しい契約名を作る必要があります。ここでは注意が必要です。この増加は、期限切れコードで最初におこなわなければならないからです。どの有効期限コードも今年満足のいくものでなかった場合のみ、年を上げます。この動作は3段階でおこなわれます。コードでは、このインクリメントを実行するために2つの三項演算子を使用しています。

ループが再び繰り返される前、そして三項演算子が実行される前にも、有効期限切れの月記号を示す値をインクリメントします。このインクリメントの後、最初の三項演算子を使用して値が許容範囲内かどうかを確認します。従って、インデックスは常に、満期月に有効な値のいずれかを示すことになります。次の手順は、2つ目の三項演算子で有効期限月を確認することです。満了月インデックスがゼロの場合、すべての月が確認されたことになります。その後、有効な契約を見つけるための新たな試みのために現在の年をインクリメントし、この確認はifコマンドで再びおこなわれます。このプロセスは有効な契約が見つかるまで繰り返され、システムが現在の契約名を検索する仕組みを示しています。これはマジックではなく、数学とプログラミングの組み合わせです。

この説明が、このプロシージャのコードがどのように機能するかを理解するのに役立てば幸いです。複雑で長い文章になりましたが、私のゴールは、同じ概念を将来の他の契約の機能実装に適用して、歴史をたどることができるように、わかりやすい方法で説明することでした。契約の有無にかかわらず、コードは常に正しい契約を使用するからです。

では、クラスのコンストラクタを参照している次のコードを解析してみましょう。

C_Terminal()
{
   m_Infos.ID = ChartID();
   CurrentSymbol();
   ChartSetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR, false);
   ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, 0, true);
   ChartSetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE, false);
   m_Infos.nDigits = (int) SymbolInfoInteger(m_Infos.szSymbol, SYMBOL_DIGITS);
   m_Infos.Width   = (int)ChartGetInteger(m_Infos.ID, CHART_WIDTH_IN_PIXELS);
   m_Infos.Height  = (int)ChartGetInteger(m_Infos.ID, CHART_HEIGHT_IN_PIXELS);
   m_Infos.PointPerTick  = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_SIZE);
   m_Infos.ValuePerPoint = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_VALUE);
   m_Infos.VolumeMinimal = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_VOLUME_STEP);
   m_Infos.AdjustToTrade = m_Infos.PointPerTick / m_Infos.ValuePerPoint;
}

このコードは、グローバル変数構造内の値が正しく初期化されることを保証します。MetaTrader 5プラットフォームの動作を変更する部分にご注意ください。起こるのは次です。MetaTrader 5プラットフォームに、このコードが適用されたチャート上のオブジェクトの説明を生成しないよう指示します。別の行では、チャートからオブジェクトが削除されるたびに、MetaTrader 5プラットフォームはどのオブジェクトが削除されたかを通知するイベントを生成する必要があることを示します。この行では、時間スケールの削除を示します。現段階で必要なのはこれだけです。続く行では、資産に関する情報を収集します。

次のコードはクラスのデストラクタです。

~C_Terminal()
{
   ChartSetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE, true);
   ChartSetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR, true);
   ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, 0, false);
}

このデストラクタコードでは、クラスコンストラクタコードが実行される前の状態をリセットし、チャートを元の状態に戻します。正確には元の状態ではないかもしれませんが、時間軸は再びチャート上に表示されます。では、チャートの振る舞いをクラスによって変更できる場合を考えてみましょう。小さな構造体を作り、コンストラクタとデストラクタのコードを変更して、実際にクラスが変更する前の状態にチャートを戻します。これは次のようにおこなわれます。


   private :
      st_Terminal m_Infos;
      struct mem
      {
         long    Show_Descr,
                 Show_Date;
      }m_Mem;
//+------------------------------------------------------------------+
   public  :
//+------------------------------------------------------------------+          
      C_Terminal()
      {
         m_Infos.ID = ChartID();
         CurrentSymbol();
         m_Mem.Show_Descr = ChartGetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR);
         m_Mem.Show_Date  = ChartGetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE);
         ChartSetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR, false);
         ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, 0, true);
         ChartSetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE, false);
         m_Infos.nDigits = (int) SymbolInfoInteger(m_Infos.szSymbol, SYMBOL_DIGITS);
         m_Infos.Width   = (int)ChartGetInteger(m_Infos.ID, CHART_WIDTH_IN_PIXELS);                                
	 m_Infos.Height  = (int)ChartGetInteger(m_Infos.ID, CHART_HEIGHT_IN_PIXELS);
         m_Infos.PointPerTick  = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_SIZE);
         m_Infos.ValuePerPoint = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_VALUE);
         m_Infos.VolumeMinimal = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_VOLUME_STEP);
         m_Infos.AdjustToTrade = m_Infos.PointPerTick / m_Infos.ValuePerPoint;
      }
//+------------------------------------------------------------------+
      ~C_Terminal()
      {
         ChartSetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE, m_Mem.Show_Date);
         ChartSetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR, m_Mem.Show_Descr);
         ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, 0, false);
      }

このグローバル変数は、データを格納する構造体を表します。こうすることで、コードによって変更される前のチャートを知ることができます。ここでは変更前のデータを取り込み、この時点でチャートを元の状態に戻します。簡単なコードの変更で、システムがより良く、より便利になることにご注目ください。注目すべきは、グローバル変数はクラスの全生涯にわたってデータを保存するということです。しかし、これを理解するためには、クラスを単なるコードセットとして考えるのではなく、クラスをオブジェクトや特別な変数のように考えることが非常に重要です。作成されるとコンストラクタのコードが実行され、削除されるか不要になるとデストラクタのコードが呼び出されます。これは自動におこなわれます。まだこの仕組みを完全に理解していないとしても、心配する必要はありません。この概念は後で明らかになるでしょう。現時点では、クラスは単なるコードの束ではなく、実際には特別なものであり、そのように扱われるべきであるということを理解する必要があります。

このトピックを終える前に、他の2つの関数を簡単に見てみましょう。詳しくは次回に譲りますが、ここではそのコードの一部を見てみましょう。次がコードです。

//+------------------------------------------------------------------+
inline const st_Terminal GetInfoTerminal(void) const 
{

   return m_Infos;
}
//+------------------------------------------------------------------+
virtual void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
{

   switch (id)
   {
      case CHARTEVENT_CHART_CHANGE:
         m_Infos.Width = (int)ChartGetInteger(m_Infos.ID, CHART_WIDTH_IN_PIXELS);
         m_Infos.Height = (int)ChartGetInteger(m_Infos.ID, CHART_HEIGHT_IN_PIXELS);
         break;
   }
}
//+------------------------------------------------------------------+

この2つの関数はあらゆる点で特別です。これらの説明は、実際に使用するときに、よりよく説明できると思いますので、簡単に付け加えておきましょう。この関数を使用すると、クラス本体以外のコードでも、クラスに含まれるグローバル変数のデータにアクセスできるようになります。この側面は、これから開発するコードを通して広範囲にカバーされます。このようなアプローチを取ることで、クラスが知らないうちに変数の値が変更される危険性がなくなります。このような種類の問題を回避するためにコンパイラを使用するためです。ただし、ここには将来解決する問題があります。この関数は、チャートが変更されたときにクラスデータを更新するためにすでに使用されています。これらの値は、チャート上に何かを描画する際に、コードの他の部分で使用されることが多くなります。繰り返しになりますが、今後詳しく見ていくことにしましょう。


結論

この記事で取り上げた内容から、すでに基本クラスC_Terminalを形成しています。しかし、まだ説明しなければならない関数があります。次回は、このためにC_Mouseクラスを作成します。ここで取り上げたことで、このクラスを使用して便利なものを作ることができます。私たちの仕事は始まったばかりなので、ここに関連するコードは添付しません。今提示されるコードは実用的ではありません。次回は、チャート作成に役立つものを作成しましょう。デモ口座やリアル口座、さらにはリプレイ/シミュレーションシステムでの運用をサポートする指標やその他のツールを開発していきます。では、次の記事でお会いしましょう。

MetaQuotes Ltdによりポルトガル語から翻訳されました。
元の記事: https://www.mql5.com/pt/articles/11328

リプレイシステムの開発(第27回):エキスパートアドバイザープロジェクト-C_Mouseクラス(I) リプレイシステムの開発(第27回):エキスパートアドバイザープロジェクト-C_Mouseクラス(I)
この記事では、C_Mouseクラスを実装します。このクラスは、最高水準でプログラミングする能力を提供します。しかし、高水準や低水準のプログラミング言語について語ることは、コードに卑猥な言葉や専門用語を含めることではありません。逆です。高水準プログラミング、低水準プログラミングというのは、他のプログラマーが理解しやすいか、しにくいかという意味です。
リプレイシステムの開発 - 市場シミュレーション(第25回):次の段階への準備 リプレイシステムの開発 - 市場シミュレーション(第25回):次の段階への準備
この記事では、リプレイ/シミュレーションシステム開発の第1段階を完了しました。この成果により、システムが高度なレベルに達したことを確認し、新機能の導入への道を開くことができました。目標は、システムをさらに充実させ、市場分析の調査開発のための強力なツールに変えることです。
リプレイシステムの開発(第28回):エキスパートアドバイザープロジェクト-C_Mouseクラス(II) リプレイシステムの開発(第28回):エキスパートアドバイザープロジェクト-C_Mouseクラス(II)
人々が初めてコンピューティングが可能なシステムを作り始めたとき、すべてには、プロジェクトを熟知しているエンジニアの参加が必要でした。コンピュータ技術の黎明期、プログラミング用の端末すらなかった時代の話です。それが発展し、より多くの人々が何かを創造できることに興味を持つようになると、新しいアイデアやプログラミングの方法が現れ、以前のようなコネクタの位置を変えるスタイルに取って変わりました。最初の端末が登場したのはこの時です。
リプレイシステムの開発 - 市場シミュレーション(第24回):FOREX (V) リプレイシステムの開発 - 市場シミュレーション(第24回):FOREX (V)
本日は、Last価格に基づくシミュレーションを妨げていた制限を取り除き、このタイプのシミュレーションに特化した新しいエントリポイントをご紹介します。操作の仕組みはすべて、FOREX市場の原理に基づいています。この手順の主な違いは、BidシミュレーションとLastシミュレーションの分離です。ただし、時間をランダム化し、C_Replayクラスに適合するように調整するために使用された方法は、両方のシミュレーションで同じままであることに注意することが重要です。これは良いことです。特にティック間の処理時間に関して、一方のモードを変更すれば、もう一方のモードも自動的に改善されるからです。