MQL5 プログラムのデバッグ

Mykola Demko | 25 11月, 2015

はじめに

本稿は第一にすでに言語を学習したがまだプログラム開発を完全にはマスターしていないプログラマーを対象としています。プログラムをデバッグする際、どの開発者も対処する主要な問題を強調しています。そこでデバッグとは何でしょうか?

デバッグ とはプログラム実行エラーを検出し取り除くためのプログラム開発段階です。デバッグ処理中開発者は発生可能な問題を検出しようとアプリケーションを解析します。解析用データは変数を観察することとプログラムを実行することで入手されます(どの関数がいつ呼ばれるか)。

補間的なデバッグテクノロジーには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 コンパイル中のデバッグデータ

図1 コンパイル中のデバッグデータ


2. デバッガ

デバッグの第二段階は デバッガ (ホットキーF5)の使用です。デバッガは段階的に実行するエミュレーションモードでプログラムを起動します。デバッガはMetaEditor 5の新機能でMetaEditor 4にはなかったものです。これはプログラマーが MQL4 から MQL5に切り替えてそれを使用する経験がなかったためです。

デバッガのインターフェースは3つのメインボタンと3つの補助ボタンを持ちます。

以上がデバッガのインターフェースです。ではそれをどのように使うのでしょうか?プログラムのデバッグはプログラマーが特殊なデバッグ関数 DebugBreak() を設定する行、または F9 ボタンを押して設定されるブレークポイント、またはツールバーの特別なボタンを押して開始します。

図2 ブレークポイントの設定

図2 ブレークポイントの設定

ブレークポイントがないと、デバッガはプログラムを実行しデバッグは問題なしと報告しますが、それでは何も確認することにはなりません。DebugBreakを使用し、関心のないコードは飛ばして問題があると考える行からプログラムを段階的に確認していきます。

デバッガを起動し、正しい位置に DebugBreak を入れるとプログラム実行を検証していることになるのです。次は?プログラムに何が起こっているか理解するのにどのように役立つのでしょうか?

まず、デバッガウィンドウの左側を見ます。そこには関数名と今いる行番号が表示されています。次にウィンドウの右側を見ます。そこには何もありませんんが、「数式」フィールドに任意の変数名を入力することができます。「値」フィールドに現在値を確認する変数名を入力します。

変数は選択し、ホットキー [Shift+F9] を使って、または以下に表示されているコンテクストメニューから追加することができます。

図3 デバッグ時注意する変数追加

図3 デバッグ時注意する変数追加

現時点でいるコード行を追跡し、重要な変数値を閲覧することができるのです。これらをすべて分析すると次第にプログラムが正常に処理しているか知ることができます。

まだ宣言されている関数に到達していないとき、関心のある変数がローカルで宣言されているのではないかと心配する必要はありません。変数の範囲外にいると、それは『不明な識別子』の値を持つのです。それはその変数が宣言されていないことを意味します。それがデバッガのエラーを起こすことはありません。変数の範囲に到達したら、その値とタイプを確認します。

図4 デバッグプロセス-変数値の閲覧

図4 デバッグプロセス変数値の閲覧

これがデバッガの主な機能です。テスターセクションはデバッガでできないことを表示します。


3. プロファイラ

コード プロファイラはデバッガに対する重要な追加部分です。実際、これは最適化で構成されるプログラムデバッグの最終段階です。

プロファイラは『プロファイルの開始』ボタンをクリックすると MetaEditor 5 メニューから呼ばれます。デバッガが行う段階的プログラム解説の代わりにプロファイラがプログラムを実行します。プログラムがインディイケータであったり Expert Advisorであると、プロファイラはプログラムがアンロードされるまで動作します。アンロードはチャートからインディケータや Expert Advisor を除外すること、また『プロファイルの中止』をクリックすることで行われます。

プロファイルにより重要な統計を取得します。各関数が呼ばれた回数、その関数の実行にかかった時間です。おそらくみなさんはパーセント表示の統計データに少し混乱されるでしょう。統計はネスト化された関数に配慮しないことを理解する必要があります。よって全パーセント値の合計は大幅に 100%を越えます。

しかしその事実にもかかわらず、プロファイラはまだプログラム最適化にとって力強いツールであり、それによりどの関数がすぴいーどに対して最適化される必要があり、どこで資金を節約することができるかユーザーが考えることができるのです。

図5 プロファイラ処理結果

図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 ストラレジーテスタを使ったデバッグ


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); }

これで簡単にデバッグ完了時コードを消去することができます。

デバッグで使用される変数に対しても同様に行います。そのためグローバルに宣言される変数および関数を使うようにします。そうするとアプリケーションの奥深くに埋もれたコンストラクションを探す必要がなくなります。


おわりに

デバッグはプログラム動作の重要な一部です。プログラムデバッグができないにとはプログラマーとは名乗れません。ただ主要なデバッグはつねに頭の中で行われるものです。本稿はデバッグに使用される方法をいくつかお伝えするに過ぎません。とはいうものの、アプリケーション処理原理を理解していないとこういった方法は何の役にも立ちません。

みなさんのデバッグが成功しますように!