English Русский 中文 Español Deutsch Português
MQL5でのエラー処理とロギング

MQL5でのエラー処理とロギング

MetaTrader 5 | 1 8月 2016, 14:20
2 286 0
Sergey Eremin
Sergey Eremin

概論

多くのプログラムは動作プロセスでエラーを起こすことがあります。そういったエラーの適切な処理というのは、質が良く安定したソフトウェアの機能の重要な局面の一つです。この記事では、エラー処理の基本的な方法の検証、その使用例の提示、またMQL5でのロギングの問題に触れていきます。

エラーの処理は、非常に難しく複雑なテーマです。多数のエラー処理の方法が存在し、それらのそれぞれがそれぞれの長所と短所を持っています。これらの方法のうちの多くを一緒に使用することができますが、万能な方法というのは存在しません。それぞれの問題ごとに適切なアプローチを選択しなければいけません。


エラー処理の主な方法

プログラムが動作時にいくつかのエラーを起こす場合は、多くの場合、正常に機能する為に何かしらのアクション(またはいくつかのアクション)を実行する必要があります。これらのアクションの例として次のものを挙げることができます。

プログラムの実行の停止。いくつかのエラーが起きた場合、最も適切な動作はプログラムの実行を停止することです。通常、これは重大なエラーであり、これらのエラーが発生した後はプログラムを実行することは不可能なので、意味がない上に単純に危険です。MQL5では実行時間の一連のエラーに対し、専用の動作の割込みメカニズムを提供しています。例えば、プログラムに配列の境界外やゼロ除算が起こった場合に動作を停止します。他の場合の動作停止については、プログラマー自身が解決をする必要があります。例として、エキスパートアドバイザにExpertRemove()関数を適用することができます。

ExpertRemove()を使った、エキスパートアドバイザの動作停止例

void OnTick()
  {
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      Alert("fail");
      ExpertRemove();
      return;
     }
  }


不正な値を正常な範囲のものに変換。多くの場合、いくつかの値は指定された範囲内に収める必要があります。しかし、いくつかのケースでは、この範囲から飛び出す値が出てくることがあります。その場合、値を許容範囲の値に強制的に戻すことができます。例として、保有ポジションの数量計算を引用します。結果数量が許容値の最小/最大値の外にある場合、この値を強制的にこれらの範囲内に戻します。

不正な値を正常な範囲に戻す例

double VolumeCalculation()
  {
   double result=...;

   result=MathMax(result,SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN));
   result=MathMin(result,SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MAX));

   return result;
  }

何かしらの理由で数量が最大範囲を超え、証拠金がこのような負荷を維持できる状態でない場合、賢明な対処は全てを記録し、プログラムの動作実行を中断することです。このエラーはよく口座にとってとても危険なものになるものです。


エラー値を戻す。この場合、エラーの発生時にある関数/メソッドは、エラーについての通知をする事前に判別した値を戻す必要があります。例えば、私達の方法または関数がstringを返さなければいけない場合、エラー時にはNULLを返すことがあります。

エラー値を返す例

#define SOME_STR_FUNCTION_FAIL_RESULT (NULL)

string SomeStrFunction()
  {
   string result="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      return SOME_STR_FUNCTION_FAIL_RESULT;
     }
   return result;
  }

void OnTick()
  {
   string someStr=SomeStrFunction();

   if(someStr==SOME_STR_FUNCTION_FAIL_RESULT)
     {
      Print("fail");
      return;
     }
  }

しかしながら、このようなアプローチはプログラマーのミスにつながる可能性があります。この動作が文書化されていない場合、もしくは、プログラマーがドキュメントまたはコードの実装を見ない場合、エラー値を知ることができません。また、関数/メソッドが通常モードで、原則としてエラー値を含む任意の値を返すことがあるという問題が起こることがあります。


特別なグローバル変数の実行結果の割当て。多くの場合、このアプローチは何の値も返さない関数/メソッドの為に適用されます。このアイディアは、いくつかのグローバル変数にこの方法や変数の実行結果が入れられ、それから呼び出したコードでこの変数値がチェックされるというものです。MQL5にはこの為のデフォルトの機能があります(SetUserError())。

SetUserError()を使ったエラーコード割当の例

#define SOME_STR_FUNCTION_FAIL_CODE (123)

string SomeStrFunction()
  {
   string result="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      SetUserError(SOME_STR_FUNCTION_FAIL_CODE);
      return "";
     }
   return result;
  }

void OnTick()
  {
   ResetLastError();
   string someStr=SomeStrFunction();

   if(GetLastError()==ERR_USER_ERROR_FIRST+SOME_STR_FUNCTION_FAIL_CODE)
     {
      Print("fail");
      return;
     }
  }

この場合、プログラマーも可能性のあるエラーを思い当てることができますが、このアプローチはエラーの事実だけでなく、その具体的なコードも知らせることができます。これは特にエラーの原因が複数ある場合に重要です。


実行結果はbool、結果の値は参照によって引き渡される変数として返す。 このアプローチは、プログラマーのミスの可能性を低くする為、前述の2つよりも幾分か良いものです。ここでは、メソッド/関数が正常に動作しない可能性があることに気付かない方が難しいです。

bool値として関数の動作結果を返す例

bool SomeStrFunction(string &value)
  {
   string resultValue="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      value="";
      return false;
     }
   value=resultValue;
   return true;
  }

void OnTick()
  {
   string someStr="";
   bool result=SomeStrFunction(someStr);

   if(!result)
     {
      Print("fail");
      return;
     }
  }

もし複数の異なるエラーが起こる可能性があり、私達は内容を把握する必要がある場合、この方法を前述のものと組み合わせることができます。falseを返し、グローバル変数にはエラーコードを割りあてます。

