MQL5 プログラムのデバッグ
はじめに
本稿は第一にすでに言語を学習したがまだプログラム開発を完全にはマスターしていないプログラマーを対象としています。プログラムをデバッグする際、どの開発者も対処する主要な問題を強調しています。そこでデバッグとは何でしょうか?
デバッグ とはプログラム実行エラーを検出し取り除くためのプログラム開発段階です。デバッグ処理中開発者は発生可能な問題を検出しようとアプリケーションを解析します。解析用データは変数を観察することとプログラムを実行することで入手されます(どの関数がいつ呼ばれるか)。
補間的なデバッグテクノロジーには2とおりあります。
- デバッガの利用-開発されたプログラムの実行を手順を踏んで表示するユーティリティ
- ジャーナルまたはファイル内の変数状態と関数の反復の画面上のインタラクティブディスプレイです。
みなさんに変数、ストラクチャなどを含む MQL5の知識があるとします。ただしまだご自身でプログラム開発をした経験はないとします。そうするとまず最初に行うことはコンパイルです。実際これはデバッグの第一段階なのです。
1. コンパイル
コンパイル とはソースコードを高レベルのプログラム言語から低レベルのプログラム言語に変換することです。
MetaEditor コンパイラがプログラムをネイティブコードではなくバイトコードに変換します(詳細についてはリンクに従います)。これにより暗号化プログラムの作成が可能となります。その上、バイトコードは32ビット、64ビット処理システムの両方d利用可能です。
ではコンパイルに戻ります。これはデバッグの第一段階です。F7 (または「コンパイル」ボタン)を押すと、MetaEditor 5 はコードを書くときにできたすべてのエラーを報告します。『ツールボックス』ウィンドウの『エラー』タブ"には検出されたエラーの記述とその位置情報があります。強調表示された記述行でカーソルか「エンター」を押して直接エラーに移動します。
コンパイラで検出されるエラーは2タイプです。
- シンタックスエラー(赤色表示)-このエラーが取り除かれるまでソースコードは コンパイルされません。
- 警告(黄色表示)-コードはコンパイルされますが、その誤りを修正するほうがよいでしょう。
シンタックスエラーはよく不注意で起こります。たとえば、"," と ";" は変数宣言の際混乱しやすいものです。
int a; b; // incorrect declaration
コンパイラはそのような宣言の場合、エラーを返します。以下は正しい宣言の記述です。
int a, b; // correct declaration
または
int a; int b; // correct declaration
警告も無視してはいけません(開発者の多くはそれらに無頓着なものです)。MetaEditor 5 がコンパイル中に警告を返すと、プログラムは作成されますが、予定通り動作する保証はありません。
警告は MQL5 開発者がプログラマーの一般的なタイプミスを体系化する多大な労力の氷山の一角に過ぎません。
2つの変数を比較しようとしているとします。
if(a==b) { } // if a is equal to b, then ...
タイプミスまたは不注意で "=="の代わりに"=" を使ってしまいました。この場合、コンパイラはコードを以下のように解釈します。
if(a=b) { } // assign b to a, if a is true, then ... (unlike MQL4, it is applicable in MQL5)
見てのとおりこのタイプミスはプログラム処理を大きく変えてしまう可能性があります。そのためコンパイラはこの行に対して警告を出すのです。
まとめます: コンパイルはデバッグの第一段階です。コンパイラの警告は無視してはいけません。
図1 コンパイル中のデバッグデータ
2. デバッガ
デバッグの第二段階は デバッガ (ホットキーF5)の使用です。デバッガは段階的に実行するエミュレーションモードでプログラムを起動します。デバッガはMetaEditor 5の新機能でMetaEditor 4にはなかったものです。これはプログラマーが MQL4 から MQL5に切り替えてそれを使用する経験がなかったためです。
デバッガのインターフェースは3つのメインボタンと3つの補助ボタンを持ちます。
- 開始[F5] -デバッグを開始します。
- ポーズ [Break] -デバッグを一時停止します。
- ストップ [Shift+F5] -デバッグを中止します。
- ステップイン [F11] -ユーザーはこの行で呼ばれる関数内に移動します。
- ステップオーバー [F10] -デバッガはこの文字列で呼ばれる関数の本文を無視し次の行に移動します。
- ステップアウト [Shift+F11] -ユーザーは今いる関数の本体を出ます。
以上がデバッガのインターフェースです。ではそれをどのように使うのでしょうか?プログラムのデバッグはプログラマーが特殊なデバッグ関数 DebugBreak() を設定する行、または F9 ボタンを押して設定されるブレークポイント、またはツールバーの特別なボタンを押して開始します。
図2 ブレークポイントの設定
ブレークポイントがないと、デバッガはプログラムを実行しデバッグは問題なしと報告しますが、それでは何も確認することにはなりません。DebugBreakを使用し、関心のないコードは飛ばして問題があると考える行からプログラムを段階的に確認していきます。
デバッガを起動し、正しい位置に DebugBreak を入れるとプログラム実行を検証していることになるのです。次は?プログラムに何が起こっているか理解するのにどのように役立つのでしょうか?
まず、デバッガウィンドウの左側を見ます。そこには関数名と今いる行番号が表示されています。次にウィンドウの右側を見ます。そこには何もありませんんが、「数式」フィールドに任意の変数名を入力することができます。「値」フィールドに現在値を確認する変数名を入力します。
変数は選択し、ホットキー [Shift+F9] を使って、または以下に表示されているコンテクストメニューから追加することができます。
図3 デバッグ時注意する変数追加
現時点でいるコード行を追跡し、重要な変数値を閲覧することができるのです。これらをすべて分析すると次第にプログラムが正常に処理しているか知ることができます。
まだ宣言されている関数に到達していないとき、関心のある変数がローカルで宣言されているのではないかと心配する必要はありません。変数の範囲外にいると、それは『不明な識別子』の値を持つのです。それはその変数が宣言されていないことを意味します。それがデバッガのエラーを起こすことはありません。変数の範囲に到達したら、その値とタイプを確認します。
図4 デバッグプロセス変数値の閲覧
これがデバッガの主な機能です。テスターセクションはデバッガでできないことを表示します。
3. プロファイラ
コード プロファイラはデバッガに対する重要な追加部分です。実際、これは最適化で構成されるプログラムデバッグの最終段階です。
プロファイラは『プロファイルの開始』ボタンをクリックすると MetaEditor 5 メニューから呼ばれます。デバッガが行う段階的プログラム解説の代わりにプロファイラがプログラムを実行します。プログラムがインディイケータであったり Expert Advisorであると、プロファイラはプログラムがアンロードされるまで動作します。アンロードはチャートからインディケータや Expert Advisor を除外すること、また『プロファイルの中止』をクリックすることで行われます。
プロファイルにより重要な統計を取得します。各関数が呼ばれた回数、その関数の実行にかかった時間です。おそらくみなさんはパーセント表示の統計データに少し混乱されるでしょう。統計はネスト化された関数に配慮しないことを理解する必要があります。よって全パーセント値の合計は大幅に 100%を越えます。
しかしその事実にもかかわらず、プロファイラはまだプログラム最適化にとって力強いツールであり、それによりどの関数がすぴいーどに対して最適化される必要があり、どこで資金を節約することができるかユーザーが考えることができるのです。
図5 プロファイラ処理結果
4. 双方向性
私はメッセージ表示関数Print およびComment は主要なデバッグツールであると考えます。それらは使い勝手が良いというのが第一です。次に言語の先行バージョンから MQL5 に切り替えているプログラマーはこの関数をすでに知っています。
『プリント』関数は渡されるパラメータをログファイルとテキスト文字列として「エキスパート」ツールに送信します。送信時刻と関数を読んだプログラム名はテキストの左に表示されます。デバッグ中、どの値が変数中にあるか明確にするために関数が使われます。
変数値以外にこれら関数の呼び出しシーケンスを知ることが必要なこともあります。そのために"__FUNCTION__" および "__FUNCSIG__" マクロ が使用されます。最初のマクロは呼ばれる関数名を持つ文字列を返します。一方、二番目のマクロは呼ばれる関数のパラメータリストを追加で表示します。
以下でマクロの使用を確認することができます。
//+------------------------------------------------------------------+ //| Example of displaying data for debugging | //+------------------------------------------------------------------+ void myfunc(int a) { Print(__FUNCSIG__); // display data for debugging //--- here is some code of the function itself }
私はマクロ "__FUNCSIG__" の使用を好みます。というのも、これはオーバーロードされた関数(名前は同じだがパラメータは異なる)の差を表示するからです。
呼び出しの中にはスキップする、または特殊な関数呼び出しに注目する必要ものもよくあります。このため「プリント」関数が条件によって保護されます。たとえばプリン度は1013回目の反復後呼ばれます。
//+------------------------------------------------------------------+ //| Example of data output for debugging | //+------------------------------------------------------------------+ void myfunc(int a) { //--- declare the static counter static int cnt=0; //--- condition for the function call if(cnt==1013) Print(__FUNCSIG__," a=",a); // data output for debugging //--- increment the counter cnt++; //--- here is some code of the function itself }
「コメント」関数に対しても同様です。それはチャートの左上のコメントに表示されます。これは大きなメリットです。デバッグ中他の画面に切り替える必要がないからです。ただし関数使用中、各新しいコメントは前のコメントを削除します。それはデメリット(ときとして便利ですが)のように思われます。
変数に新しい文字列を追加記述してこの欠点を解消します。まず文字列タイプ変数が宣言され(ほとんどの場合グローバルに)、空の値で初期化されます。それから各新規テキスト文字列が追加の改行文字と共に冒頭に入れられます。一方で前の変数値は末尾に追加されます。
string com=""; // declare the global variable for storing debugging data //+------------------------------------------------------------------+ //| Example of data output for debugging | //+------------------------------------------------------------------+ void myfunc(int a) { //--- declare the static counter static int cnt=0; //--- storing debugging data in the global variable com=(__FUNCSIG__+" cnt="+(string)cnt+"\n")+com; Comment(com); // вывод информации для отладки //--- increase the counter cnt++; //--- here is some code of the function itself }
ここでプログラム内容を詳しく閲覧する機会を得ます。ファイルへのプリントです。「プリント」および「コメント」関数は常に大容量データや高速プリントに適しているわけではありません。前者は変更を表示するのに十分な時間がないこともあります(呼び出しが混乱を招く表示に先行して実行することがあるため)。後者はゆっくり処理を行うためです。その上コメントは再読出しされず細かく検討されません。
呼び出しのシーケンスを確認したり大容量のデータをログする必要があれば、ファイルへのプリントはもっとも便利なデータアウトプットの方法です。ただ、プリントは反復で毎回使用されませんが、その代りファイルの末尾で行われ、一方、上記の原則に従って、反復ごとにデータは文字列変数に保存される(唯一の違いは新規データは追加で変数末尾に書き込まれることです)ことを念頭に置く必要があります。
string com=""; // declare the global variable for storing debugging data //+------------------------------------------------------------------+ //| Program shutdown | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- saving data to the file when closing the program WriteFile(); } //+------------------------------------------------------------------+ //| Example of data output for debugging | //+------------------------------------------------------------------+ void myfunc(int a) { //--- declare the static counter static int cnt=0; //--- storing debugging data in the global variable com+=__FUNCSIG__+" cnt="+(string)cnt+"\n"; //--- increment the counter cnt++; //--- here is some code of the function itself } //+------------------------------------------------------------------+ //| Save data to file | //+------------------------------------------------------------------+ void WriteFile(string name="Отладка") { //--- open the file ResetLastError(); int han=FileOpen(name+".txt",FILE_WRITE|FILE_TXT|FILE_ANSI," "); //--- check if the file has been opened if(han!=INVALID_HANDLE) { FileWrite(han,com); // печать данных FileClose(han); // закрытие файла } else Print("File open failed "+name+".txt, error",GetLastError()); }
WriteFile は OnDeinitで呼ばれます。そのためプログラムに生じる変更はすべてファイルに書き込まれます。
注意:ログが大きすぎる場合、複数の変数に格納する方が賢明でしょう。 それを行うもっともよい方法はテキスト変数のコンテンツを文字列タイプの配列セルに格納し、com 変数をゼロ設定することです(動作の次段階への準備)。
それは文字列 100万~200万1-2 の後に行われます(再現性のない入力)。まず変数オーバーフローによるデータ損失を避けます(ところでどんなに努力してもそれはできませんでした。なぜなら開発者が文字列タイプに懸命に取り組んだからです)。それからもっとも重要な点です。エディタで巨大ファイルを開く代わりに複数ファイルにデータを表示することができるようになります。
保存されている文字列の量を常に追跡することがないようにファイルと連携する関数を3つの部分に分割することができます。最初の部分はファイルのオープンです。二番目は反復ごとのファイルへの書き込み。三番目はファイルのクローズです。
//--- open the file int han=FileOpen("Debugging.txt",FILE_WRITE|FILE_TXT|FILE_ANSI," "); //--- print data if(han!=INVALID_HANDLE) FileWrite(han,com); if(han!=INVALID_HANDLE) FileWrite(han,com); if(han!=INVALID_HANDLE) FileWrite(han,com); if(han!=INVALID_HANDLE) FileWrite(han,com); //--- close the file if(han!=INVALID_HANDLE) FileClose(han);
ただしこの方法は注意して行う必要があります。プログラムの実行に失敗すると(たとえばゼロ除算など)、処理システムの動作を混乱させる手におえないオープンしたファイルを取得する可能性があります。
また反復ごとに完全なオープン・ライト・クローズループを使うことはまったくお薦めできません。私の個人的経験では、その場合ハードドライブが数か月で使い物にならなくなります。
5. テスター
Expert Advisorsをデバッグするときはよく特殊な条件をいくつか有効にする必要があります。ですが上記のデバッガはリアルタイムモードでのみ Expert Advisor を起動し、そういった条件が最終的に有効になる間長い時間待ちます。
事実特定のトレード条件が生じるのは稀です。ただそれが生じることは解りませんが、何か月もそれを待つのは不合理なことでしょう。ではどうすればよいのでしょうか?
この場合ストラレジーテスタが役に立ちます。同じプリントとコメント関数がデバッグに使われます。コメントはつねに状況評価するには最初に発生します。一方でプリント関数はより詳細な分析のために使用されます。テスターはテスターログに表示されるデータを格納します(書くテスターエージェントに個別のディレクトリ)。
正しい間隔で Expert Advisor を起動するのに、私は時間をローカライズし(私の意見では失敗が起こる箇所)、テスターに必要なデータを設定し、すべてのティックで可視化モードにて起動します。
またこのデバッグ法はプログラム実行中にデバッグするほとんど唯一の方法である MetaTrader 4 から拝借したことをお伝えしたいと思います。
図6 ストラレジーテスタを使ったデバッグ
6. OOPでのデバッグ
MQL5で登場したオブジェクト指向プログラミングOはでバグ手順に影響を与えました。手順をデバッグするとき、関数名だけで簡単にプログラム内を検索することができるのです。ただし OOPではよく異なるメソッドが呼ばれるオブジェクトを知る必要があります。特にオブジェクトが縦方向に(継承を利用して)設計されている場合重要です。その場合テンプレート(最近 MQL5に採用されました)が役に立ちます。
テンプレート関数によりポインタータイプを文字列タイプ値として取得することができます。
template<typename T> string GetTypeName(const T &t) { return(typename(T)); }
私はこのプロパティを以下の方法でデバッグするのに使用します。
//+------------------------------------------------------------------+ //| Base class contains the variable for storing the type | //+------------------------------------------------------------------+ class CFirst { public: string m_typename; // variable for storing the type //--- filling the variable by the custom type in the constructor CFirst(void) { m_typename=GetTypeName(this); } ~CFirst(void) { } }; //+------------------------------------------------------------------+ //| Derived class changes the value of the base class variable | //+------------------------------------------------------------------+ class CSecond : public CFirst { public: //--- filling the variable by the custom type in the constructor CSecond(void) { m_typename=GetTypeName(this); } ~CSecond(void) { } };
基本クラスにはその他言うを格納する変数があります(変数は各オブジェクトのコンストラクタで初期化されます)。派生クラスもタイプを格納するためにこの変数の値を使用します。そのためマクロが呼ばれるとき、呼ばれる関数名だけでなくこの関数を呼ぶオブジェクトタイプを取得する変数m_typename を追加します。
ポインター自体はユーザーが数字によってオブジェクトを差別化できるオブジェクトをより正確に認識するために派生します。オブジェクト内部ではこれは以下のように行われます。
Print((string)this); // print pointer number inside the class
一方外部では以下のような記述となります。
Print((string)GetPointer(pointer)); // print pointer number outside the class
またオブジェクト名を格納する変数は各クラス内でも使用できます。その場合オブジェクト名をコンストラクタのパラメータとしてオブジェクト作成時に渡すことが可能です。これによりオブジェクトをその番号で分割するだけえでなく各オブジェクトが何を表すのか理解することもできます(名前のように)。この方法は m_typename 変数に似た方法で実現することができます。
7. トレース
上記方法はすべてお互いに補完しデバッグにはひじょうに重要です。ただあまり一般的でない方法もあります。それはトレースです。
その複雑さゆえにこの方法が使用されるのは稀です。行き詰り何がおこっているのかわからないとき、トレースが役に立ちます。
この方法によりアプリケーションのストラクチャ、すなわちシーケンスおよび呼び出しのオブジェクトを知ることができます。トレースを利用することでプログラムの何がおかしいのかわかります。その上、この方法はプロジェクトの概要を示してくれます。
トレースは以下のように処理されます。マクロを2件作成します。
//--- opening substitution #define zx Print(__FUNCSIG__+"{"); //--- closing substitution #define xz Print("};");
これらはそれぞれオープンzx およびクローズ xz のマクロです。それではそれらをトレースされる関数本文に入れましょう。
//+------------------------------------------------------------------+ //| Example of function tracing | //+------------------------------------------------------------------+ void myfunc(int a,int b) { zx //--- here is some code of the function itself if(a!=b) { xz return; } // exit in the middle of the function //--- here is some code of the function itself xz return; }
関数が条件に従った終了を含んでいれば、各戻り値の前の保護された領域にクローズの xz を設定します。これでストラクチャのトレースを混乱させなくてすみます。
上述のマクは例をシンプルにするためだけに使用されていることに注意が必要です。トレースにはファイルへのプリントを使用する方がよいです。また、私はファイルにプリントする際、ティックを1つ使用します。トレースのストラクチャ全体を確認するには、以下の構文に関数名をラップします。
if() {...}
結果としてのファイルは MetaEditorでそれを開き、トレースのストラクチャを表示するために スタイラー [Ctrl+,] を使うことのできる ".mqh" 拡張子を伴って設定します。
以下はトレースの完全コードです。
string com=""; // declare global variable for storing debugging data //--- opening substitution #define zx com+="if("+__FUNCSIG__+"){\n"; //--- closing substitution #define xz com+="};\n"; //+------------------------------------------------------------------+ //| Program shutdown | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- //--- saving data to the file when closing the program WriteFile(); } //+------------------------------------------------------------------+ //| Example of the function tracing | //+------------------------------------------------------------------+ void myfunc(int a,int b) { zx //--- here is some code of the function itself if(a!=b) { xz return; } // exit in the middle of the function //--- here is some code of the function itself xz return; } //+------------------------------------------------------------------+ //| Save data to file | //+------------------------------------------------------------------+ void WriteFile(string name="Tracing") { //--- open the file ResetLastError(); int han=FileOpen(name+".mqh",FILE_WRITE|FILE_TXT|FILE_ANSI," "); //--- check if the file has opened if(han!=INVALID_HANDLE) { FileWrite(han,com); // print data FileClose(han); // close the file } else Print("File open failed "+name+".mqh, error",GetLastError()); }
特定箇所からトレースを開始するには、マクロを条件で補足する必要があります。
bool trace=0; // variable for protecting tracing by condition //--- opening substitution #define zx if(trace) com+="if("+__FUNCSIG__+"){\n"; //--- closing substitution #define xz if(trace) com+="};\n";
この場合、特定イベント後または特定箇所の "trace" 変数を『真』または『偽』に設定した後、トレースを有効または無効にすることができます。
のちに必要となるにもかかわらずトレースがすでに必要でないか、またはその時点でソースを消去する時間がなければ、マクロ値を空の値に変更することで無効化することができます。
//--- substitute empty values #define zx #define xz
トレースへの変更を持つ標準Expert Advisor のファイルは以下に添付しています。トレース結果はチャート(tracing.mqhファイルが作成されます)上に Expert Advisor を起動したあと「ファイル」ディレクトリで確認可能です。以下は作成されたファイルテキストの一部です。
if(int OnInit()){ }; if(void OnTick()){ if(void CheckForOpen()){ }; }; if(void OnTick()){ if(void CheckForOpen()){ }; }; if(void OnTick()){ if(void CheckForOpen()){ }; }; //--- ...
ネスト化された呼び出しのストラクチャは新規に作成されたファイルで初めに明確に決められませんが、コードのスタイラーを使用したあと、ストラクチャ全体を確認することができます。以下はスタイラー使用後作成されたファイルのテキストです。
if(int OnInit()) { }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ if(void OnTick()) { if(void CheckForOpen()) { }; }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ if(void OnTick()) { if(void CheckForOpen()) { }; }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ if(void OnTick()) { if(void CheckForOpen()) { }; }; //--- ...
これは私だけの技でトレースが行われる方法例ではありません。みなさんはご自身の方法でトレースを行ってください。重要なことはトレースは関数呼び出しのストラクチャにあるということです。
デバッグに関する重要な注意点
デバッグ中コードに変更を加える場合、直接MQL5 関数の呼び出しをラップします。以下がその方法です。
//+------------------------------------------------------------------+ //| Example of wrapping a standard function in a shell function | //+------------------------------------------------------------------+ void DebugPrint(string text) { Print(text); }
これで簡単にデバッグ完了時コードを消去することができます。
- "DebugPrint" 関数呼び出しを削除します。
- その後コンパイルし、
- MetaEditor がコンパイルエラーについて警告している行でこの関数の呼び出しを削除します。
デバッグで使用される変数に対しても同様に行います。そのためグローバルに宣言される変数および関数を使うようにします。そうするとアプリケーションの奥深くに埋もれたコンストラクションを探す必要がなくなります。
おわりに
デバッグはプログラム動作の重要な一部です。プログラムデバッグができないにとはプログラマーとは名乗れません。ただ主要なデバッグはつねに頭の中で行われるものです。本稿はデバッグに使用される方法をいくつかお伝えするに過ぎません。とはいうものの、アプリケーション処理原理を理解していないとこういった方法は何の役にも立ちません。
みなさんのデバッグが成功しますように!
MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/654
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索