
MQL5開発用のカスタムデバッグおよびプロファイリングツール(第1回):高度なロギング
計画は次のとおりです。
はじめに
MQL5でエキスパートアドバイザー(EA)、インジケーター、あるいはスクリプトを開発した経験がある方なら、ライブトレードが予期せぬ挙動を示す、複雑な計算式が誤った値を返す、あるいは市場が活発化するタイミングでEAが停止してしまう、といった苛立ちを覚えたことがあるでしょう。従来の応急策としては、Print文をあちこちに挿入し、ストラテジーテスターを起動し、問題が再現されることを祈る、といった方法が考えられます。しかし、コードベースが拡大するにつれて、この手法では問題解決に至らなくなります。
MQL5には、一般的なプログラミング言語にはない独自のデバッグ上の課題があります。取引プログラムはリアルタイムで動作するため、タイミングの正確性が極めて重要です。また、実際の資金を扱うため、ミスは即座に大きな損失につながります。さらに、変動の激しい市場環境下でも高速かつ安定した処理を維持する必要があります。MetaEditorに標準搭載された機能(ステップ実行デバッガ、PrintやCommentによる基本的出力、高レベルのプロファイラ)は便利ではありますが、汎用的な設計であり、取引アルゴリズムに求められる精密な診断には不十分です。
このような背景から、独自のデバッグおよびプロファイリングツールを構築することは、開発効率や品質管理において決定的な優位性をもたらします。カスタムユーティリティを用いれば、標準機能では得られない詳細な診断情報や柔軟なワークフローを実現でき、バグの早期発見、パフォーマンスの最適化、コード品質の確保が可能になります。
本連載では、まさにそのような高度なツールキットの構築手法を体系的に解説します。まず、基盤となるのは単なるPrint文に比べ格段に強力な汎用ロギングフレームワークです。これに続き、高度なデバッガ、カスタムプロファイラ、ユニットテスト環境、静的コード解析ツールを組み合わせていきます。最終的には、問題発生後に対処する「事後対応型」の開発スタイルから、事前に品質を管理する「予防型」の開発プロセスへ移行できます。
各回の内容は実践的で、MQL5で即座に利用可能なサンプルコード、仕組みの詳細な解説、設計上の意図まで含まれています。これにより、読者は自分のプロジェクトにすぐに応用できるツールと、その応用ノウハウを習得することができます。
まずは、診断の基礎として、プログラムの動作を瞬時に正確に把握するためのカスタムロギングフレームワークの構築から始めましょう。
カスタムロギングフレームワークの構築
このセクションでは、MQL5の基本的なPrint関数をはるかに超える、柔軟で強力なロギングフレームワークを開発します。作成するカスタムロガーは、複数の出力形式、重要度レベル、文脈情報をサポートし、複雑な取引システムのデバッグを格段に効率化します。
通常のPrintが不十分な理由
新しいシステムを構築する前に、Printのみに頼ることがプロジェクト上問題になる理由を確認しておきましょう。
- 重要度階層がない:すべてのメッセージが同じ扱いになるため、重大な警告が日常的な出力に埋もれてしまいます。
- 文脈情報が乏しい:Printでは、どの関数がメッセージを出したのか、実行時のアプリケーション状態が何だったのかを知ることができません。
- 出力先が限定的:すべての出力は[エキスパート]タブに集約され、ファイルや別のターゲットに送る仕組みはありません。
- フィルタリング不可:本番環境で冗長なデバッグログを抑制しようとすると、重要なエラーまで抑制されてしまいます。
- 非構造化テキスト:自由形式の出力は、自動ツールで解析するには扱いにくいです。
私たちのカスタムロギングフレームワークは、これらの課題すべてに対応し、複雑な取引コードのトラブルシューティングのための堅牢な基盤を提供します。
ロガーの設計
クリーンでモジュール化されたオブジェクト指向システムを、次の3つの主要コンポーネントを中心に構築します。- LogLevels:重要度レベル(DEBUG、INFO、WARN、ERROR、FATAL)を定義する列挙型
- ILogHandler:FileLogHandlerやConsoleLogHandlerなど、異なる出力先を差し替え可能にするインターフェース
- CLogger:ハンドラを保持し、ロギングAPIを提供するシングルトン管理者
次に、それぞれのパートを詳しく解説します。
ログレベル
まず、LogLevels.mqhで重大度レベルを定義します。
enum LogLevel { LOG_LEVEL_DEBUG = 0, // Detailed information for debugging purposes. LOG_LEVEL_INFO = 1, // General information about the system's operation. LOG_LEVEL_WARN = 2, // Warnings about potential issues that are not critical. LOG_LEVEL_ERROR = 3, // Errors that affect parts of the system but allow continuity. LOG_LEVEL_FATAL = 4, // Serious problems that interrupt the system's execution. LOG_LEVEL_OFF = 5 // Turn off logging. };
これらのレベルにより、メッセージを重要度ごとに分類し、必要に応じてフィルタリングできます。たとえば、開発中はすべてのメッセージ(DEBUGも含む)を確認したい場合がありますが、本番環境ではWARN以上の重要度のメッセージだけを表示したいこともあります。
ハンドラインターフェース
次に、ログハンドラ用のインターフェースをILogHandler.mqhで定義します。
#property strict #include "LogLevels.mqh" #include <Arrays/ArrayObj.mqh> // For managing handlers //+------------------------------------------------------------------+ //| Interface: ILogHandler | //| Description: Defines the contract for log handling mechanisms. | //| Each handler is responsible for processing and | //| outputting log messages in a specific way (e.g., to | //| console, file, database). | //+------------------------------------------------------------------+ interface ILogHandler { //--- Method to configure the handler with specific settings virtual bool Setup(const string settings=""); //--- Method to process and output a log message virtual void Log(const datetime time, const LogLevel level, const string origin, const string message, const long expert_id=0); //--- Method to perform any necessary cleanup virtual void Shutdown(); }; //+------------------------------------------------------------------+
このヘッダファイルILogHandler.mqhは、ロギングフレームワークの重要なコンポーネントであるILogHandlerインターフェースを定義しています。MQL5におけるインターフェースは、設計図または契約のようなもので、実装するクラスが必ず提供しなければならないメソッドのセットを指定します。ILogHandlerの目的は、コンソールへの出力やファイルへの書き込みなど、異なるログ出力メカニズムがメインロガーと統一的に連携できるように標準化することです。
ILogHandlerインターフェース自体では、具体的なハンドラクラスが実装すべき3つの仮想メソッドを宣言しています。- virtual bool Setup(const string settings=""):このメソッドは、特定のログハンドラを初期化および設定するために使用されます。オプションの文字列引数settingsにより、ファイルパス、フォーマット文字列、最小ログレベルなどの設定パラメータをハンドラに渡すことができます。成功した場合はtrue、失敗した場合はfalseを返し、メインロガーがハンドラの準備状況を把握できるようにします。
- virtual void Log(const datetime time, const LogLevel level, const string origin, const string message, const long expert_id=0):このメソッドは、単一のログメッセージを処理・出力するためのコアメソッドです。ログイベントについて必要なすべての情報、つまり、タイムスタンプ(time)、重要度レベル(level:LogLevels.mqhから)、メッセージの発生源(origin)、実際のログメッセージ(message)、オプションのEA ID(expert_id)を受け取ります。各実装クラスは、目的に応じてこの情報をどのようにフォーマットし、どこに送信するかを定義します(例:コンソールにPrint、ファイルに書き込み)。
- virtual void Shutdown():このメソッドは、ログハンドラが不要になったときにクリーンアップ処理をおこなうために使用されます。通常はメインロガーやアプリケーションのシャットダウン時に呼び出されます。実装例としては、開いているファイルハンドルを閉じる、確保したリソースを解放する、バッファ内の出力をフラッシュしてすべてのログを保存する、などがあります。
この標準インターフェースを定義することで、ロギングフレームワークは柔軟性と拡張性を実現できます。メインのCLoggerクラスは、複数のILogHandlerオブジェクトを管理し、それぞれにログメッセージを送信できます。ハンドラの具体的な動作方法を知る必要はありません。新しい出力先を追加したい場合は、単にILogHandlerを実装した新しいクラスを作成すればよいのです。
コンソールログハンドラ
このヘッダファイルは、ConsoleLogHandlerクラスを提供します。これはILogHandlerインターフェースの具体的実装で、フォーマット済みログメッセージをMetaTrader 5の[エキスパート]タブに送ることを目的としています。このタブは、EAやスクリプト実行時のコンソール出力領域として機能します。
#property strict #include "ILogHandler.mqh" #include "LogLevels.mqh" //+------------------------------------------------------------------+ //| Class: ConsoleLogHandler | //| Description: Implements ILogHandler to output log messages to | //| the MetaTrader 5 Experts tab (console). | //+------------------------------------------------------------------+ class ConsoleLogHandler : public ILogHandler { private: LogLevel m_min_level; // Minimum level to log string m_format; // Log message format string //--- Helper to format the log message string FormatMessage(const datetime time, const LogLevel level, const string origin, const string message); //--- Helper to get string representation of LogLevel string LogLevelToString(const LogLevel level); public: ConsoleLogHandler(const LogLevel min_level = LOG_LEVEL_INFO, const string format = "[{time}] {level}: {origin} - {message}"); ~ConsoleLogHandler(); //--- ILogHandler implementation virtual bool Setup(const string settings="") override; virtual void Log(const datetime time, const LogLevel level, const string origin, const string message, const long expert_id=0) override; virtual void Shutdown() override; //--- Setters void SetMinLevel(const LogLevel level) { m_min_level = level; } void SetFormat(const string format) { m_format = format; } };
ConsoleLogHandlerクラスはILogHandlerからpublicで継承しており、インターフェースで定義されたSetup、Log、Shutdownメソッドを必ず実装することを約束しています。このクラスには2つのprivateメンバー変数があります。m_min_level(型:LogLevel)は、このハンドラがログを出力するために必要な最小の重要度レベルを保持します。m_format(型:string)は、出力メッセージをフォーマットするためのテンプレート文字列を保持します。さらに、privateのヘルパーメソッドとしてFormatMessageとLogLevelToStringを宣言しており、インターフェース実装用のpublicメソッドや、privateメンバー用のセッターも提供しています。
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ ConsoleLogHandler::ConsoleLogHandler(const LogLevel min_level = LOG_LEVEL_INFO, const string format = "[{time}] {level}: {origin} - {message}") { m_min_level = min_level; m_format = format; // No specific setup needed for console logging initially } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ ConsoleLogHandler::~ConsoleLogHandler() { // No specific cleanup needed }
コンストラクタは、新しいConsoleLogHandlerオブジェクトを初期化します。コンストラクタは2つのオプション引数を受け取ります。min_level(デフォルトはLOG_LEVEL_INFO)とformat(デフォルトは標準テンプレート "[{time}] {level}: {origin} - {message}")です。これらの引数は、それぞれメンバー変数m_min_levelとm_formatの初期値を設定するために使用されます。これにより、ユーザーはハンドラ作成時に、フィルタリングレベルや出力形式を簡単にカスタマイズできます。
デストラクタは、ConsoleLogHandlerオブジェクトが破棄される際にリソースを解放する役割を持ちます。この具体的な実装では、動的に確保されたリソースやオープンハンドルを直接管理していないため、デストラクタの本体は空です。つまり、このハンドラに特別なクリーンアップ処理は必要ありません。
//+------------------------------------------------------------------+ //| Setup | //+------------------------------------------------------------------+ bool ConsoleLogHandler::Setup(const string settings="") { // Settings could be used to parse format or min_level, but we use constructor args for now // Example: Parse settings string if needed return true; } //+------------------------------------------------------------------+ //| Log | //+------------------------------------------------------------------+ void ConsoleLogHandler::Log(const datetime time, const LogLevel level, const string origin, const string message, const long expert_id=0) { // Check if the message level meets the minimum requirement if(level >= m_min_level && level < LOG_LEVEL_OFF) { // Format and print the message to the Experts tab Print(FormatMessage(time, level, origin, message)); } } //+------------------------------------------------------------------+ //| Shutdown | //+------------------------------------------------------------------+ void ConsoleLogHandler::Shutdown() { // No specific shutdown actions needed for console logging PrintFormat("%s: ConsoleLogHandler shutdown.", __FUNCTION__); }
- セットアップメソッド(ConsoleLogHandler::Setup):
このメソッドは、ILogHandlerインターフェースで要求されるSetup関数を実装しています。設定用に設計されていますが、現在の実装ではsettings文字列パラメータは使用されていません。主な設定(最小ログレベルとフォーマット)はコンストラクタで処理されているためです。このメソッドは単にtrueを返し、ハンドラは常に作成後に使用可能であることを示します。 - ログメソッド(ConsoleLogHandler::Log):
これはコンソールへのログ出力のコア実装です。メインのCLoggerから呼び出されると、まずメッセージの重要度レベルがハンドラ設定のm_min_level以上であり、かつLOG_LEVEL_OFF未満であるかどうかをチェックします。メッセージがフィルターを通過した場合、privateヘルパー関数FormatMessageを呼び出し、m_formatテンプレートと提供されたログ情報(time、level、origin、message)に基づき最終的な出力文字列を作成します。最後に、MQL5の組み込み関数Printを使用して、フォーマット済み文字列を[エキスパート]タブに表示します。 - シャットダウンメソッド(ConsoleLogHandler::Shutdown):
このメソッドはインターフェースのShutdown関数を実装しています。デストラクタと同様、コンソールへのログ出力では通常、ファイルを閉じるなどの特別な終了処理は不要です。この実装では単に、コンソールハンドラがシャットダウンされることを示すメッセージを表示し、アプリケーション終了シーケンス中の確認用としています。
//+------------------------------------------------------------------+ //| FormatMessage | //+------------------------------------------------------------------+ string ConsoleLogHandler::FormatMessage(const datetime time, const LogLevel level, const string origin, const string message) { string formatted_message = m_format; // Replace placeholders StringReplace(formatted_message, "{time}", TimeToString(time, TIME_DATE | TIME_SECONDS)); StringReplace(formatted_message, "{level}", LogLevelToString(level)); StringReplace(formatted_message, "{origin}", origin); StringReplace(formatted_message, "{message}", message); return formatted_message; } //+------------------------------------------------------------------+ //| LogLevelToString | //+------------------------------------------------------------------+ string ConsoleLogHandler::LogLevelToString(const LogLevel level) { switch(level) { case LOG_LEVEL_DEBUG: return "DEBUG"; case LOG_LEVEL_INFO: return "INFO"; case LOG_LEVEL_WARN: return "WARN"; case LOG_LEVEL_ERROR: return "ERROR"; case LOG_LEVEL_FATAL: return "FATAL"; default: return "UNKNOWN"; } } //+------------------------------------------------------------------+
- ヘルパーメソッド(FormatMessage)
このprivateヘルパー関数は、生のログ情報(time、level、origin、message)とハンドラのフォーマット文字列(m_format)を入力として受け取ります。関数の役割は、フォーマット文字列内の{time}、{level}、{origin}、{message}といったプレースホルダーを実際の値に置き換えることです。タイムスタンプのフォーマットにはTimeToStringを使用し、重要度レベルの文字列化にはLogLevelToStringを呼び出します。こうして生成された完全にフォーマットされた文字列は、最終的にLogメソッドに返され、コンソールに出力されます。 - ヘルパーメソッド(LogLevelToString)
このprivateユーティリティ関数は、LogLevel列挙型の値を対応する文字列に変換します(例:LOG_LEVEL_INFO →「INFO」)。switch文で定義済みのログレベルを処理し、想定外の値の場合は「UNKNOWN」を返します。これにより、フォーマット済みログ出力で人間が読みやすいレベル表示を提供できます。 - セッターメソッド(SetMinLevel、SetFormat):これらのpublicメソッドを使うことで、ハンドラ作成後に設定を変更できます。SetMinLevelはm_min_levelメンバー変数を更新し、今後のログメッセージのフィルタリング閾値を変更します。SetFormatはm_formatメンバー変数を更新し、今後のログメッセージのフォーマットテンプレートを変更します。
ファイルログハンドラ
このヘッダファイルには、FileLogHandlerクラスが含まれています。これはILogHandlerインターフェースのもう一つの具体的実装です。このハンドラは、永続的なロギングを目的としており、フォーマット済みのログメッセージをファイルに書き込みます。これには、日付やファイルサイズに基づくログファイルの自動ローテーションや、保存するログファイルの数の管理など、コンソールハンドラに比べてより高度な機能が含まれています。
#property strict #include "ILogHandler.mqh" #include "LogLevels.mqh" //+------------------------------------------------------------------+ //| Class: FileLogHandler | //| Description: Implements ILogHandler to output log messages to | //| files with rotation capabilities. | //+------------------------------------------------------------------+ class FileLogHandler : public ILogHandler { private: LogLevel m_min_level; // Minimum level to log string m_format; // Log message format string string m_file_path; // Base path for log files string m_file_prefix; // Prefix for log file names int m_file_handle; // Current file handle datetime m_current_day; // Current day for rotation int m_max_size_kb; // Maximum file size in KB before rotation int m_max_files; // Maximum number of log files to keep //--- Helper to format the log message string FormatMessage(const datetime time, const LogLevel level, const string origin, const string message); //--- Helper to get string representation of LogLevel string LogLevelToString(const LogLevel level); //--- Helper to create or rotate log file bool EnsureFileOpen(); //--- Helper to generate file name based on date string GenerateFileName(const datetime time); //--- Helper to perform log rotation void RotateLogFiles(); //--- Helper to check if file size exceeds limit bool IsFileSizeExceeded(); // Add custom helper function to sort string arrays void SortStringArray(string &arr[]); //--- New helper to clean file paths string CleanPath(const string path); public: FileLogHandler(const string file_path="MQL5\\Logs", const string file_prefix="EA_Log", const LogLevel min_level=LOG_LEVEL_INFO, const string format="[{time}] {level}: {origin} - {message}", const int max_size_kb=1024, const int max_files=5); virtual ~FileLogHandler(); //--- ILogHandler implementation virtual bool Setup(const string settings="") override; virtual void Log(const datetime time, const LogLevel level, const string origin, const string message, const long expert_id=0) override; virtual void Shutdown() override; //--- Setters void SetFilePath(const string path) { m_file_path = CleanPath(path); } void SetMinLevel(const LogLevel level) { m_min_level = level; } void SetFormat(const string format) { m_format = format; } void SetFilePrefix(const string prefix){ m_file_prefix = prefix; } void SetMaxSizeKB(const int size) { m_max_size_kb = size; } void SetMaxFiles(const int count) { m_max_files = count; } };
FileLogHandlerクラスはILogHandlerから継承しています。このクラスは状態や設定を管理するためにいくつかのprivateメンバー変数を保持しています。m_min_levelとm_formatはコンソールハンドラと同様にログの重要度とフォーマットを管理します。m_file_pathはログファイルを保存するディレクトリを示し、m_file_prefixはログファイルの基本名として使用されます。m_file_handleは現在開かれているログファイルのハンドルを保持し、m_current_dayは日次ローテーションの管理に用いられます。さらに、m_max_size_kbは単一ログファイルのサイズ上限をキロバイト単位で保持し、m_max_filesは保持するログファイルの最大数を管理します。
このクラスではフォーマットやファイル管理、ローテーションのためのprivateヘルパーメソッドも複数宣言されています。具体的にはFormatMessage、LogLevelToString、EnsureFileOpen、GenerateFileName、RotateLogFiles、IsFileSizeExceeded、SortStringArray、CleanPathです。publicメソッドとしてはコンストラクタ、デストラクタ、インターフェース実装、そして設定用のセッターが用意されています。
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ FileLogHandler::FileLogHandler(const string file_path, const string file_prefix, const LogLevel min_level, const string format, const int max_size_kb, const int max_files) { m_min_level = min_level; m_format = format; m_file_path = CleanPath(file_path); m_file_prefix = file_prefix; m_file_handle = INVALID_HANDLE; m_current_day = 0; m_max_size_kb = max_size_kb; m_max_files = max_files; // Create directory if it doesn't exist if(!FolderCreate(m_file_path)) { if(GetLastError() != 0) Print("FileLogHandler: Failed to create directory: ", m_file_path, ", error: ", GetLastError()); } } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ FileLogHandler::~FileLogHandler() { Shutdown(); }
- コンストラクタ(FileLogHandler::FileLogHandler)
コンストラクタはFileLogHandlerを初期化します。ファイルパス、プレフィックス、最小ログレベル、フォーマット文字列、最大ファイルサイズ、最大ファイル数の引数を受け取り、それぞれ対応するメンバー変数に設定します。CleanPathヘルパーを使用して、ファイルパスが正しいディレクトリ区切り文字を使用していることを確認します。重要なのは、指定されたログディレクトリ(端末のデータパスに対するm_file_path)が存在しない場合、FolderCreateを使用して作成を試みる点です。これにより、ハンドラがファイルを書き込む場所を確保できます。 - デストラクタ(FileLogHandler::~FileLogHandler)
デストラクタはShutdownメソッドを呼び出すことで適切なクリーンアップをおこないます。これにより、FileLogHandlerオブジェクトが破棄される際に現在開いているログファイルハンドルが閉じられ、リソースリークを防止できます。
//+------------------------------------------------------------------+ //| Setup | //+------------------------------------------------------------------+ bool FileLogHandler::Setup(const string settings) { // Parse settings if provided // Format could be: "path=MQL5/Logs;prefix=MyEA;min_level=INFO;max_size=2048;max_files=10" if(settings != "") { string parts[]; int count = StringSplit(settings, ';', parts); for(int i = 0; i < count; i++) { string key_value[]; if(StringSplit(parts[i], '=', key_value) == 2) { string key = key_value[0]; StringTrimLeft(key); StringTrimRight(key); string value = key_value[1]; StringTrimLeft(value); StringTrimRight(value); if(key == "path") m_file_path = CleanPath(value); else if(key == "prefix") m_file_prefix = value; else if(key == "min_level") { if(value == "DEBUG") m_min_level = LOG_LEVEL_DEBUG; else if(value == "INFO") m_min_level = LOG_LEVEL_INFO; else if(value == "WARN") m_min_level = LOG_LEVEL_WARN; else if(value == "ERROR") m_min_level = LOG_LEVEL_ERROR; else if(value == "FATAL") m_min_level = LOG_LEVEL_FATAL; } else if(key == "max_size") m_max_size_kb = (int)StringToInteger(value); else if(key == "max_files") m_max_files = (int)StringToInteger(value); } } } return true; } //+------------------------------------------------------------------+ //| Log | //+------------------------------------------------------------------+ void FileLogHandler::Log(const datetime time, const LogLevel level, const string origin, const string message, const long expert_id=0) { // Check if the message level meets the minimum requirement if(level >= m_min_level && level < LOG_LEVEL_OFF) { // Ensure file is open and ready for writing if(EnsureFileOpen()) { // Format the message string formatted_message = FormatMessage(time, level, origin, message); // Write to file FileWriteString(m_file_handle, formatted_message + "\r\n"); // Flush to ensure data is written immediately FileFlush(m_file_handle); // Check if rotation is needed if(IsFileSizeExceeded()) { FileClose(m_file_handle); m_file_handle = INVALID_HANDLE; RotateLogFiles(); EnsureFileOpen(); } } } } //+------------------------------------------------------------------+ //| Shutdown | //+------------------------------------------------------------------+ void FileLogHandler::Shutdown() { if(m_file_handle != INVALID_HANDLE) { FileClose(m_file_handle); m_file_handle = INVALID_HANDLE; } }
- セットアップメソッド(FileLogHandler::Setup)
このメソッドはインターフェースのSetup関数を実装しています。作成後に単一の設定文字列(例:"path=MQL5/Logs;prefix=MyEA;max_size=2048")を使ってハンドラを構成する代替手段を提供します。設定文字列を解析し、キーと値のペアに分割して、それぞれ対応するメンバー変数(m_file_path、m_file_prefix、m_min_level、m_max_size_kb、m_max_files)を更新します。これにより、外部ソースから設定を読み込むことも可能です。解析後はtrueを返します。 - ログメソッド(FileLogHandler::Log)
このメソッドはファイルへのログ出力の核心部分を処理します。まず、メッセージのレベルがm_min_levelを満たしているかを確認します。条件を満たす場合、EnsureFileOpenを呼び出して有効なログファイルが開かれていることを確認します(必要に応じて日次ローテーションも処理します)。ファイルが正常に開かれた場合、FormatMessageでメッセージをフォーマットし、フォーマット済み文字列に改行コード\r\nを付加してFileWriteStringでファイルに書き込みます。その後、FileFlushを呼び出してデータがすぐにディスクに書き込まれるようにします。これはアプリケーションがクラッシュしてもログを確実に残すために重要です。最後に、IsFileSizeExceededを使って現在のファイルサイズがm_max_size_kbの制限を超えていないかを確認します。制限を超えている場合、現在のファイルを閉じ、RotateLogFilesで古いファイルを管理し、EnsureFileOpenで新しいファイルを再度開きます。 - シャットダウンメソッド(FileLogHandler::Shutdown)
このメソッドはインターフェースのShutdown要件を実装しています。主な役割は、現在開かれているログファイルハンドルm_file_handleが有効(!= INVALID_HANDLE)であればFileCloseを使って閉じることです。これにより、ロガーがシャットダウンされる際にファイルが正しく閉じられ、すべてのバッファデータが書き込まれます。
//+------------------------------------------------------------------+ //| FormatMessage | //+------------------------------------------------------------------+ string FileLogHandler::FormatMessage(const datetime time, const LogLevel level, const string origin, const string message) { string formatted_message = m_format; // Replace placeholders StringReplace(formatted_message, "{time}", TimeToString(time, TIME_DATE | TIME_SECONDS)); StringReplace(formatted_message, "{level}", LogLevelToString(level)); StringReplace(formatted_message, "{origin}", origin); StringReplace(formatted_message, "{message}", message); return formatted_message; } //+------------------------------------------------------------------+ //| LogLevelToString | //+------------------------------------------------------------------+ string FileLogHandler::LogLevelToString(const LogLevel level) { switch(level) { case LOG_LEVEL_DEBUG: return "DEBUG"; case LOG_LEVEL_INFO: return "INFO"; case LOG_LEVEL_WARN: return "WARN"; case LOG_LEVEL_ERROR: return "ERROR"; case LOG_LEVEL_FATAL: return "FATAL"; default: return "UNKNOWN"; } }
ヘルパーメソッド(FormatMessage、LogLevelToString): これらのprivateヘルパーは、コンソールハンドラの対応するメソッドと同様に動作します。m_format文字列に基づいたメッセージのフォーマットをおこない、LogLevel列挙型を読みやすい文字列に変換します。
//+------------------------------------------------------------------+ //| EnsureFileOpen | //+------------------------------------------------------------------+ bool FileLogHandler::EnsureFileOpen() { datetime current_time = TimeCurrent(); MqlDateTime time_struct; TimeToStruct(current_time, time_struct); // Create a datetime that represents just the current day (time set to 00:00:00) MqlDateTime day_struct; day_struct.year = time_struct.year; day_struct.mon = time_struct.mon; day_struct.day = time_struct.day; day_struct.hour = 0; day_struct.min = 0; day_struct.sec = 0; datetime current_day = StructToTime(day_struct); // Check if we need to open a new file (either first time or new day) if(m_file_handle == INVALID_HANDLE || m_current_day != current_day) { // Close existing file if open if(m_file_handle != INVALID_HANDLE) { FileClose(m_file_handle); m_file_handle = INVALID_HANDLE; } // Update current day m_current_day = current_day; // Generate new file name string file_name = GenerateFileName(current_time); // Open file for writing (append if exists) m_file_handle = FileOpen(file_name, FILE_WRITE | FILE_READ | FILE_TXT); if(m_file_handle == INVALID_HANDLE) { Print("FileLogHandler: Failed to open log file: ", file_name, ", error: ", GetLastError()); return false; } // Move to end of file for appending FileSeek(m_file_handle, 0, SEEK_END); } return true; } //+------------------------------------------------------------------+ //| GenerateFileName | //+------------------------------------------------------------------+ string FileLogHandler::GenerateFileName(const datetime time) { MqlDateTime time_struct; TimeToStruct(time, time_struct); string date_str = StringFormat("%04d%02d%02d", time_struct.year, time_struct.mon, time_struct.day); return m_file_path + "\\" + m_file_prefix + "_" + date_str + ".log"; } //+------------------------------------------------------------------+ //| IsFileSizeExceeded | //+------------------------------------------------------------------+ bool FileLogHandler::IsFileSizeExceeded() { if(m_file_handle != INVALID_HANDLE) { // Get current position (file size) ulong size = FileSize(m_file_handle); // Check if size exceeds limit (convert KB to bytes) return (size > (ulong)m_max_size_kb * 1024); } return false; }
- ヘルパーメソッド(EnsureFileOpen)
この重要なヘルパーメソッドは、ログファイルのオープンと日次ローテーションを管理します。現在の日付(TimeCurrent()で取得)を保存されているm_current_dayと比較します。ファイルハンドルが無効である場合や日付が変わっている場合、既存のハンドルを閉じ、m_current_dayを更新し、GenerateFileNameを使用して新しいファイル名(日付を含む)を生成し、この新しいファイルを読み書きモード(FILE_WRITE | FILE_READ | FILE_TXT)で開きます。さらに、FileSeekを使ってファイルの末尾に移動し、新しいログが追記されるようにします。ファイルが正常に開かれた場合またはすでに開かれている場合はtrueを返し、失敗した場合はfalseを返します。 - ヘルパーメソッド(GenerateFileName)
このユーティリティは、現在の時刻に基づいてログファイルのフルパスを生成します。日付部分をYYYYMMDD形式にフォーマットし、設定されたm_file_path、m_file_prefix、および.log拡張子と組み合わせます。 - ヘルパーメソッド(IsFileSizeExceeded)
この関数は、現在開かれているログファイル(m_file_handle)のサイズが設定されたm_max_size_kbの制限を超えているかを確認します。FileSizeでファイルサイズを取得し、制限(バイト単位に変換)と比較します。サイズが超過している場合はtrueを返し、それ以外の場合はfalseを返します。
//+------------------------------------------------------------------+ //| RotateLogFiles | //+------------------------------------------------------------------+ void FileLogHandler::RotateLogFiles() { // Get list of log files string terminal_path = TerminalInfoString(TERMINAL_DATA_PATH); string full_path = terminal_path + "\\" + m_file_path; string file_pattern = m_file_prefix + "_*.log"; string files[]; int file_count = 0; long search_handle = FileFindFirst(full_path + "\\" + file_pattern, files[file_count]); if(search_handle != INVALID_HANDLE) { file_count++; // Find all matching files while(FileFindNext(search_handle, files[file_count])) { file_count++; ArrayResize(files, file_count + 1); } // Close search handle FileFindClose(search_handle); } // Resize array to actual number of found files before sorting ArrayResize(files, file_count); // Sort the string array using the custom sorter SortStringArray(files); // Delete oldest files if we have too many int files_to_delete = file_count - m_max_files + 1; // +1 for the new file we'll create if(files_to_delete > 0) { for(int i = 0; i < files_to_delete; i++) { if(!FileDelete(m_file_path + "\\" + files[i])) Print("FileLogHandler: Failed to delete old log file: ", files[i], ", error: ", GetLastError()); } } } //+------------------------------------------------------------------+ //| SortStringArray | //+------------------------------------------------------------------+ void FileLogHandler::SortStringArray(string &arr[]) { int n = ArraySize(arr); for(int i = 0; i < n - 1; i++) { for(int j = i + 1; j < n; j++) { if(arr[i] > arr[j]) { string temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } } } } //+------------------------------------------------------------------+ //| New implementation: CleanPath | //+------------------------------------------------------------------+ string FileLogHandler::CleanPath(const string path) { string result = path; // Replace all "/" with "\\" StringReplace(result, "/", "\\"); return result; } //+------------------------------------------------------------------+
- ヘルパーメソッド(RotateLogFiles)
このメソッドはログファイルの保持ポリシーを実装します。ログディレクトリ内でパターンm_file_prefix_*.logに一致するすべてのファイルをFileFindFirstとFileFindNextを使って検索します。ファイル名は文字列配列に格納され、SortStringArrayヘルパーを使ってアルファベット順にソートされます。ファイル名に日付が含まれているため、通常はこれが時系列順になります。次に、m_max_files制限を超えるファイル数を計算し、最も古いファイル(ソートされたリストの先頭にあるもの)をFileDeleteで削除します。 - ヘルパーメソッド(SortStringArray)
これはRotateLogFilesで取得したログファイル名配列をソートするための簡単なバブルソート実装です。MQL5標準ライブラリには文字列配列をソートする組み込み関数がないため使用されます。 - ヘルパーメソッド(CleanPath)
このユーティリティは、ディレクトリパスがMQL5のファイル関数で期待されるバックスラッシュ(\)区切りを使用するようにし、入力パス文字列に含まれるスラッシュ(/)を置き換えます。 - セッターメソッド(SetFilePath、SetMinLevelなど)
これらのpublicメソッドにより、ハンドラ作成後でもパス、プレフィックス、ログレベル、フォーマット、サイズ制限などの設定を変更できます。これにより柔軟性が提供されます。
CLogger
このヘッダファイルはCLoggerクラスを定義しています。CLoggerはロギングフレームワーク全体の中心的なオーケストレーターとして機能します。シングルトンデザインパターンを用いて実装されており、アプリケーション全体でロガーのインスタンスが一つだけ存在することを保証します。この単一のインスタンスが、登録されたすべてのログハンドラを管理し、ユーザーのコードがログメッセージを送信するための主要なインターフェースを提供します。#property strict #include "LogLevels.mqh" #include "ILogHandler.mqh" //+------------------------------------------------------------------+ //| Class: CLogger | //| Description: Singleton class for managing and dispatching log | //| messages to registered handlers. | //+------------------------------------------------------------------+ class CLogger { private: static CLogger *s_instance; ILogHandler* m_handlers[]; LogLevel m_global_min_level; long m_expert_magic; string m_expert_name; //--- Private constructor for Singleton CLogger(); ~CLogger(); public: //--- Get the singleton instance static CLogger* Instance(); //--- Cleanup the singleton instance static void Release(); //--- Handler management bool AddHandler(ILogHandler *handler); void ClearHandlers(); //--- Configuration void SetGlobalMinLevel(const LogLevel level); void SetExpertInfo(const long magic, const string name); //--- Logging methods void Log(const LogLevel level, const string origin, const string message); void Debug(const string origin, const string message); void Info(const string origin, const string message); void Warn(const string origin, const string message); void Error(const string origin, const string message); void Fatal(const string origin, const string message); //--- Formatted logging methods void LogFormat(const LogLevel level, const string origin, const string formatted_message); void DebugFormat(const string origin, const string formatted_message); void InfoFormat(const string origin, const string formatted_message); void WarnFormat(const string origin, const string formatted_message); void ErrorFormat(const string origin, const string formatted_message); void FatalFormat(const string origin, const string formatted_message); };
CLoggerクラスにはいくつかのprivateメンバーがあります。s_instanceはクラス自身の単一インスタンスを保持する静的ポインタです。m_handlersはILogHandlerポインタの動的配列で、アクティブなすべてのログハンドラ(コンソールやファイルハンドラなど)への参照を保持します。m_global_min_levelはグローバルなフィルタリング閾値を設定し、このレベル未満のメッセージは個々のハンドラに送信される前に無視されます。m_expert_magicとm_expert_nameは、ロガーを使用しているEAに関する任意の情報を保持し、ログメッセージに文脈情報として含めることができます。
コンストラクタとデストラクタはシングルトンパターンを強制するためにprivateになっています。publicメソッドは、インスタンスへのアクセス、ハンドラ管理、設定、およびさまざまなロギング機能を提供します。
//+------------------------------------------------------------------+ //| Static instance initialization | //+------------------------------------------------------------------+ CLogger *CLogger::s_instance = NULL; //+------------------------------------------------------------------+ //| Get Singleton Instance | //+------------------------------------------------------------------+ CLogger* CLogger::Instance() { if(s_instance == NULL) { s_instance = new CLogger(); } return s_instance; } //+------------------------------------------------------------------+ //| Release Singleton Instance | //+------------------------------------------------------------------+ void CLogger::Release() { if(s_instance != NULL) { delete s_instance; s_instance = NULL; } } //+------------------------------------------------------------------+ //| Constructor (Private) | //+------------------------------------------------------------------+ CLogger::CLogger() { m_global_min_level = LOG_LEVEL_DEBUG; m_expert_magic = 0; m_expert_name = ""; } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CLogger::~CLogger() { ClearHandlers(); }
- シングルトンの実装(Instance、Release、privateコンストラクタ)
シングルトンパターンは静的メソッドInstance()によって実装されています。このメソッドは初回呼び出し時にCLoggerオブジェクトを生成し、その後の呼び出しでは同じインスタンスを返します。コンストラクタ(CLogger::CLogger)はprivateになっており、クラス外から直接インスタンス化できません。コンストラクタ内では、グローバル最小ログレベルやエキスパート情報のデフォルト値を初期化します。静的メソッドRelease()は、シングルトンインスタンスを明示的に削除しリソースをクリーンアップするために提供されており、通常はアプリケーションのシャットダウン時に呼び出されます。 - デストラクタ(CLogger::~CLogger)
デストラクタは、Release()メソッドによってシングルトンインスタンスが削除される際に呼び出されます。主な役割は、ClearHandlersメソッドを呼び出して管理下のすべてのハンドラをクリーンアップすることです。これにより、各ハンドラのShutdownメソッドが呼ばれ、ハンドラオブジェクト自体も削除されます。
//+------------------------------------------------------------------+ //| AddHandler | //+------------------------------------------------------------------+ bool CLogger::AddHandler(ILogHandler *handler) { if(CheckPointer(handler) == POINTER_INVALID) { Print("CLogger::AddHandler - Error: Invalid handler pointer."); return false; } int size = ArraySize(m_handlers); ArrayResize(m_handlers, size + 1); m_handlers[size] = handler; return true; } //+------------------------------------------------------------------+ //| ClearHandlers | //+------------------------------------------------------------------+ void CLogger::ClearHandlers() { for(int i = 0; i < ArraySize(m_handlers); i++) { ILogHandler *handler = m_handlers[i]; if(CheckPointer(handler) != POINTER_INVALID) { handler.Shutdown(); delete handler; } } ArrayResize(m_handlers, 0); } //+------------------------------------------------------------------+ //| SetGlobalMinLevel | //+------------------------------------------------------------------+ void CLogger::SetGlobalMinLevel(const LogLevel level) { m_global_min_level = level; } //+------------------------------------------------------------------+ //| SetExpertInfo | //+------------------------------------------------------------------+ void CLogger::SetExpertInfo(const long magic, const string name) { m_expert_magic = magic; m_expert_name = name; }
- ハンドラ管理(AddHandler、ClearHandlers)
AddHandlerメソッドは、新しいログハンドラ(ILogHandlerを実装する任意のオブジェクト)をロガーの内部リストm_handlersに追加するために使用されます。有効なポインタであることを確認し、動的配列のサイズを拡張してハンドラを追加します。ClearHandlersメソッドはm_handlers配列を順に処理し、有効なハンドラのShutdownメソッドを呼び出した後、ハンドラオブジェクト自体を削除します(ロガーが所有権を持つ場合)。最後に配列をクリアします。これはリソースを適切にクリーンアップするために非常に重要です。 - 構成(SetGlobalMinLevel、SetExpertInfo)
これらのメソッドはロガーの動作をカスタマイズするために使用されます。SetGlobalMinLevelはグローバルフィルタリング閾値m_global_min_levelを調整し、メッセージが各ハンドラに到達する前にフィルタリングされるようにします。SetExpertInfoはEAのマジックナンバーと名前を設定でき、複数のEAが同時にロギングしている場合でも、ハンドラがログメッセージに自動的にこの情報を含めて識別を容易にします。
//+------------------------------------------------------------------+ //| Log | //+------------------------------------------------------------------+ void CLogger::Log(const LogLevel level, const string origin, const string message) { // Check global level first if(level < m_global_min_level || level >= LOG_LEVEL_OFF) return; datetime current_time = TimeCurrent(); string effective_origin = origin; if(m_expert_name != "") effective_origin = m_expert_name + "::" + origin; // Dispatch to all registered handlers for(int i = 0; i < ArraySize(m_handlers); i++) { ILogHandler *handler = m_handlers[i]; if(CheckPointer(handler) != POINTER_INVALID) { handler.Log(current_time, level, effective_origin, message, m_expert_magic); } } } //+------------------------------------------------------------------+ //| Convenience Logging Methods | //+------------------------------------------------------------------+ void CLogger::Debug(const string origin, const string message) { Log(LOG_LEVEL_DEBUG, origin, message); } void CLogger::Info(const string origin, const string message) { Log(LOG_LEVEL_INFO, origin, message); } void CLogger::Warn(const string origin, const string message) { Log(LOG_LEVEL_WARN, origin, message); } void CLogger::Error(const string origin, const string message) { Log(LOG_LEVEL_ERROR, origin, message); } void CLogger::Fatal(const string origin, const string message) { Log(LOG_LEVEL_FATAL, origin, message); } //+------------------------------------------------------------------+ //| LogFormat | //+------------------------------------------------------------------+ void CLogger::LogFormat(const LogLevel level, const string origin, const string formatted_message) { // Check global level first if(level < m_global_min_level || level >= LOG_LEVEL_OFF) return; Log(level, origin, formatted_message); } //+------------------------------------------------------------------+ //| Convenience Formatted Logging Methods | //+------------------------------------------------------------------+ void CLogger::DebugFormat(const string origin, const string formatted_message) { LogFormat(LOG_LEVEL_DEBUG, origin, formatted_message); } void CLogger::InfoFormat(const string origin, const string formatted_message) { LogFormat(LOG_LEVEL_INFO, origin, formatted_message); } void CLogger::WarnFormat(const string origin, const string formatted_message) { LogFormat(LOG_LEVEL_WARN, origin, formatted_message); } void CLogger::ErrorFormat(const string origin, const string formatted_message) { LogFormat(LOG_LEVEL_ERROR, origin, formatted_message); } void CLogger::FatalFormat(const string origin, const string formatted_message) { LogFormat(LOG_LEVEL_FATAL, origin, formatted_message); } //+------------------------------------------------------------------+
- コアロギングメソッド(Log)
これはログ要求を受け取る中心的なメソッドです。まず、メッセージのレベルがm_global_min_levelを満たしているかを確認します。条件を満たす場合、現在時刻を取得し、m_expert_nameを必要に応じて先頭に付加したeffective_origin文字列を構築します。その後、m_handlers配列を順に処理し、有効な各ハンドラのLogメソッドを呼び出して、タイムスタンプ、レベル、オリジン、メッセージ、EAマジックナンバーを渡します。これにより、ログメッセージはすべてのアクティブな出力先に効率的に送信されます。 - 便利なロギングメソッド(Debug、Info、Warn、Error、Fatal)
これらのpublicメソッドは、特定の重要度レベルでメッセージを簡単にロギングするためのインターフェースを提供します。それぞれのメソッド(たとえばDebugやInfo)は、対応するLogLevel列挙値(LOG_LEVEL_DEBUG、LOG_LEVEL_INFOなど)を指定してメインのLogメソッドを呼び出すだけです。これにより、ユーザーのアプリケーションでログを記録する際に必要なコード量が減ります。 - フォーマット済みロギングメソッド(LogFormat、DebugFormatなど)
これらのメソッドは、すでにフォーマットされたメッセージをログするための代替手段を提供します。LogFormatは事前にフォーマットされたメッセージ文字列を受け取り、メインのLogメソッドを呼び出します。DebugFormat、InfoFormatなどの便利メソッドは、それぞれ適切な重要度レベルでLogFormatを呼び出します。これは、メッセージのフォーマットロジックが複雑で、ロガーを呼び出す前に別の場所で処理される場合に便利です。
CLoggerの実装が完了したので、次は実際に使用例を確認する段階です。
ロギングフレームワークの使用
このEAは、カスタムMQL5ロギングフレームワーク(CLogger、ILogHandler、ConsoleLogHandler、FileLogHandlerで構成)を統合して活用する方法を実践的に示すサンプルです。標準的なEA構造の中で、ロギングコンポーネントのセットアップ、設定、実行中の使用、そしてクリーンアップを順に紹介します。
LoggingExampleEA.mq5の冒頭部分では、標準的なEAのプロパティを設定し、カスタムロギングフレームワークから必要なコンポーネントをインクルードします。
プロパティの後に続く#include文は、ロギング機能を統合するために不可欠です。CLogger.mqhはメインロガークラスの定義を取り込みます。ConsoleLogHandler.mqhはMetaTraderのコンソール([エキスパート]タブ)へのロギングをおこなうクラスを含みます。FileLogHandler.mqhはファイルへのロギングを担当するクラスを含みます。これらのインクルードにより、ヘッダーファイルで定義されたクラスや関数をこのEA内で利用できるようになります。
入力パラメータ(input)
// Input parameters input int MagicNumber = 654321; // EA Magic Number input double LotSize = 0.01; // Fixed lot size input int StopLossPips = 50; // Stop Loss in pips input int TakeProfitPips = 100; // Take Profit in pips input LogLevel ConsoleLogLevel = LOG_LEVEL_INFO; // Minimum level for console output input LogLevel FileLogLevel = LOG_LEVEL_DEBUG; // Minimum level for file output
このセクションでは、ユーザーがEAをチャートに適用する際に設定可能な外部パラメータを定義しています。これらの入力により、EAの売買動作や、特にロギング設定をカスタマイズできます。
- input int MagicNumber = 654321;:この標準的なEAパラメータは、このEAのインスタンスによって発注された注文を識別するために使われます。他のEAや手動注文と区別するのに役立ちます。
- input double LotSize = 0.01;:EAが発注する際に使用する固定ロットサイズを定義します。
- input int StopLossPips = 50;:注文のストップロス幅をピップス単位で設定します。
- input int TakeProfitPips = 100;:注文のテイクプロフィット幅をピップス単位で設定します。
- input LogLevel ConsoleLogLevel = LOG_LEVEL_INFO;:このパラメータは、MetaTraderのExpertsタブ(コンソール)に表示するメッセージの最小重要度をユーザーが選択できるようにします。これはLogLevels.mqhで定義されたLogLevel列挙型を利用しています。デフォルトではLOG_LEVEL_INFOに設定されており、INFO、WARN、ERROR、FATALメッセージはコンソールに表示されますが、DEBUGメッセージは抑制されます。
- input LogLevel FileLogLevel = LOG_LEVEL_DEBUG;:同様に、この入力はログファイルに書き込まれるメッセージの最小重要度を設定します。これもLogLevel列挙型を使用します。デフォルトはLOG_LEVEL_DEBUGで、詳細なデバッグ情報を含むすべてのメッセージがログファイルに保存されます。これにより、通常の運用ではコンソール出力を抑制しつつ、詳細なログを後で分析やトラブルシューティングに利用できるようになります。
これらのロギング専用の入力は、フレームワークが外部から簡単に設定可能であることを示しています。つまり、EAのコードを変更することなく、ログの詳細度を調整できるのです。
// Global logger pointer (optional, can use CLogger::Instance() directly) CLogger *g_logger = NULL;
EAは1つのグローバル変数を宣言しています。 CLogger *g_logger = NULL;:この行は、CLoggerクラスのオブジェクトを指すことができるポインタg_loggerを宣言しています。初期値はNULLであり、最初は有効なオブジェクトを指していません。このグローバルポインタは、シングルトンパターンを通じて取得するCLoggerの単一インスタンスを保持するために使われます(CLogger::Instance()経由で取得)。
ロギングが必要な場所で毎回直接CLogger::Instance()を呼び出すことも可能ですが、OnInit()でインスタンスを取得してこのグローバル変数に保存しておけば、OnTick、OnDeinit、OnChartEventといった他の関数からも簡単にロガーオブジェクトにアクセスできます。つまり、この変数はシングルトンロガーへのキャッシュポインタとして機能します。
OnInit()
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Get the logger instance g_logger = CLogger::Instance(); if(CheckPointer(g_logger) == POINTER_INVALID) { Print("Critical Error: Failed to get Logger instance!"); return(INIT_FAILED); } //--- Set EA information for context in logs g_logger.SetExpertInfo(MagicNumber, MQL5InfoString(MQL5_PROGRAM_NAME)); //--- Configure Handlers --- // 1. Console Handler ConsoleLogHandler *console_handler = new ConsoleLogHandler(ConsoleLogLevel); if(CheckPointer(console_handler) != POINTER_INVALID) { // Optionally customize format // console_handler.SetFormat("[{level}] {message}"); if(!g_logger.AddHandler(console_handler)) { Print("Warning: Failed to add ConsoleLogHandler."); delete console_handler; // Clean up if not added } } else { Print("Warning: Failed to create ConsoleLogHandler."); } // 2. File Handler string log_prefix = MQL5InfoString(MQL5_PROGRAM_NAME) + "_" + IntegerToString(MagicNumber); FileLogHandler *file_handler = new FileLogHandler("MQL5/Logs/EA_Logs", // Directory relative to MQL5/Files log_prefix, // File name prefix FileLogLevel, // Minimum level to log to file "[{time}] {level} ({origin}): {message}", // Format 2048, // Max file size in KB (e.g., 2MB) 10); // Max number of log files to keep if(CheckPointer(file_handler) != POINTER_INVALID) { if(!g_logger.AddHandler(file_handler)) { Print("Warning: Failed to add FileLogHandler."); delete file_handler; // Clean up if not added } } else { Print("Warning: Failed to create FileLogHandler."); } //--- Log initialization message g_logger.Info(__FUNCTION__, "Expert Advisor initialized successfully."); g_logger.Debug(__FUNCTION__, StringFormat("Settings: Lots=%.2f, SL=%d, TP=%d, ConsoleLevel=%s, FileLevel=%s", LotSize, StopLossPips, TakeProfitPips, EnumToString(ConsoleLogLevel), EnumToString(FileLogLevel))); //--- succeed return(INIT_SUCCEEDED); }
この例において、OnInit()はカスタムロギングフレームワークのセットアップと構成において重要な役割を果たします。 OnInit内の最初のステップは、ロガーのシングルトンインスタンスを取得することです。
g_logger = CLogger::Instance();
この静的メソッドは、CLoggerオブジェクトが1つしか存在しないことを保証します。返されたポインタは後で簡単にアクセスできるようにグローバル変数g_loggerに保存されます。その後、CheckPointerを使った基本的なエラーチェックがおこなわれ、インスタンスが正常に取得されたかどうかを確認します。失敗した場合は、致命的なエラーが標準ログに出力され、初期化は失敗(INIT_FAILED)となります。
g_logger.SetExpertInfo(MagicNumber, MQL5InfoString(MQL5_PROGRAM_NAME));
この行は、ロガーを使用するEAに関するコンテキスト情報を設定します。入力パラメータから渡されたMagicNumberと、MQL5InfoString(MQL5_PROGRAM_NAME)で取得されるEAの名前を渡します。この情報は、ハンドラのフォーマット文字列次第でログメッセージに自動的に含めることができ、特に複数のEAが同時に稼働している場合に、特定のEAからのログを識別しやすくします。
ConsoleLogHandlerはnewを使って動的に生成されます。
ConsoleLogHandler *console_handler = new ConsoleLogHandler(ConsoleLogLevel);
これはコンストラクタで直接設定され、ConsoleLogLevel入力パラメータで指定された最小ログレベルが適用されます。コード内にはコメントアウトされた例(console_handler.SetFormat("[{level}] {message}");)も含まれており、必要に応じて作成後に出力フォーマットをカスタマイズできることが示されています。次に、このハンドラはメインロガーに追加されます。if(!g_logger.AddHandler(console_handler))
もしハンドラの追加に失敗した場合(falseが返る)、警告が出力され、生成したハンドラオブジェクトはdeleteで削除され、メモリリークを防ぎます。ハンドラ生成(new)自体に対するエラーチェックも含まれています。
同様に、FileLogHandlerも生成されます。// 2. File Handler string log_prefix = MQL5InfoString(MQL5_PROGRAM_NAME) + "_" + IntegerToString(MagicNumber); FileLogHandler *file_handler = new FileLogHandler("MQL5/Logs/EA_Logs", // Directory relative to MQL5/Files log_prefix, // File name prefix FileLogLevel, // Minimum level to log to file "[{time}] {level} ({origin}): {message}", // Format 2048, // Max file size in KB (e.g., 2MB) 10); // Max number of log files to keep
ログファイルのプレフィックスは、EA名とマジックナンバーを組み合わせて一意に識別できるように構築されます。FileLogHandlerのコンストラクタは複数の引数を取ります。ディレクトリパス(端末のMQL5/Filesディレクトリからの相対パスとして「MQL5/Logs/EA_Logs」)、生成されたプレフィックス、FileLogLevel入力から指定された最小ログレベル、カスタムフォーマット文字列、最大ファイルサイズ(2048 KB = 2MB)、保持するログファイルの最大数(10)です。コンソールハンドラと同様に、g_logger.AddHandler()を使ってロガーに追加され、生成または追加が失敗した場合には同じようにエラーハンドリングとクリーンアップ(delete)がおこなわれます。
ハンドラのセットアップが完了した後、EAは初期化を確認するためにログを出力します。g_logger.Info(__FUNCTION__, \"Expert Advisor initialized successfully.\"); g_logger.Debug(__FUNCTION__, StringFormat(\"Settings: ...\"));
Infoレベルのメッセージは成功を確認するものです。Debugレベルのメッセージでは、StringFormatを使って主要な入力パラメータを記録します。__FUNCTION__は発生元文字列として使用され、現在の関数名(この場合はOnInit)を自動的に提供します。これらのメッセージは、追加されたハンドラによって設定された最小レベルに基づいて処理されます。
最後に、すべての初期化が正常に完了すると、この関数はINIT_SUCCEEDEDを返し、EAがティック処理を開始する準備ができたことを端末に通知します。致命的なエラー(たとえばロガーインスタンスの取得失敗)が発生した場合には、INIT_FAILEDを返します。
OnDeinit()
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Log deinitialization if(CheckPointer(g_logger) != POINTER_INVALID) { string reason_str = "Unknown reason"; switch(reason) { case REASON_ACCOUNT: reason_str = "Account change"; break; case REASON_CHARTCHANGE: reason_str = "Chart symbol or period change"; break; case REASON_CHARTCLOSE: reason_str = "Chart closed"; break; case REASON_PARAMETERS: reason_str = "Input parameters changed"; break; case REASON_RECOMPILE: reason_str = "Recompiled"; break; case REASON_REMOVE: reason_str = "EA removed from chart"; break; case REASON_TEMPLATE: reason_str = "Template applied"; break; case REASON_CLOSE: reason_str = "Terminal closed"; break; } g_logger.Info(__FUNCTION__, "Expert Advisor shutting down. Reason: " + reason_str + " (" + IntegerToString(reason) + ")"); // Release the logger instance (this calls Shutdown() on all handlers) CLogger::Release(); g_logger = NULL; // Set pointer to NULL after release } else { Print("Logger instance was already invalid during Deinit."); } //--- Print to standard log just in case logger failed Print(MQL5InfoString(MQL5_PROGRAM_NAME) + ": Deinitialized. Reason code: " + IntegerToString(reason)); }
LoggingExampleEA.mq5において、OnDeinitはロギングフレームワークを適切にシャットダウンすることに重点を置いています。
if(CheckPointer(g_logger) != POINTER_INVALID)
この関数はまず、グローバルロガーポインタg_loggerがまだ有効かどうかを確認します。これにより、ロガーがすでに解放されていたり、初期化が失敗している場合にOnDeinitが呼ばれてもエラーを防ぐことができます。
ifブロックの中では、OnDeinitに渡された理由コードに対応する人間が読める形式の文字列をswitch文で判定します。これにより、EAが停止する理由に関するコンテキストが提供されます。その後、g_logger.Info()を使って情報メッセージが記録され、判定された理由文字列と元の理由コードがログに含まれます。
string reason_str = "Unknown reason"; switch(reason) { case REASON_ACCOUNT: reason_str = "Account change"; break; case REASON_CHARTCHANGE: reason_str = "Chart symbol or period change"; break; ... ... case REASON_CLOSE: reason_str = "Terminal closed"; break; } g_logger.Info(__FUNCTION__, "Expert Advisor shutting down. Reason: " + reason_str + " (" + IntegerToString(reason) + ")");
これにより、EAの動作終了時の最後の瞬間、すなわち停止理由がログに記録されることが保証されます(コンソールとファイルの両方、設定されたレベルに応じて)。
ここで最も重要なのがロガーのクリーンアップ手順です。
CLogger::Release();
CLoggerクラスの静的メソッドRelease()を呼び出すことで、シングルトンロガーインスタンスの削除が実行されます。破棄の過程では、CLoggerのデストラクタがすべての追加済みハンドラ(この場合はコンソールとファイルのハンドラ)を順に処理し、それぞれのShutdown()メソッドを呼び出します。特にFileLogHandlerでは、これにより開いているログファイルが閉じられます。その後、ハンドラオブジェクト自体も削除されます。これにより、すべてのリソースが正しく解放され、ファイルが適切にクローズされることが保証されます。
次に、グローバルポインタを明示的にNULLにします。
g_logger = NULL;
インスタンスを解放した後にg_loggerをNULLへ戻すことは、もはや有効なオブジェクトを指していないことを明確に示すための良い習慣です。
一方、もしOnDeinitが呼ばれた時点ですでにg_loggerが無効だった場合は、elseブロックが処理され、標準のEAログにメッセージを出力します。さらに、ロガーの仕組み全体が完全に失敗した場合でも、標準ログには必ず終了メッセージが記録されるように、ロガー処理の外で最後のPrint文が実行されます。
この実装は、カスタムロギングフレームワークを正しくシャットダウンする手順を示しており、EAが終了する際にログファイルが確実に閉じられ、リソースが解放されることを保証します。
OnTick()
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Ensure logger is valid if(CheckPointer(g_logger) == POINTER_INVALID) { // Attempt to re-initialize logger if it became invalid unexpectedly // This is defensive coding, ideally it shouldn't happen if OnInit succeeded. Print("Error: Logger instance invalid in OnTick! Attempting re-init..."); if(OnInit() != INIT_SUCCEEDED) { Print("Critical Error: Failed to re-initialize logger in OnTick. Stopping EA."); ExpertRemove(); // Stop the EA return; } } //--- Log tick arrival MqlTick latest_tick; if(SymbolInfoTick(_Symbol, latest_tick)) { g_logger.Debug(__FUNCTION__, StringFormat("New Tick: Time=%s, Bid=%.5f, Ask=%.5f, Volume=%d", TimeToString(latest_tick.time, TIME_DATE|TIME_SECONDS), latest_tick.bid, latest_tick.ask, (int)latest_tick.volume_real)); } else { g_logger.Warn(__FUNCTION__, "Failed to get latest tick info. Error: " + IntegerToString(GetLastError())); } //--- Example Logic: Check for a simple crossover // Note: Use more robust indicator handling in a real EA double ma_fast[], ma_slow[]; int copied_fast = CopyBuffer(iMA(_Symbol, _Period, 10, 0, MODE_SMA, PRICE_CLOSE), 0, 0, 3, ma_fast); int copied_slow = CopyBuffer(iMA(_Symbol, _Period, 50, 0, MODE_SMA, PRICE_CLOSE), 0, 0, 3, ma_slow); if(copied_fast < 3 || copied_slow < 3) { g_logger.Warn(__FUNCTION__, "Failed to copy enough indicator data."); return; // Not enough data yet } // ArraySetAsSeries might be needed depending on how you access indices // ArraySetAsSeries(ma_fast, true); // ArraySetAsSeries(ma_slow, true); bool cross_up = ma_fast[1] > ma_slow[1] && ma_fast[2] <= ma_slow[2]; bool cross_down = ma_fast[1] < ma_slow[1] && ma_fast[2] >= ma_slow[2]; if(cross_up) { g_logger.Info(__FUNCTION__, "MA Cross Up detected. Potential Buy Signal."); // --- Add trading logic here --- // Example: SendBuyOrder(); } else if(cross_down) { g_logger.Info(__FUNCTION__, "MA Cross Down detected. Potential Sell Signal."); // --- Add trading logic here --- // Example: SendSellOrder(); } // Log account info periodically static datetime last_account_log = 0; if(TimeCurrent() - last_account_log >= 3600) // Log every hour { g_logger.Info(__FUNCTION__, StringFormat("Account Update: Balance=%.2f, Equity=%.2f, Margin=%.2f, FreeMargin=%.2f", AccountInfoDouble(ACCOUNT_BALANCE), AccountInfoDouble(ACCOUNT_EQUITY), AccountInfoDouble(ACCOUNT_MARGIN), AccountInfoDouble(ACCOUNT_MARGIN_FREE))); last_account_log = TimeCurrent(); } }
ズームイン...
//--- Ensure logger is valid if(CheckPointer(g_logger) == POINTER_INVALID) { // Attempt to re-initialize logger if it became invalid unexpectedly // This is defensive coding, ideally it shouldn't happen if OnInit succeeded. Print("Error: Logger instance invalid in OnTick! Attempting re-init..."); if(OnInit() != INIT_SUCCEEDED) { Print("Critical Error: Failed to re-initialize logger in OnTick. Stopping EA."); ExpertRemove(); // Stop the EA return; } }
OnDeinitと同様に、この関数はまずCheckPointerを使ってg_loggerポインタが有効かどうかを確認します。防御的な措置として、もしロガーが無効であると判定された場合(本来、OnInitが成功していれば起こらないはずですが)、再初期化を試みるためにOnInit()を呼び出します。もし再初期化にも失敗した場合は、標準のPrint関数を使って重大なエラーをログに記録し、ExpertRemove()を呼び出してEAを停止させます。
さらに、EAはSymbolInfoTick()を使って最新のティック情報の取得を試みます。//--- Log tick arrival MqlTick latest_tick; if(SymbolInfoTick(_Symbol, latest_tick)) { g_logger.Debug(__FUNCTION__, StringFormat("New Tick: Time=%s, Bid=%.5f, Ask=%.5f, Volume=%d", TimeToString(latest_tick.time, TIME_DATE|TIME_SECONDS), latest_tick.bid, latest_tick.ask, (int)latest_tick.volume_real)); } else { g_logger.Warn(__FUNCTION__, "Failed to get latest tick info. Error: " + IntegerToString(GetLastError())); }
成功した場合、Debugレベルのメッセージがログに記録されます。内容はティックのタイムスタンプ、Bid価格、Ask価格、出来高であり、StringFormatを使って整形されます。これにより、受信した価格データの詳細なトレースが得られ、デバッグに役立ちます。SymbolInfoTickが失敗した場合は、Warnレベルのメッセージが記録され、GetLastError()で取得したエラーコードが含まれます。
さらにコードには、移動平均(MA)のクロスオーバーを確認するシンプルな例が含まれています。//--- Example Logic: Check for a simple crossover // Note: Use more robust indicator handling in a real EA double ma_fast[], ma_slow[]; int copied_fast = CopyBuffer(iMA(_Symbol, _Period, 10, 0, MODE_SMA, PRICE_CLOSE), 0, 0, 3, ma_fast); int copied_slow = CopyBuffer(iMA(_Symbol, _Period, 50, 0, MODE_SMA, PRICE_CLOSE), 0, 0, 3, ma_slow); if(copied_fast < 3 || copied_slow < 3) { g_logger.Warn(__FUNCTION__, "Failed to copy enough indicator data."); return; // Not enough data yet } // ArraySetAsSeries might be needed depending on how you access indices // ArraySetAsSeries(ma_fast, true); // ArraySetAsSeries(ma_slow, true); bool cross_up = ma_fast[1] > ma_slow[1] && ma_fast[2] <= ma_slow[2]; bool cross_down = ma_fast[1] < ma_slow[1] && ma_fast[2] >= ma_slow[2]; if(cross_up) { g_logger.Info(__FUNCTION__, "MA Cross Up detected. Potential Buy Signal."); // --- Add trading logic here --- // Example: SendBuyOrder(); } else if(cross_down) { g_logger.Info(__FUNCTION__, "MA Cross Down detected. Potential Sell Signal."); // --- Add trading logic here --- // Example: SendSellOrder(); }
まず、2つのiMAインディケーターからデータをコピーしようとします。もし十分なデータが取得できなかった場合は、Warnレベルのメッセージがログに記録され、関数は処理を終了します。データが利用可能な場合は、直近2本のバーにおける高速MAと低速MAのクロスオーバー条件を確認します。クロスオーバー(cross_upまたはcross_down)が検出された場合、Infoレベルのメッセージがログに記録され、潜在的な取引シグナルを示します。これにより、取引戦略内で重要なイベントをロギングする方法が示されます。
最後に、すべてのティックでログを出力するのではなく、一定間隔で情報をログする例も示しています。
// Log account info periodically static datetime last_account_log = 0; if(TimeCurrent() - last_account_log >= 3600) // Log every hour { g_logger.Info(__FUNCTION__, StringFormat("Account Update: Balance=%.2f, Equity=%.2f, Margin=%.2f, FreeMargin=%.2f", AccountInfoDouble(ACCOUNT_BALANCE), AccountInfoDouble(ACCOUNT_EQUITY), AccountInfoDouble(ACCOUNT_MARGIN), AccountInfoDouble(ACCOUNT_MARGIN_FREE))); last_account_log = TimeCurrent(); }
静的変数last_account_logは、最後にアカウント情報がログに記録された時刻を保持します。コードは、現在時刻 (TimeCurrent())が最後のログ時刻より少なくとも3600秒(1時間)以上経過しているかを確認します。条件を満たす場合、現在の口座残高、エクイティ、証拠金、余剰証拠金を含むInfoレベルのメッセージがログに記録され、last_account_logが更新されます。これにより、同じ情報でログが溢れるのを防ぎつつ、定期的なステータス更新を提供できます。
全体として、OnTick関数はEA実行中にロガーをさまざまな目的で利用する方法を示しています。詳細なデバッグ(ティックごとのDebug)、潜在的な問題に対する警告(データコピー失敗時のWarn)、重要なイベントに関する情報(シグナル発生時のInfo)、および定期的なステータス更新(口座状況のInfo)です。
OnChartEvent()
OnChartEvent()関数は、EAが稼働しているチャート上で発生するさまざまなイベントを処理するためのMQL5イベントハンドラです。これらのイベントには、キーボード操作やマウスの移動、グラフィカルオブジェクトのクリック、EAや他のMQL5プログラムによって生成されたカスタムイベントなどが含まれます。
//+------------------------------------------------------------------+ //| ChartEvent function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- Ensure logger is valid if(CheckPointer(g_logger) == POINTER_INVALID) return; //--- Log chart events string event_name = "Unknown Chart Event"; switch(id) { case CHARTEVENT_KEYDOWN: event_name = "KeyDown"; break; case CHARTEVENT_MOUSE_MOVE: event_name = "MouseMove"; break; // Add other CHARTEVENT cases as needed case CHARTEVENT_OBJECT_CLICK: event_name = "ObjectClick"; break; case CHARTEVENT_CUSTOM+1: event_name = "CustomEvent_1"; break; // Example custom event } g_logger.Debug(__FUNCTION__, StringFormat("Chart Event: ID=%d (%s), lparam=%d, dparam=%.5f, sparam='%s'", id, event_name, lparam, dparam, sparam)); } //+------------------------------------------------------------------+
OnTickおよびOnDeinitと同様に、この関数もまずグローバルロガーポインタg_loggerが有効であることを確認するところから始まります。
if(CheckPointer(g_logger) == POINTER_INVALID) return;
ロガーが無効である場合、関数は単に終了し、それ以上の処理や潜在的なエラーを防ぎます。
関数の核心部分では、発生したイベントの種類を判定し、その詳細をログに記録します。
//--- Log chart events string event_name = "Unknown Chart Event"; switch(id) { case CHARTEVENT_KEYDOWN: event_name = "KeyDown"; break; case CHARTEVENT_MOUSE_MOVE: event_name = "MouseMove"; break; // Add other CHARTEVENT cases as needed case CHARTEVENT_OBJECT_CLICK: event_name = "ObjectClick"; break; case CHARTEVENT_CUSTOM+1: event_name = "CustomEvent_1"; break; // Example custom event } g_logger.Debug(__FUNCTION__, StringFormat("Chart Event: ID=%d (%s), lparam=%d, dparam=%.5f, sparam='%s'", id, event_name, lparam, dparam, sparam));
switch文は、受信した各イベントIDを人間が理解しやすいイベント名に変換します。たとえば、CHARTEVENT_KEYDOWN、CHARTEVENT_MOUSE_MOVE、CHARTEVENT_OBJECT_CLICKなどです。また、ユーザー定義のシグナル(CHARTEVENT_CUSTOM + 1)に反応する方法も示しています。
次に、g_logger.Debug()を使ってDebugレベルのメッセージを出力します。このログには、イベントID、解決されたイベント名、lparam、dparam、sparamのパラメータ値がStringFormatで整形されて記録されます。Debugレベルで情報を保持することで、開発やテスト中にチャート上の操作やカスタムイベントのフローを追跡することができ、非常に有用です。
カスタムロギングフレームワークの利点
私たちのカスタムロギングシステムは、基本的なPrint()関数に比べていくつかの改善点を提供します。
- 重要度によるフィルタリング:優先度に応じて、必要なメッセージだけを表示できます。
- 複数出力:ログをコンソール、ファイル、その他の出力先に同時に送信できます。
- 豊富なコンテキスト:タイムスタンプ、発生元、EAの詳細情報が自動的に付加されます。
- 柔軟なフォーマット:メッセージのレイアウトを読みやすさに合わせて調整できます。
- ファイルローテーション:ログファイルのサイズが無制限に増加するのを防ぎます。
- 集中管理:グローバルまたは個別ハンドラ単位でロギングのオン/オフを制御できます。
これらの機能により、複雑な取引システムのデバッグが格段に効率化されます。問題の特定が迅速になり、挙動の追跡や、実際に重要なデータに集中することが可能になります。
結論
このカスタムロギングフレームワークが導入されれば、Print()文を行き当たりばったりに使う必要がなくなり、明確で文脈豊か、かつ柔軟に調整可能なメッセージでコードを理解できるようになります。重大なエラーは一目で分かり、詳細なトレースは後から参照可能で、ログファイルも整然と保たれます。さらに、このシステムはあなたの使い方に合わせて柔軟に変化します。ハンドラを入れ替えたり、フォーマットを再構築したり、ログの詳細度を自由に調整することも可能です。次の記事では、プロファイリングやユニットテストツールを追加し、パフォーマンスの問題やロジックの不具合を実際のチャートに現れる前に発見できるようにします。これこそが、本物のMQL5の職人技です。
そして、これは旅の第一歩に過ぎないことを覚えておいてください。今後、より高度なデバッグ手法、カスタムプロファイラー、堅牢なユニットテストハーネス、自動化されたコード品質チェックなどが控えています。本連載の最後には、反応的なバグ探しから、規律ある積極的な品質管理体制へと進化できるでしょう。
それまでは、取引とコーディングをお楽しみください。
ファイルの概要
ファイル名 | ファイルの説明 |
---|---|
LogLevels.mqh | フレームワーク全体で使用されるDEBUG→OFF重大度レベルを持つLogLevel列挙を定義します。 |
ILogHandler.mqh | すべての具体的なログ出力クラスが実装するILogHandlerインターフェース(Setup/Log/Shutdown)を宣言します。 |
ConsoleLogHandler.mqh | ILogHandlerを実装して、レベルベースのフィルタリングを使用してフォーマットされたログメッセージをMetaTraderの[エキスパート]タブに出力します。 |
FileLogHandler.mqh | ILogHandlerを実装して、構成可能なファイル履歴を保持しながら、毎日ローテーションするサイズ制限付きファイルにログを書き込みます。 |
CLogger.mqh | ハンドラを保存し、グローバル重大度フィルタリングを適用し、便利なログメソッドを提供するシングルトンロガー |
LoggingExampleEA.mq5 | 実際にカスタムロギングフレームワークを設定、使用、およびシャットダウンする方法を示すサンプルEA |
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/17933
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。




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