bool値としての関数の動作結果の返しとSetUserError()を使用したエラーコードの割当の例

#define SOME_STR_FUNCTION_FAIL_CODE_1 (123)
#define SOME_STR_FUNCTION_FAIL_CODE_2 (124)

bool SomeStrFunction(string &value)
  {
   string resultValue="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      value="";

      SetUserError(SOME_STR_FUNCTION_FAIL_CODE_1);

      return false;
     }

   bool resultOfSomeOperation2=SomeOperation2();

   if(!resultOfSomeOperation2)
     {
      value="";
      SetUserError(SOME_STR_FUNCTION_FAIL_CODE_2);
      return false;
     }
   value=resultValue;
   return true;
  }

void OnTick()
  {
   string someStr="";
   bool result=SomeStrFunction(someStr);

   if(!result)
     {
      Print("fail, code = "+(string)(GetLastError()-ERR_USER_ERROR_FIRST));
      return;
     }
  }

しかし、この方法は理解や(コードの読み取り時)、今後のサポートが難しいものです。


列挙(enum)からの値として結果を返し、結果の値は(もし考慮にいれている場合)参照で引き渡される変数として返す この方法では、可能性のあるエラータイプが複数で、グローバル変数を使用しない場合に、障害が起きた際に正しいエラータイプを返すことができます。一つの値だけが正常な実行に対応し、残りのものはエラーとなります。

列挙(enum)からの値として関数の動作結果を返す例

enum ENUM_SOME_STR_FUNCTION_RESULT
  {
   SOME_STR_FUNCTION_SUCCES,
   SOME_STR_FUNCTION_FAIL_CODE_1,
   SOME_STR_FUNCTION_FAIL_CODE_2
  };

ENUM_SOME_STR_FUNCTION_RESULT SomeStrFunction(string &value)
  {
   string resultValue="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      value="";
      return SOME_STR_FUNCTION_FAIL_CODE_1;
     }

   bool resultOfSomeOperation2=SomeOperation2();

   if(!resultOfSomeOperation2)
     {
      value="";
      return SOME_STR_FUNCTION_FAIL_CODE_2;
     }

   value=resultValue;
   return SOME_STR_FUNCTION_SUCCES;
  }

void OnTick()
  {
   string someStr="";

   ENUM_SOME_STR_FUNCTION_RESULT result=SomeStrFunction(someStr);

   if(result!=SOME_STR_FUNCTION_SUCCES)
     {
      Print("fail, error = "+EnumToString(result));
      return;
     }
  }

グローバル変数は下手にまたは不注意に取り扱った場合に重大な問題を引き起こすことがあるので、グローバル変数を除くことはこの方法にとってとても重要な点です。


ブール変数または列挙値(enum)、そして結果の値から構成される構造体のインスタンスとして結果を返すこの方法は参照による変数の引き渡しをしない前述の方法に関するものです。また、将来的に実行結果のリストを拡張することができる為、enumの使用をすることが好ましいです。

列挙(enum)や結果の値から構成される構造体のインスタンスとして関数の動作結果を返す例

enum ENUM_SOME_STR_FUNCTION_RESULT
  {
   SOME_STR_FUNCTION_SUCCES,
   SOME_STR_FUNCTION_FAIL_CODE_1,
   SOME_STR_FUNCTION_FAIL_CODE_2
  };

struct SomeStrFunctionResult
  {
   ENUM_SOME_STR_FUNCTION_RESULT code;
   char              value[255];
  };

SomeStrFunctionResult SomeStrFunction()
  {
   SomeStrFunctionResult result;

   string resultValue="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      result.code=SOME_STR_FUNCTION_FAIL_CODE_1;
      return result;
     }

   bool resultOfSomeOperation2=SomeOperation2();

   if(!resultOfSomeOperation2)
     {
      result.code=SOME_STR_FUNCTION_FAIL_CODE_2;
      return result;
     }

   result.code=SOME_STR_FUNCTION_SUCCES;
   StringToCharArray(resultValue,result.value);
   return result;
  }

void OnTick()
  {
   SomeStrFunctionResult result=SomeStrFunction();

   if(result.code!=SOME_STR_FUNCTION_SUCCES)
     {
      Print("fail, error = "+EnumToString(result.code));
      return;
     }
   string someStr=CharArrayToString(result.value);
  }


操作の複数回実行を試みる。多くの場合、失敗したと考える前に、操作を数回実行した方が良いです。例えば、他のプロセッサで使用されている為、ファイルを読み込めない場合に、インターバルを増やして数回操作を試みてみましょう。高い確率で他のプロセスがファイルを解放して、私達のメソッド/関数がそのファイルにアクセスできるようになります。

数回ファイルを開く試みをする実行例

string fileName="test.txt";
int fileHandle=INVALID_HANDLE;

for(int iTry=0; iTry<=10; iTry++)
  {
   fileHandle=FileOpen(fileName,FILE_TXT|FILE_READ|FILE_WRITE);

   if(fileHandle!=INVALID_HANDLE)
     {
      break;
     }
   Sleep(iTry*200);
  }

注意:この例はアプローチの要点を反映しており、実際の使用では発生するエラーを分析する必要があります。例として、エラー5002(無効なファイル名)または5003(長すぎるファイル名)が発生した場合、後続の試行は意味をなしません。また、このアプローチはどんな実行の遅延も望ましくないシステムでは適用するべきではないという点にご留意ください。


明示的にユーザーへ通知する。いくつかのエラーについては、ユーザーに明示的な方法で(ポップアップウィンドウやチャート上のラベルなど)通知する必要があります。多くの場合、明示的な通知はプログラム実行の一時停止または停止と組み合わせて使用することができます。例えば、口座の残高が不足している、またはユーザーが明らかに間違っている入力パラメータを入れた場合、この事についてユーザーに知らせる必要があるのです。

不正な入力パラメータについてのユーザーへの通知例

input uint MAFastPeriod = 10;
input uint MASlowPeriod = 200;

int OnInit()
  {
//---
   if(MAFastPeriod>=MASlowPeriod)
     {
      Alert("短期移動平均線の期間は中期移動平均線の期間よりも小さくする必要があります!");
      return INIT_PARAMETERS_INCORRECT;
     }
//---
   return(INIT_SUCCEEDED);
  }

勿論、エラー処理の方法は他にもあり、このリストはより一般的なものを表示しています。


エラー処理の一般的な推奨事項

エラー処理の適切なレベルを選択してください。エラー処理のレベルによって、様々なプログラムへ全く異なる要件が提示されます。第三者に引き渡されることがなく、小さなアイディアのチェック時に数回だけ使用される小さいスクリプトが開発される場合、どんなエラー処理もせずに済みます。反対に、数十万人の潜在的ユーザーを持つプロジェクトの場合、適切なアプローチは全ての考え得るエラーの処理です。それぞれの特定のケースで必要とされるエラー処理のレベルを常に理解することに努めてください。

ユーザー参加の適切なレベルを選択してください。いくつかのエラーにはユーザーの明示的な参加が必要不可欠で、他の場合にはそうではない、つまりプログラムはユーザーへ通知することなく動作を自力で続けることができます。私達は中庸を探す必要があり、ユーザーをエラーについてのメッセージで煩わせることは望ましくありませんが、プログラムが危機的状況にある中で黙っているわけにもいきません。良い解決策として以下のアプローチがあります。重大なエラーやユーザーの参加が必要なエラーについてのみユーザーに明示的に通知し、他の全てのものはログファイルに保持します。

それらを返す全ての関数/メソッドの実行結果のチェックをしましょう。何らかの関数/メソッドがエラーを示す値を返す場合、それらの全てをチェックした方が良いです。プログラムの質を向上させるこのチャンスを見逃さないようにしましょう。

必要に応じて特定の操作を実行する前に条件のチェックをしましょう。例えば、取引の開始を試みる前に以下をチェックした方が良いです。

  1. ターミナル側でエキスパートアドバイザによる取引は許可されているか: TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)。
  2. その口座はエキスパートアドバイザによる取引が許可されているか: AccountInfoInteger(ACCOUNT_TRADE_EXPERT)。
  3. 取引サーバーへの接続があるか: TerminalInfoInteger(TERMINAL_CONNECTED)。
  4. 取引操作のパラメータは正確なものか: OrderCheck()。

プログラムの様々な部分の実行速度を監視してください。十分によくある取引サーバへの要求速度が考慮されていないコードの例として、トレーリングストップがあります。通常、このような関数の呼び出しは各ティックで実装されます。一定方向への継続的な動きがある、もしくは取引の変更時に何かしらのエラーが発生する場合、この関数は取引の変更リクエストをほぼティック毎に1度(もしくは複数の取引の為の複数の要求)を送ります。

あまり頻繁に相場が受信されない場合には、このような関数は問題を引き起こすことはありません。しかし、そうでない場合には、深刻な問題が発生する可能性があり、頻繁すぎる取引変更のリクエストは、ブローカーによる特定の口座の自動売買適用の無効化やサポートサービスとの不快な会話に繋がる可能性があります。最も簡単な方法は、取引変更リクエスト実行の試行頻度の制限です。前回のリクエスト時間を記憶し、XX秒以下しか時間が経っていない場合、再度その実行を試みないというものです。

トレーリングストップロスを30秒に一回以下にする関数の実行例

const int TRAILING_STOP_LOSS_SECONDS_INTERVAL=30;

void TrailingStopLoss()
  {
   static datetime prevModificationTime=0;

   if((int)TimeCurrent() -(int)prevModificationTime<=TRAILING_STOP_LOSS_SECONDS_INTERVAL)
     {
      return;
     }

//--- Stop Lossの変更
     {
      ...
      ...
      ...
      prevModificationTime=TimeCurrent();
     }
  }

短時間に多すぎる指値注文の発注を試みた場合にこのような問題は起こることがあり、このケースも筆者に経験があります。


安定と正確の適切な組み合わせを目指しましょう。一般的に、プログラムを作成していると、コードの安定性と正確性の間の妥協点を探すことになります。安定性とは、プログラムはエラー発生時にも動作を続け、少し正確ではない結果をもたらしてもそれを許容するということです。正確性とは、不正確な結果が返ってくる、または間違った動作の実行を認めず、これらは正確である必要があり、つまり不正確な結果や間違ったことをするよりも、プログラムの実行を停止を良しとします。

例として、インディケータが何かを計算することができない場合、インディケータは完全に動作を停止するよりも、シグナルが欠如している方がましです。反対に、トレードロボットの場合、多すぎる数量の取引を開始するよりも、自分の動作を終了した方が良いです。また、自分の動作を停止する前に、ユーザーが問題について知ることができ、それにすぐに対応することができるように、ロボットはプッシュ通知を使ってユーザーに通知することができます。


エラーについての有益な情報を表示しましょう。エラーに関するメッセージを十分に有益なものにするように心がけましょう。もしプログラムが『取引を発注することができませんでした』とだけエラーを出したとしたら、これは良くありません。もしメッセージが『取引を発注することができませんでした:保有するポジションの不正な数量(0.9999)』というようになっていたら、こちらの方が遥かにいいです。エラーメッセージをポップアップウィンドウかログファイルに出力するかは重要ではなく、プログラムはどんな場合にもユーザーやプログラマー(特にログファイルの分析時)がエラーの原因を理解し、可能な場合にはそれを修正することができるのに十分なものである必要があります。また、ユーザーを情報で煩わせてもいけません。ユーザーの為のポップアップウィンドウには、ユーザーが得られるものが少ないので、エラーコードを表示しなくても良いです。


MQL5ツールでのロギング

通常、ログファイルは様々なバグやエラーの原因の探索を楽にする目的でプログラマー自身の為、また任意の時点でのシステムの状態の評価をする為にプログラムによって作成されます。また、ロギングはソフトウェアのプロファイリングにも適用されます。


ロギングレベル

ログファイルに受信されるメッセージは、多くの場合異なる重要性を持ち、様々な注意を必要とします。異なる重要性のメッセージを互いに分離し、また出力するメッセージの重要度の設定をできるようにする為に、ロギングレベルが適用されます。原則として、いくつかのロギングレベルが実装されます。

  • Debug:デバッグメッセージ。このロギングレベルは開発やデバッグ、試運転の段階に含まれています。
  • Info:インフォメーションメッセージ。このメッセージは、システムの様々なアクションについての情報を持っています(例:動作の開始/終了、取引の発注/決済など)。通常、このレベルのメッセージは何のリアクションも必要としませんが、特定の動作エラーをもたらすイベントの連鎖を学ぶ時に役立ちます。
  • Warning:警告メッセージ。このイベントレベルは、ユーザーの介入を求めないエラーをもたらす状況の詳細を含みます。例として、計算される取引数量が最小未満で、プログラムが自動的にそれを修正した場合、これについて«Warning»レベルのログファイルに通知することができます。
  • Error:明示的な介入を必要とするエラーについてのメッセージ。このロギングレベルは、通常何かしらのファイルや取引の発注または変更などうぃ保存できないエラーが発生した時に適用されます。言い換えれば、ここにはプログラムが自分で克服することができない、(ユーザーやプログラマーの)明示的な介入を必要とするエラーが入ります。
  • Fatal: 将来的にプログラムが動作しなくなる重大なエラーについてのメッセージ。これらのメッセージは、より迅速な対応を必要とし、多くの場合このレベルの為にEメールやSMSなどを介したプログラマーやユーザーへの通知が想定されています。以下に示すように、MQL5では通知の為にプッシュ通知を使用することができます。


ログファイルの処理

MQL5ツールによるログファイルの処理の最も簡単な方法は、PrintまたはPrintFormatの標準関数の適用です。結果として、全てのメッセージはエキスパートアドバイザやインディケータ、ターミナルのスクリプトの共通のログに送信されます。

Print()関数を使用したエキスパートアドバイザの共通ログへのメッセージの表示例

double VolumeCalculation()
  {
   double result=...;
   if(result<SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN))
     {
      Print("取引量(",DoubleToString(result,2)")が許容未満だった為"+DoubleToString(SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN),2))まで修正されました。
      result=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN);
     }
   return result;
  }

このアプローチはいくつかの短所を持っています。

  1. 複数のプログラムからのメッセージは、共通の場所に入れられ、その分析を困難にする可能性があります。
  2. ログファイルは、利便性の観点から、ユーザーによって不意にまたは故意に削除される可能性があります。
  3. ロギングレベルを設定し実装するのは十分に難しいです。
  4. ログメッセージの出力を他のソース(外部ファイル、データベース、メールなど)へリダイレクトすることはできません。
  5. ログファイルの強制的なローテーション(日付や時間または特定のサイズへの到達でのファイルの交換)の実装をすることはできません。

このアプローチの長所。

  1. 特に開発をする必要がなく、同じ関数を使用すれば十分です。
  2. 多くの場合、ログファイルはターミナルで直接見ることができ、別個に探す必要はありません。

独自のロギングメカニズムの実装は、Print()やPrintFormat()の使用の全ての欠点を補うことができますが、コードを繰り返し使用する必要がある場合、新しいプロジェクトやロギングメカニズムへの転送を必要とします(またはコード内でのその使用の拒否)。

MQL5でのロギングメカニズムの実装例として、次の方法を見てみましょう。

MQL5での独自のロギングメカニズムの実装例

//+------------------------------------------------------------------+
//|                                                       logger.mqh |
//|                                   Copyright 2015, Sergey Eryomin |
//|                                             http://www.ensed.org |
//+------------------------------------------------------------------+
#property copyright "Sergey Eryomin"
#property link      "http://www.ensed.org"

#define LOG(level, message) CLogger::Add(level, message+" ("+__FILE__+"; "+__FUNCSIG__+"; Line: "+(string)__LINE__+")")
//--- 『新規の1MBごとに新規ログファイル』モードのファイルの最大数
#define MAX_LOG_FILE_COUNTER (100000) 
//--- メガバイトあたりのバイト数
#define BYTES_IN_MEGABYTE (1048576)
//--- ログファイル名の最大長
#define MAX_LOG_FILE_NAME_LENGTH (255)
//--- ロギングレベル
enum ENUM_LOG_LEVEL
  {
   LOG_LEVEL_DEBUG,
   LOG_LEVEL_INFO,
   LOG_LEVEL_WARNING,
   LOG_LEVEL_ERROR,
   LOG_LEVEL_FATAL
  };
//--- ロギングメソッド
enum ENUM_LOGGING_METHOD
  {
   LOGGING_OUTPUT_METHOD_EXTERN_FILE,// 外部ファイル
   LOGGING_OUTPUT_METHOD_PRINT // Print関数
  };
//--- 通知方法
enum ENUM_NOTIFICATION_METHOD
  {
   NOTIFICATION_METHOD_NONE,// 無効
   NOTIFICATION_METHOD_ALERT,// Alert関数
   NOTIFICATION_METHOD_MAIL, // SendMail関数
   NOTIFICATION_METHOD_PUSH // SendNotification関数
  };
//--- ログファイルの制限タイプ
enum ENUM_LOG_FILE_LIMIT_TYPE
  {
   LOG_FILE_LIMIT_TYPE_ONE_DAY,// 新しい日ごとに新しいログファイル
   LOG_FILE_LIMIT_TYPE_ONE_MEGABYTE // 新しい1MBごとに新しいログファイル
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CLogger
  {
public:
   //--- メッセージをログに追加
   //--- 備考:
   //--- 外部ファイルへの出力モードがオンになっているのに、出力ができない場合、
   //--- メッセージの出力はPrint()関数を介して実行されます
   static void Add(const ENUM_LOG_LEVEL level,const string message)
     {
      if(level>=m_logLevel)
        {
         Write(level,message);
        }

      if(level>=m_notifyLevel)
        {
         Notify(level,message);
        }
     }
   //--- ロギングレベルの設定
   static void SetLevels(const ENUM_LOG_LEVEL logLevel,const ENUM_LOG_LEVEL notifyLevel)
     {
      m_logLevel=logLevel;
      //--- 通知を介するメッセージの出力レベルはログファイルへの記録レベル以下であってはいけません
      m_notifyLevel=fmax(notifyLevel,m_logLevel);
     }
   //--- ロギング方法の設定
   static void SetLoggingMethod(const ENUM_LOGGING_METHOD loggingMethod)
     {
      m_loggingMethod=loggingMethod;
     }
   //--- 通知方法の設定
   static void SetNotificationMethod(const ENUM_NOTIFICATION_METHOD notificationMethod)
     {
      m_notificationMethod=notificationMethod;
     }
   //--- ログファイル名の設定
   static void SetLogFileName(const string logFileName)
     {
      m_logFileName=logFileName;
     }
   //--- ログファイルへの制限タイプの設定
   static void SetLogFileLimitType(const ENUM_LOG_FILE_LIMIT_TYPE logFileLimitType)
     {
      m_logFileLimitType=logFileLimitType;
     }

private:
   //--- ロギングレベル以上のメッセージはログファイル/ジャーナルへ保存されます
   static ENUM_LOG_LEVEL m_logLevel;
   //--- ロギングレベル以上のメッセージは通知として表示されます
   static ENUM_LOG_LEVEL m_notifyLevel;
   //--- ロギング方法
   static ENUM_LOGGING_METHOD m_loggingMethod;
   //--- 通知方法
   static ENUM_NOTIFICATION_METHOD m_notificationMethod;
   //--- ログファイル名
   static string     m_logFileName;
   //--- ログファイルへの制限タイプ
   static ENUM_LOG_FILE_LIMIT_TYPE m_logFileLimitType;
   //--- ログの為のファイル名取得結果           
   struct GettingFileLogNameResult
     {
                        GettingFileLogNameResult(void)
        {
         succes=false;
         ArrayInitialize(value,0);
        }
      bool              succes;
      char              value[MAX_LOG_FILE_NAME_LENGTH];
     };
   //--- 既存のログファイルサイズのチェック結果
   enum ENUM_LOG_FILE_SIZE_CHECKING_RESULT
     {
      IS_LOG_FILE_LESS_ONE_MEGABYTE,
      IS_LOG_FILE_NOT_LESS_ONE_MEGABYTE,
      LOG_FILE_SIZE_CHECKING_ERROR
     };
   //--- ログファイルへの記録
   static void Write(const ENUM_LOG_LEVEL level,const string message)
     {
      switch(m_loggingMethod)
        {
         case LOGGING_OUTPUT_METHOD_EXTERN_FILE:
           {
            GettingFileLogNameResult getLogFileNameResult=GetLogFileName();
            //---
            if(getLogFileNameResult.succes)
              {
               string fileName=CharArrayToString(getLogFileNameResult.value);
               //---
               if(WriteToFile(fileName,GetDebugLevelStr(level)+": "+message))
                 {
                  break;
                 }
              }
           }
         case LOGGING_OUTPUT_METHOD_PRINT:
            default:
              {
               Print(GetDebugLevelStr(level)+": "+message);
               break;
              }
        }
     }
   //--- 通知の実行
   static void Notify(const ENUM_LOG_LEVEL level,const string message)
     {
      if(m_notificationMethod==NOTIFICATION_METHOD_NONE)
        {
         return;
        }
      string fullMessage=TimeToString(TimeLocal(),TIME_DATE|TIME_SECONDS)+", "+Symbol()+" ("+GetPeriodStr()+"), "+message;
      //---
      switch(m_notificationMethod)
        {
         case NOTIFICATION_METHOD_MAIL:
           {
            if(TerminalInfoInteger(TERMINAL_EMAIL_ENABLED))
              {
               if(SendMail("Logger",fullMessage))
                 {
                  return;
                 }
              }
           }
         case NOTIFICATION_METHOD_PUSH:
           {
            if(TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED))
              {
               if(SendNotification(fullMessage))
                 {
                  return;
                 }
              }
           }
        }
      //---
      Alert(GetDebugLevelStr(level)+": "+message);
     }
   //--- 記録の為のログファイル名の取得
   static GettingFileLogNameResult GetLogFileName()
     {
      if(m_logFileName=="")
        {
         InitializeDefaultLogFileName();
        }
      //---
      switch(m_logFileLimitType)
        {
         case LOG_FILE_LIMIT_TYPE_ONE_DAY:
           {
            return GetLogFileNameOnOneDayLimit();
           }
         case LOG_FILE_LIMIT_TYPE_ONE_MEGABYTE:
           {
            return GetLogFileNameOnOneMegabyteLimit();
           }
         default:
           {
            GettingFileLogNameResult failResult;
            failResult.succes=false;
            return failResult;
           }
        }
     }
   //--- 『新しい日ごとに新しいログファイル』の制限時のログファイル名の取得
   static GettingFileLogNameResult GetLogFileNameOnOneDayLimit()
     {
      GettingFileLogNameResult result;
      string fileName=m_logFileName+"_"+Symbol()+"_"+GetPeriodStr()+"_"+TimeToString(TimeLocal(),TIME_DATE);
      StringReplace(fileName,".","_");
      fileName=fileName+".log";
      result.succes=(StringToCharArray(fileName,result.value)==StringLen(fileName)+1);
      return result;
     }
   //--- 『新しい1MBごとに新しいログファイル』の制限時のログファイル名の取得
   static GettingFileLogNameResult GetLogFileNameOnOneMegabyteLimit()
     {
      GettingFileLogNameResult result;
      //---
      for(int i=0; i<MAX_LOG_FILE_COUNTER; i++)
        {
         ResetLastError();
         string fileNameToCheck=m_logFileName+"_"+Symbol()+"_"+GetPeriodStr()+"_"+(string)i;
         StringReplace(fileNameToCheck,".","_");
         fileNameToCheck=fileNameToCheck+".log";
         ResetLastError();
         bool isExists=FileIsExist(fileNameToCheck);
         //---
         if(!isExists)
           {
            if(GetLastError()==5018)
              {
               continue;
              }
           }
         //---
         if(!isExists)
           {
            result.succes=(StringToCharArray(fileNameToCheck,result.value)==StringLen(fileNameToCheck)+1);

            break;
           }
         else
           {
            ENUM_LOG_FILE_SIZE_CHECKING_RESULT checkLogFileSize=CheckLogFileSize(fileNameToCheck);

            if(checkLogFileSize==IS_LOG_FILE_LESS_ONE_MEGABYTE)
              {
               result.succes=(StringToCharArray(fileNameToCheck,result.value)==StringLen(fileNameToCheck)+1);

               break;
              }
            else if(checkLogFileSize!=IS_LOG_FILE_NOT_LESS_ONE_MEGABYTE)
              {
               break;
              }
           }
        }
      //---
      return result;
     }
   //---
   static ENUM_LOG_FILE_SIZE_CHECKING_RESULT CheckLogFileSize(const string fileNameToCheck)
     {
      int fileHandle=FileOpen(fileNameToCheck,FILE_TXT|FILE_READ);
      //---
      if(fileHandle==INVALID_HANDLE)
        {
         return LOG_FILE_SIZE_CHECKING_ERROR;
        }
      //---
      ResetLastError();
      ulong fileSize=FileSize(fileHandle);
      FileClose(fileHandle);
      //---
      if(GetLastError()!=0)
        {
         return LOG_FILE_SIZE_CHECKING_ERROR;
        }
      //---
      if(fileSize<BYTES_IN_MEGABYTE)
        {
         return IS_LOG_FILE_LESS_ONE_MEGABYTE;
        }
      else
        {
         return IS_LOG_FILE_NOT_LESS_ONE_MEGABYTE;
        }
     }
   //--- デフォルトのログファイル名を初期化
   static void InitializeDefaultLogFileName()
     {
      m_logFileName=MQLInfoString(MQL_PROGRAM_NAME);
      //---
#ifdef __MQL4__
      StringReplace(m_logFileName,".ex4","");
#endif

#ifdef __MQL5__
      StringReplace(m_logFileName,".ex5","");
#endif
     }
   //--- ファイルにメッセージを記録
   static bool WriteToFile(const string fileName,
                           const string text)
     {
      ResetLastError();
      string fullText=TimeToString(TimeLocal(),TIME_DATE|TIME_SECONDS)+", "+Symbol()+" ("+GetPeriodStr()+"), "+text;
      int fileHandle=FileOpen(fileName,FILE_TXT|FILE_READ|FILE_WRITE);
      bool result=true;
      //---
      if(fileHandle!=INVALID_HANDLE)
        {
         //--- ファイル末尾にファイルポインタを配置する            
         if(!FileSeek(fileHandle,0,SEEK_END))
           {
            Print("Logger: FileSeek() is failed, error #",GetLastError(),"; text = \"",fullText,"\"; fileName = \"",fileName,"\"");
            result=false;
           }
         //--- ファイルにテキストを書き込む
         if(result)
           {
            if(FileWrite(fileHandle,fullText)==0)
              {
               Print("Logger: FileWrite() is failed, error #",GetLastError(),"; text = \"",fullText,"\"; fileName = \"",fileName,"\"");
               result=false;
              }
           }
         //---
         FileClose(fileHandle);
        }
      else
        {
         Print("Logger: FileOpen() is failed, error #",GetLastError(),"; text = \"",fullText,"\"; fileName = \"",fileName,"\"");
         result=false;
        }
      //---
      return result;
     }
   //--- 文字列として現在の期間を取得
   static string GetPeriodStr()
     {
      ResetLastError();
      string periodStr=EnumToString(Period());
      if(GetLastError()!=0)
        {
         periodStr=(string)Period();
        }
      StringReplace(periodStr,"PERIOD_","");
      //---
      return periodStr;
     }
   //---
   static string GetDebugLevelStr(const ENUM_LOG_LEVEL level)
     {
      ResetLastError();
      string levelStr=EnumToString(level);
      //---
      if(GetLastError()!=0)
        {
         levelStr=(string)level;
        }
      StringReplace(levelStr,"LOG_LEVEL_","");
      //---
      return levelStr;
     }
  };
ENUM_LOG_LEVEL CLogger::m_logLevel=LOG_LEVEL_INFO;
ENUM_LOG_LEVEL CLogger::m_notifyLevel=LOG_LEVEL_FATAL;
ENUM_LOGGING_METHOD CLogger::m_loggingMethod=LOGGING_OUTPUT_METHOD_EXTERN_FILE;
ENUM_NOTIFICATION_METHOD CLogger::m_notificationMethod=NOTIFICATION_METHOD_ALERT;
string CLogger::m_logFileName="";
ENUM_LOG_FILE_LIMIT_TYPE CLogger::m_logFileLimitType=LOG_FILE_LIMIT_TYPE_ONE_DAY;
//+------------------------------------------------------------------+

このコードは、別個に含まれるファイルに配置することができ(例:Logger.mqh)、<ターミナルのデータフォルダ>/MQL5/Include(このファイルは記事に添付されています)に保存することができます。この後のCLoggerクラスを使った作業は以下のようになります。

独自のロギングメカニズムの使用例

#include <Logger.mqh>

//--- ロガーの初期化の実行
void InitLogger()
  {
//--- ロギングレベルの設定: 
//--- ログファイルへのメッセージの記録の為のDEBUGレベル
//--- 通知の為のERRORレベル
   CLogger::SetLevels(LOG_LEVEL_DEBUG,LOG_LEVEL_ERROR);
//--- プッシュ通知としての通知タイプの設定
   CLogger::SetNotificationMethod(NOTIFICATION_METHOD_PUSH);
//--- 外部ファイルへの記録としてのロギング方法の設定
   CLogger::SetLoggingMethod(LOGGING_OUTPUT_METHOD_EXTERN_FILE);
//--- ログファイル名の設定
   CLogger::SetLogFileName("my_log");
//---『新しい日ごとに新しいファイル』としてのログファイルへの制限タイプの設定
   CLogger::SetLogFileLimitType(LOG_FILE_LIMIT_TYPE_ONE_DAY);
  }

int OnInit()
  {
//---
   InitLogger();
//---
   CLogger::Add(LOG_LEVEL_INFO,"");
   CLogger::Add(LOG_LEVEL_INFO,"---------- OnInit() -----------");
   LOG(LOG_LEVEL_DEBUG,"Example of debug message");
   LOG(LOG_LEVEL_INFO,"Example of info message");
   LOG(LOG_LEVEL_WARNING,"Example of warning message");
   LOG(LOG_LEVEL_ERROR,"Example of error message");
   LOG(LOG_LEVEL_FATAL,"Example of fatal message");
//---
   return(INIT_SUCCEEDED);
  }

初めにInitLogger()関数で全ての可能なロガーパラメータが初期化され、それからログファイルへのメッセージの記録が行われます。このコードの動作結果は、<data_folder>/MQL5/Files内の«my_log_USDCAD_D1_2015_09_23.log»という名前のログファイルへ記録されます。

2015.09.23 09:02:10, USDCAD (D1), INFO: 
2015.09.23 09:02:10, USDCAD (D1), INFO: ---------- OnInit() -----------
2015.09.23 09:02:10, USDCAD (D1), DEBUG: Example of debug message (LoggerTest.mq5; int OnInit(); Line: 36)
2015.09.23 09:02:10, USDCAD (D1), INFO: Example of info message (LoggerTest.mq5; int OnInit(); Line: 38)
2015.09.23 09:02:10, USDCAD (D1), WARNING: Example of warning message (LoggerTest.mq5; int OnInit(); Line: 40)
2015.09.23 09:02:10, USDCAD (D1), ERROR: Example of error message (LoggerTest.mq5; int OnInit(); Line: 42)
2015.09.23 09:02:10, USDCAD (D1), FATAL: Example of fatal message (LoggerTest.mq5; int OnInit(); Line: 44)

また、プッシュ通知を介して、ERRORとFATALレベルのメッセージが送信されます。

ログファイルへの記録の為のメッセージレベルをWarningに設定する場合(CLogger::SetLevels(LOG_LEVEL_WARNING,LOG_LEVEL_ERROR))、表示は次のようになります。

2015.09.23 09:34:00, USDCAD (D1), WARNING: Example of warning message (LoggerTest.mq5; int OnInit(); Line: 40)
2015.09.23 09:34:00, USDCAD (D1), ERROR: Example of error message (LoggerTest.mq5; int OnInit(); Line: 42)
2015.09.23 09:34:00, USDCAD (D1), FATAL: Example of fatal message (LoggerTest.mq5; int OnInit(); Line: 44)

つまり、WARNINGレベル以下のメッセージの保存は実行されないということです。


CLoggerクラスとLOGマクロスの公開されているメソッド

CLoggerクラスとLOGマクロスの公開されているメソッドを詳細に見ていきましょう。


void SetLevelsメソッド(const ENUM_LOG_LEVEL logLevel, const ENUM_LOG_LEVEL notifyLevel)。 ロギングレベルを設定します。

const ENUM_LOG_LEVEL logLevel — ロギングレベル以上のメッセージはログファイル/ジャーナルに保存されます。デフォルト = LOG_LEVEL_INFO.

const ENUM_LOG_LEVEL notifyLevel — ロギングレベル以上のメッセージは通知として表示されます。デフォルト = LOG_LEVEL_FATAL.

両方に可能な値:

  • LOG_LEVEL_DEBUG,
  • LOG_LEVEL_INFO,
  • LOG_LEVEL_WARNING,
  • LOG_LEVEL_ERROR,
  • LOG_LEVEL_FATAL.


void SetLoggingMethod(const ENUM_LOGGING_METHOD loggingMethod)。ロギングレベルを設定します。

const ENUM_LOGGING_METHOD loggingMethod — ロギング方法。デフォルト = LOGGING_OUTPUT_METHOD_EXTERN_FILE.

可能な値:

  • LOGGING_OUTPUT_METHOD_EXTERN_FILE — 外部ファイル
  • LOGGING_OUTPUT_METHOD_PRINT — Print関数


void SetNotificationMethod(const ENUM_NOTIFICATION_METHOD notificationMethod)。通知方法を設定します。

const ENUM_NOTIFICATION_METHOD notificationMethod — 通知方法。デフォルト = NOTIFICATION_METHOD_ALERT.

可能な値:

  • NOTIFICATION_METHOD_NONE — 無効
  • NOTIFICATION_METHOD_ALERT — Alert関数
  • NOTIFICATION_METHOD_MAIL — SendMail関数
  • NOTIFICATION_METHOD_PUSH — SendNotification関数


void SetLogFileName(const string logFileName)。ログファイル名を設定します。

const string logFileName — ログファイル名。デフォルト値はロガーが適用されるプログラム名になりますInitializeDefaultLogFileName())プライベートメソッドを参照してください)。


void SetLogFileLimitType(const ENUM_LOG_FILE_LIMIT_TYPE logFileLimitType)。ログファイルへの制限タイプを設定します。

const ENUM_LOG_FILE_LIMIT_TYPE logFileLimitType - ログファイルへの制限タイプ。デフォルト値: LOG_FILE_LIMIT_TYPE_ONE_DAY.

可能な値:

  • LOG_FILE_LIMIT_TYPE_ONE_DAY — новый лог-файл на каждый новый день. my_log_USDCAD_D1_2015_09_21.log、my_log_USDCAD_D1_2015_09_22.log、my_log_USDCAD_D1_2015_09_23 .logといった名前のファイルが作成されます。
  • LOG_FILE_LIMIT_TYPE_ONE_MEGABYTE — 新しい1MBごとに新しいログファイル。my_log_USDCAD_D1_0.log、my_log_USDCAD_D1_1.log、my_log_USDCAD_D1_2.logといた名前のファイルが作成されます。前のファイルが1メガバイトに到達したら次のファイルへ移動します。


void Add(const ENUM_LOG_LEVEL level,const string message)。ログにメッセージを追加します。

const ENUM_LOG_LEVEL level — メッセージレベル。可能な値:

  • LOG_LEVEL_DEBUG
  • LOG_LEVEL_INFO
  • LOG_LEVEL_WARNING
  • LOG_LEVEL_ERROR
  • LOG_LEVEL_FATAL

const string message — メッセージテキスト。


Addメソッドの他に、メッセージテキストにファイル名、関数のシグネチャ、文字列番号、どこからログファイルへの記録が行われているかを追加するLOGマクロが実装されています。

#define LOG(level, message) CLogger::Add(level, message+" ("+__FILE__+"; "+__FUNCSIG__+"; Line: "+(string)__LINE__+")")

このマクロはデバッグ時に特に有益です。

このように、例の中で以下のことを可能にするロギングメカニズムを表示しました。

  1. ロギングレベルの設定(DEBUG..FATAL)。
  2. どのレベルのメッセージについてユーザーに通知するかを設定。
  3. Print()介してエキスパートアドバイザのジャーナルか外部ファイルのどこにログを書くかを設定する。
  4. 外部ファイルへの出力に、ファイル名を指定し、ログファイルへの制限(別個の日付のファイルごと、または各ログのメガバイトのファイルごと)の設定。
  5. 通知タイプの設定(Alert()、SendMail()、SendNotify())。

提案されたものは例でしかなく、特定の課題に対しては処理をする必要があります(余分な機能の排除を含む)。例として、外部ファイルや共通ジャーナルへの記録の他に、データベースへの出力のようなログの処理のメソッドを追加することができます。


まとめ

この記事では、エラーとMQL5ツールによるロギングの処理を見ていきました。正しいエラー処理と適切なロギングは、開発したソフトウェアの質を大幅に高め、今後のサポートを簡素化することができます。

MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/2041

添付されたファイル |
logger.mqh (24.94 KB)
loggertest.mq5 (4.67 KB)

この著者による他の記事

エリック・ナイマンの『チャネル』インディケータ エリック・ナイマンの『チャネル』インディケータ
この記事では、エリック・L・ナイマン氏の著書『トレーダーの小百科事典』を元に『チャネル』インディケータの作成について述べていきます。このインディケータは、指定した期間で計算したベアとブルの値に基づき、トレンドの方向を表示します。この記事では、サンプルコードと共にインディケータの計算と構築の原理を説明し、インディケータをベースにエキスパートアドバイザを作成し、外部パラメータの最適化について述べていきます。
ユニバーサルEA:保留注文とサポートヘッジ(その5) ユニバーサルEA:保留注文とサポートヘッジ(その5)
この記事では、CStrategyの取引エンジンのさらなる詳細を扱います。多くの要望により、取引エンジンに保留中のサポート関数を追加しました。また、MT5の最新バージョンでは、ヘッジオプションを使用してアカウントをサポートしています。同じサポートがCStrategyに追加されています。この記事では、有効なヘッジオプションを持つアカウントのCStrategyと同様に、予約注文のアルゴリズムについて説明します。
ユニバーサルEA:カスタムトレーリングストップ(その6) ユニバーサルEA:カスタムトレーリングストップ(その6)
The sixth part of the article about the universal Expert Advisor describes the use of the trailing stop feature. The article will guide you through how to create a custom trailing stop module using unified rules, as well as how to add it to the trading engine so that it would automatically manage positions.
マーケットでの公開前にトレードロボットに行うべき検査 マーケットでの公開前にトレードロボットに行うべき検査
マーケットの全ての製品は、均一な品質基準を確保する為に、公開前に事前の必須検査を受けます。この記事では、開発者が自分のテクニカルインディケータやトレードロボットで犯しがちなミスについてお話しします。また、マーケットへ提出する前の、製品の自己テストの方法もご紹介します。