English Deutsch
preview
ログレコードをマスターする(第4回):ログをファイルに保存する

ログレコードをマスターする(第4回):ログをファイルに保存する

MetaTrader 5 | 4 6月 2025, 07:35
54 1
joaopedrodev
joaopedrodev

はじめに

本連載最初の記事、「ログレコードをマスターする(第1回):MQL5の基本概念と最初のステップ」では、エキスパートアドバイザー(EA)開発向けのカスタムログライブラリの構築に着手しました。そこで、MetaTrader 5の標準ログの制限を克服し、MQL5の世界に堅牢でカスタマイズ可能な強力なソリューションをもたらすという、この重要なツールを作る動機を探りました。

前回の記事で取り上げた主なポイントを振り返ると、私たちは次のような基本要件をもとにライブラリの基盤を構築しました。

  1. シングルトンによる堅牢な構造で、コードコンポーネント間の一貫性を確保
  2. データベースへの永続的な保存により、詳細な監査や分析を可能にする追跡可能な履歴の実現
  3. 柔軟な出力形式に対応し、コンソール、ファイル、端末、データベースなど多様な出力先をサポート
  4. ログレベルによる分類で、情報メッセージと重大なアラート・エラーを明確に区別
  5. 出力フォーマットのカスタマイズにより、開発者やプロジェクトごとの個別ニーズに対応

このような強固な基盤を構築したことで、私たちのログフレームワークは単なるイベントログではなく、EAの動作をリアルタイムで把握・監視・最適化するための戦略的ツールとしての価値を持つことが明らかになりました。

これまでに、ログの基本を学び、それらの書式を整える方法や、ハンドラによってメッセージの出力先を制御する仕組みについて理解してきました。しかし、これらのログを将来参照するためには、どこに保存すればよいのでしょうか。そこで今回は、ログをファイルに保存する方法について掘り下げていきます。それでは始めましょう。


ログをファイルに保存する理由

ログをファイルに保存することは、システムの信頼性や保守性を高めるうえで欠かせない作業です。たとえば、エキスパートアドバイザー(EA)が数日間動き続けたあとに、突然エラーが発生したとします。そのとき、何が起きたのかを正確に把握できるでしょうか。記録が残っていなければ、それは足りないピースでパズルを解こうとするようなものです。

ログファイルは単なるメッセージの保存手段ではありません。それはシステムの記憶を担う重要な存在です。ここでは、ログをファイルに保存する主な理由を紹介します。

  1. 永続性と履歴

    ファイルに保存されたログは、プログラムの実行が終わったあとでも参照できます。これにより、過去のパフォーマンスを確認したり、システムの挙動を振り返ったり、長期的なパターンを分析したりすることが可能になります。

  2. 監査と透明性

    金融市場のようなクリティカルな分野では、自動売買の判断根拠や履歴を残すことが欠かせません。詳細なログを保存しておけば、監査や説明責任が求められる場面でも有効な証拠として機能します。

  3. 診断とデバッグ

    ログファイルがあれば、特定のエラーの追跡や、重要なイベントの監視、システムの各ステップの動作確認などをスムーズにおこなうことができます。

  4. アクセスの柔軟性

    コンソールやターミナルに表示されるログとは異なり、ログファイルはリモートで参照したり、チーム内で共有したりすることができます。これにより、重要なイベントに関する共通認識を持つことが容易になります。

  5. 自動化と統合

    ログファイルは、自動化ツールや外部分析ツールによって読み取ることができます。これにより、重大な問題が発生したときにアラートを送信したり、パフォーマンスに関するレポートを自動生成したりすることも可能です。

このように、ログをファイルに保存することで、単なる記録を、システム管理・監視・改善の強力なツールへと昇華させることができます。このデータをファイルに保存することの重要性について、これ以上詳しく述べる必要はないでしょう。次のトピックでは、ログのファイル出力機能をライブラリに効率的に実装する方法を見ていきます。

コードの実装に入る前に、ファイルハンドラが提供すべき機能を明確にしておきましょう。以下に、求められる要件を整理します。

  • ディレクトリ、ファイル名、ファイル形式のカスタマイズ

    ユーザーが以下の項目を設定できるようにします。

    • ログを保存するディレクトリのパス
    • ログファイルの名前(識別性と整理性の向上)
    • 出力ファイル形式(.log、.txt、.jsonをサポート)
  • エンコーディングの設定

    ログファイルの文字エンコーディングとして、以下の形式に対応します。

    • UTF-8(推奨)
    • UTF-7、ANSIコードページ(ACP)など、用途に応じた選択が可能
  • ライブラリエラーの報告

    ライブラリ自体の実行中に発生したエラーについても、次のような方法で報告できるようにします。

    • エラーメッセージをターミナルコンソールに直接表示
    • 問題の特定と修正を支援する、明確でわかりやすい情報の出力


MQL5におけるファイル操作

MQL5でファイルを扱うには、この言語がどのようにファイル操作をおこなっているのか、基本的な理解が必要です。もし、読み書きや各種フラグの詳細な使い方までしっかり学びたいのであれば、「MQL5プログラミングの基礎:ファイル」(Dmitry Fedoseev氏著)を強くおすすめします。複雑な内容であっても、その本質を損なうことなく、分かりやすく丁寧に解説されています。

ただし、この記事では、もっと実践的で直接的な内容に焦点を当てます。細かい部分に立ち入ることはしません。ここでの目的は、ファイルを開く・操作する・閉じるという基本的な流れを、シンプルかつ実践的に学んでいただくことです。

  1. MQL5のファイルディレクトリー:MQL5では、標準のファイル操作関数を使う場合、ファイルはすべてターミナルのインストールディレクトリ内にあるMQL5/Filesフォルダに自動的に保存されます。そのため、ファイルパスを指定する際には、このフォルダからの相対パスを記述すれば十分で、フルパスを明記する必要はありません。たとえば、logs/expert.logに保存する場合、実際のファイルパスは以下のようになります。

    <terminal folder>/MQL5/Files/logs/expert.log

  1. ファイルの作成とオープン:ファイルを作成・または開くにはFileOpen関数を使います。この関数は、必須引数として、ファイルパス(MQL5/Filesからの相対パス)とフラグ(ファイルをどのように扱うかを指定)を受け取ります。使用するフラグは以下の通りです。

    • FILE_READ:ファイルを読み取り用に開く
    • FILE_WRITE:ファイルを書き込み用に開く
    • FILE_ANSI:ANSI形式(1文字=1バイト)で文字列を書き込む

    MQL5の便利な点のひとつは、FILE_READとFILE_WRITEを同時に指定すると、ファイルが存在しない場合に自動で作成されることです。これにより、ファイルの存在確認をおこなう必要がなくなります。

  2. ファイルのクローズ:最後に、ファイル操作が終わったら、FileClose()関数を使って、ファイルを必ず閉じるようにします。

以下は、ファイルを開き(もしくは作成する)、書き込み/読み取りをおこない、閉じるという一連の操作をおこなうMQL5のコード例です。

int OnInit()
  {
   //--- Open the file and store the handler
   int handle_file = FileOpen("logs\\expert.log", FILE_READ|FILE_WRITE|FILE_ANSI, '\t', CP_UTF8);
   
   //--- If opening fails, display an error in the terminal log
   if(handle_file == INVALID_HANDLE)
     {
      Print("[ERROR] Unable to open log file. Ensure the directory exists and is writable. (Code: "+IntegerToString(GetLastError())+")");
      return(INIT_FAILED);
     }
   
   //--- Close file
   FileClose(handle_file);
   
   return(INIT_SUCCEEDED);
  }

ファイルを開いたので、続いて、ファイルへの書き込み方法を見ていきます。

  1. 書き込み位置の指定データを書き込む前に、どこに書き込むかを指定する必要があります。そのために使用するのがFileSeek関数です。この関数で、書き込みポインタをファイルの末尾に移動させることで、既存の内容を上書きせずに追記することができます。
  2. データの書き込み:書き込みにはFileWrite関数を使用します。この関数は、文字列をファイルに出力します。行の区切りとして「\n」を使う必要はありません。FileWriteを使用すると、書き込むたびに自動的に改行されるため、次のデータは常に新しい行に追加されます。そのため、ログが見やすく整理された形で保存されます。

これを実際におこなう方法は以下の通りです。

int OnInit()
  {
   //--- Open the file and store the handler
   int handle_file = FileOpen("logs\\expert.log", FILE_READ|FILE_WRITE|FILE_ANSI, '\t', CP_UTF8);
   
   //--- If opening fails, display an error in the terminal log
   if(handle_file == INVALID_HANDLE)
     {
      Print("[ERROR] Unable to open log file. Ensure the directory exists and is writable. (Code: "+IntegerToString(GetLastError())+")");
      return(INIT_FAILED);
     }
   
   //--- Move the writing pointer
   FileSeek(handle_file, 0, SEEK_END);
   
   //--- Writes the content inside the file
   FileWrite(handle_file, "[2025-01-02 12:35:27] DEBUG (CTradeManager): Order sent successfully, server responded in 32ms");
   
   //--- Close file
   FileClose(handle_file);
   
   return(INIT_SUCCEEDED);
  }

コードを実行すると、Filesフォルダにファイルが作成されます。フルパスは次のようになります。

<Terminal folder>/MQL5/Files/logs/expert.log

そのファイルを開くと、私たちが書いたとおりのことが書かれています。

[2025-01-02 12:35:27] DEBUG (CTradeManager): Order sent successfully, server responded in 32ms

さて、MQL5で非常に簡単な方法でファイルを扱う方法を学んだので、ファイルの保存を担当するハンドラークラス、CLogifyHandlerFileに追加してみましょう。


CLogifyHandlerFileクラスの設定

ここでは、前のセクションで説明した「ファイルローテーション」の要件を効率的に処理するために、CLogifyHandlerFileクラスをどのように設定するかを詳しく見ていきます。ところで、「ファイルローテーション」とは具体的に何を意味するのでしょうか。少し掘り下げて説明します。ログファイルのローテーションとは、1つのログファイルが延々と大きくなり続けることを防ぐための仕組みです。ログが1つのファイルに延々と書き込まれていくと、ファイルサイズが膨大になり、読み込みや分析が困難になってしまいます。ログが巨大化することで、パフォーマンスの低下や可読性の喪失といった問題が発生しやすくなります。

たとえば、EAが数週間〜数ヶ月にわたり稼働し続け、すべてのイベントやエラー、通知を1つのファイルに記録していたとします。やがてそのログは巨大になり、情報を読み解くことが非常に困難になります。そこで登場するのが「ローテーション」です。ローテーションによってログを小さく整理された単位に分割することで、後からの確認や分析が圧倒的にしやすくなります。

ローテーションには主に以下の2つの方法があります。

  1. サイズによるローテーション:ファイルサイズの上限(通常はMB単位)を設定し、その上限に達するごとに新しいログファイルを自動で作成します。この方法は、スケジュールに縛られず、ログの肥大化を防ぐのに適しています。ファイルがサイズ上限(メガバイト単位)に達すると、現在のログファイルは「log1.log」のようにインデックス付きで名前変更されます。ディレクトリ内の既存ファイルも順に番号が繰り上がり、たとえば「log1.log」は「log2.log」に名前変更されます。保存できるファイル数の上限に達している場合は、最も古いファイルが削除されます。この方法は、ログが占有するディスク容量やファイル数を制御したい場合に非常に便利です。
  2. 日付によるローテーション:この方法では、毎日新しいログファイルが作成され、ファイル名に日付が含まれます。それぞれのログの名前には、作成された日付が含まれています(例:log_2025-01-19.log)。この方式は、特定の日のログを確認したいときに非常に便利で、巨大な1ファイルに迷い込むことがありません。私自身も、EAのログ保存にはこの方法をもっともよく使っています。整理されたログは確認や管理がしやすく、作業効率を高めます。

さらに、保存するログファイルの数を制限することもできます。この管理は、古いログが不必要に蓄積されるのを防ぐために非常に重要です。たとえば「最新30ファイルのみ保持」と設定すれば、31個目のファイルが生成された際に、システムが自動で最も古いログを削除します。

もうひとつ重要なのは、キャッシュの使用です。ログメッセージを受け取るたびに都度ディスクへ書き込むのではなく、一時的にメモリ上のキャッシュに保持しておきます。そしてキャッシュが一定サイズに達した時点で、まとめて一括でファイルに書き出します。その結果、ディスクへの読み書きの回数が減ってパフォーマンスが向上し、ストレージデバイスの寿命が延びます。

ファイルローテーションの概念を理解したところで、CLogifyHandlerFileクラスで使用する各種設定をまとめて保持する構造体MqlLogifyHandleFileConfigを作成します。この構造体には、ログの管理に関するすべてのパラメータを格納します。

最初の部分では、使用するローテーションタイプとファイル拡張子の列挙型を定義します。

//+------------------------------------------------------------------+
//| ENUMS for log rotation and file extension                        |
//+------------------------------------------------------------------+
enum ENUM_LOG_ROTATION_MODE
  {
   LOG_ROTATION_MODE_NONE = 0,       // No rotation
   LOG_ROTATION_MODE_DATE,           // Rotate based on date
   LOG_ROTATION_MODE_SIZE,           // Rotate based on file size
  };
enum ENUM_LOG_FILE_EXTENSION
  {
   LOG_FILE_EXTENSION_TXT = 0,       // .txt file
   LOG_FILE_EXTENSION_LOG,           // .log file
   LOG_FILE_EXTENSION_JSON,          // .json file
  };

MqlLogifyHandleFileConfig構造体自体には以下のパラメータが含まれます。

  • directory:ログファイルが保存されるディレクトリ
  • base_filename:拡張子を除いた基本ファイル名
  • file_extension:ログファイルの拡張子タイプ(.txt、.log、.jsonなど)
  • rotation_mode:ファイルローテーションモード
  • messages_per_flush:ファイルに書き込む前にキャッシュするログメッセージの数
  • codepageログファイルに使用されるエンコーディング(UTF-8やANSIなど)
  • max_file_size_mb:ローテーションがサイズに基づいておこなわれる場合の、各ログファイルの最大サイズ
  • max_file_count:最も古いログファイルを削除する前に保持するログファイルの最大数

コンストラクタおよびデストラクタに加えて、各ローテーションモードを設定するための補助メソッドをこの構造体に追加します。これらのメソッドは、設定処理をより実用的かつ信頼性の高いものにするために設計されています。ただ見た目を整えるためのものではなく、設定時に重要なポイントを見落とさないようにする役割があります。

たとえば、ローテーションモードが日付(LOG_ROTATION_MODE_DATE)に設定されている場合に、max_file_size_mb属性を設定しようとするのは意味がありません。このパラメータはサイズモード(LOG_ROTATION_MODE_SIZE)でのみ有効だからです。こうした矛盾を回避するために補助メソッドが存在し、無効な構成からシステムを守ります。

また、万が一必須のパラメータが指定されなかった場合でも、システムが自動的にデフォルト値を設定し、開発者に警告を出すことで、流れが途切れることなく、予期せぬ不具合を防ぐことができます。

実装予定の補助メソッドは以下のとおりです。

  • CreateNoRotationConfig:ファイルのローテーションをおこなわない構成(すべてのログを1つのファイルに記録)
  • CreateDateRotationConfig:日付によるローテーションの設定
  • CreateSizeRotationConfig:ファイルサイズによるローテーションの設定
  • ValidateConfig:すべての設定が正しく構成され、使用準備が整っているかを検証するメソッド(ライブラリ内部で自動的に使用され、開発者が直接呼び出すことは想定していない)

以下は、その構造体の完全な実装です。

//+------------------------------------------------------------------+
//| Struct: MqlLogifyHandleFileConfig                                |
//+------------------------------------------------------------------+
struct MqlLogifyHandleFileConfig
  {
   string directory;                         // Directory for log files
   string base_filename;                     // Base file name
   ENUM_LOG_FILE_EXTENSION file_extension;   // File extension type
   ENUM_LOG_ROTATION_MODE rotation_mode;     // Rotation mode
   int messages_per_flush;                   // Messages before flushing
   uint codepage;                            // Encoding (e.g., UTF-8, ANSI)
   ulong max_file_size_mb;                   // Max file size in MB for rotation
   int max_file_count;                       // Max number of files before deletion
   
   //--- Default constructor
   MqlLogifyHandleFileConfig(void)
     {
      directory = "logs";                    // Default directory
      base_filename = "expert";              // Default base name
      file_extension = LOG_FILE_EXTENSION_LOG;// Default to .log extension
      rotation_mode = LOG_ROTATION_MODE_SIZE;// Default size-based rotation
      messages_per_flush = 100;              // Default flush threshold
      codepage = CP_UTF8;                    // Default UTF-8 encoding
      max_file_size_mb = 5;                  // Default max file size in MB
      max_file_count = 10;                   // Default max file count
     }

   //--- Destructor
   ~MqlLogifyHandleFileConfig(void)
     {
     }

   //--- Create configuration for no rotation
   void CreateNoRotationConfig(string base_name="expert", string dir="logs", ENUM_LOG_FILE_EXTENSION extension=LOG_FILE_EXTENSION_LOG, int msg_per_flush=100, uint cp=CP_UTF8)
     {
      directory = dir;
      base_filename = base_name;
      file_extension = extension;
      rotation_mode = LOG_ROTATION_MODE_NONE;
      messages_per_flush = msg_per_flush;
      codepage = cp;
     }

   //--- Create configuration for date-based rotation
   void CreateDateRotationConfig(string base_name="expert", string dir="logs", ENUM_LOG_FILE_EXTENSION extension=LOG_FILE_EXTENSION_LOG, int max_files=10, int msg_per_flush=100, uint cp=CP_UTF8)
     {
      directory = dir;
      base_filename = base_name;
      file_extension = extension;
      rotation_mode = LOG_ROTATION_MODE_DATE;
      messages_per_flush = msg_per_flush;
      codepage = cp;
      max_file_count = max_files;
     }

   //--- Create configuration for size-based rotation
   void CreateSizeRotationConfig(string base_name="expert", string dir="logs", ENUM_LOG_FILE_EXTENSION extension=LOG_FILE_EXTENSION_LOG, ulong max_size=5, int max_files=10, int msg_per_flush=100, uint cp=CP_UTF8)
     {
      directory = dir;
      base_filename = base_name;
      file_extension = extension;
      rotation_mode = LOG_ROTATION_MODE_SIZE;
      messages_per_flush = msg_per_flush;
      codepage = cp;
      max_file_size_mb = max_size;
      max_file_count = max_files;
     }
   
   //--- Validate configuration
   bool ValidateConfig(string &error_message)
     {
      //--- Saves the return value
      bool is_valid = true;
      
      //--- Check if the directory is not empty
      if(directory == "")
        {
         directory = "logs";
         error_message = "The directory cannot be empty.";
         is_valid = false;
        }
      
      //--- Check if the base filename is not empty
      if(base_filename == "")
        {
         base_filename = "expert";
         error_message = "The base filename cannot be empty.";
         is_valid = false;
        }
      
      //--- Check if the number of messages per flush is positive
      if(messages_per_flush <= 0)
        {
         messages_per_flush = 100;
         error_message = "The number of messages per flush must be greater than zero.";
         is_valid = false;
        }
      
      //--- Check if the codepage is valid (verify against expected values)
      if(codepage != CP_ACP
      && codepage != CP_MACCP
      && codepage != CP_OEMCP
      && codepage != CP_SYMBOL
      && codepage != CP_THREAD_ACP
      && codepage != CP_UTF7
      && codepage != CP_UTF8)
        {
         codepage = CP_UTF8;
         error_message = "The specified codepage is invalid.";
         is_valid = false;
        }
      
      //--- Validate limits for size-based rotation
      if(rotation_mode == LOG_ROTATION_MODE_SIZE)
        {
         if(max_file_size_mb <= 0)
           {
            max_file_size_mb = 5;
            error_message = "The maximum file size (in MB) must be greater than zero.";
            is_valid = false;
           }
         if(max_file_count <= 0)
           {
            max_file_count = 10;
            error_message = "The maximum number of files must be greater than zero.";
            is_valid = false;
           }
        }
      
      //--- Validate limits for date-based rotation
      if(rotation_mode == LOG_ROTATION_MODE_DATE)
        {
         if(max_file_count <= 0)
           {
            max_file_count = 10;
            error_message = "The maximum number of files for date-based rotation must be greater than zero.";
            is_valid = false;
           }
        }
   
      //--- No errors found
      error_message = "";
      return(is_valid);
     }
  };
//+------------------------------------------------------------------+

ここで特に注目していただきたいのは、ValidateConfig()関数の動作です。この関数を見てみると興味深いのは、設定値に誤りが見つかったときに、ただ単に「失敗した」としてfalseを即座に返すのではない点です。そうではなく、まず先に自動的な是正処理をおこない、問題を解消しようと試みます。そして、すべての対応を終えてから最終的な結果を返すのです。

まず最初に、この関数は無効な値を検出すると、それをデフォルト値にリセットします。これにより、設定全体が一時的に「正常」な状態に戻され、プログラムの流れが止まることを防ぎます。次に、ただ修正するだけでなく、どの設定に問題があったのか、どのように修正されたのかを明確に示す説明メッセージを用意します。そして最後に、is_valid変数をfalseに設定し、問題があったことをしっかりと記録します。これらの処理をすべておこなったうえで、最終的にこの変数を返し、設定が有効かどうかを判断させます。

さらに優れているのは、ValidateConfigが複数のエラーを同時に処理できる点です。最初に見つかったエラーだけに対応して終わるのではなく、すべての不正な設定を一括で検査し、それぞれに対して修正をおこないます。最終的に返されるメッセージには、最後に検出されたエラーの情報が含まれており、見落としを防ぐよう設計されています。

このようなアプローチは非常に実用的で、開発者にとって大きな助けになります。システム開発では、設定値が誤って定義されてしまうことはよくあります。しかし、ここで注目すべきは、この関数がプログラマーがひとつひとつのエラーに気づくのを待つのではなく、自動的にそれらを修正してくれるという点です。小さなエラーでも放置しておけば、たとえばログ記録の保存に失敗するなど、より大きな問題へとつながりかねません。私が実装したこのエラー処理の自動化機構は、まさにそういった小さなミスがシステム全体の動作を止めるのを防ぎ、安定した運用を実現するための一助となっています。


CLogifyHandlerFileクラスの実装

前回の記事で既に作成したクラスを、機能するように修正していきます。ここでは、各調整点を詳しく説明し、動作の仕組みを理解していただくことを目的としています。

クラスのprivateスコープスコープには、以下の変数や補助メソッドを追加します。

  1. 構成:構成情報を保持するために、MqlLogifyHandleFileConfig型の変数m_configを用意します。これによりロギングシステムの設定を管理します。
  2. クラスの設定を外部から定義・取得できるように、SetConfig()メソッドとGetConfig()メソッドを実装します。

以下に、基本的な定義とメソッドを含むクラスの初期構造を示します。

//+------------------------------------------------------------------+
//| class : CLogifyHandlerFile                                       |
//|                                                                  |
//| [PROPERTY]                                                       |
//| Name        : CLogifyHandlerFile                                 |
//| Heritage    : CLogifyHandler                                     |
//| Description : Log handler, inserts data into file, supports      |
//| rotation modes.                                                  |
//|                                                                  |
//+------------------------------------------------------------------+
class CLogifyHandlerFile : public CLogifyHandler
  {
private:
   //--- Config
   MqlLogifyHandleFileConfig m_config;
   
public:
   //--- Configuration management
   void              SetConfig(MqlLogifyHandleFileConfig &config);
   MqlLogifyHandleFileConfig GetConfig(void);
  };
//+------------------------------------------------------------------+
//| Set configuration                                                |
//+------------------------------------------------------------------+
void CLogifyHandlerFile::SetConfig(MqlLogifyHandleFileConfig &config)
  {
   m_config = config;
   
   //--- Validade
   string err_msg = "";
   if(!m_config.ValidateConfig(err_msg))
     {
      Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Log system error: "+err_msg);
     }
  }
//+------------------------------------------------------------------+
//| Get configuration                                                |
//+------------------------------------------------------------------+
MqlLogifyHandleFileConfig CLogifyHandlerFile::GetConfig(void)
  {
   return(m_config);
  }
//+------------------------------------------------------------------+

補助メソッドの一覧と、それぞれの動作について詳しく説明します。ファイル管理において便利な3つのメソッドを実装しました。

  1. LogFileExtensionToStr:このメソッドは、ENUM_LOG_FILE_EXTENSION列挙型の値を、ファイル拡張子を表す文字列に変換します。この列挙型では、.log、.txt、.jsonといったファイルタイプの選択肢が定義されており、それに応じた適切な文字列が返されます。

    //+------------------------------------------------------------------+
    //| Convert log file extension enum to string                        |
    //+------------------------------------------------------------------+
    string CLogifyHandlerFile::LogFileExtensionToStr(ENUM_LOG_FILE_EXTENSION file_extension)
      {
       switch(file_extension)
         {
          case LOG_FILE_EXTENSION_LOG:
            return(".log");
          case LOG_FILE_EXTENSION_TXT:
            return(".txt");
          case LOG_FILE_EXTENSION_JSON:
            return(".json");
         }
       //--- Default return
       return(".txt");
      }
    //+------------------------------------------------------------------+

  2. LogPath:この関数は、現在のクラス設定に基づいてログファイルのフルパスを生成する役割を担っています。まず、LogFileExtensionToStr関数を使って、設定されているファイル拡張子を文字列に変換します。次に、設定されているローテーションの種類を確認します。ローテーションが「サイズによる」または「ローテーションなし」に設定されている場合は、指定されたディレクトリ内のファイル名のみを返します。一方、「日付による」に設定されている場合は、ファイル名の先頭に現在の日付(YYYY-MM-DD形式)を追加して返します。

    //+------------------------------------------------------------------+
    //| Generate log file path based on config                           |
    //+------------------------------------------------------------------+
    string CLogifyHandlerFile::LogPath(void)
      {
       string file_extension = this.LogFileExtensionToStr(m_config.file_extension);
       string base_name = m_config.base_filename + file_extension;
       
       if(m_config.rotation_mode == LOG_ROTATION_MODE_SIZE || m_config.rotation_mode == LOG_ROTATION_MODE_NONE)
         {
          return(m_config.directory + "\\\\" + base_name);
         }
       else if(m_config.rotation_mode == LOG_ROTATION_MODE_DATE)
         {
          MqlDateTime date;
          TimeCurrent(date);
          string date_str = IntegerToString(date.year) + "-" + IntegerToString(date.mon, 2, '0') + "-" + IntegerToString(date.day, 2, '0');
          base_name = date_str + (m_config.base_filename != "" ? "-" + m_config.base_filename : "") + file_extension;
          return(m_config.directory + "\\\\" + base_name);
         }
       
       //--- Default return
       return(base_name);
      }
    //+------------------------------------------------------------------+

Emit()メソッドは、ログメッセージをファイルに記録する役割を担います。現時点の実装では、メッセージをターミナルのコンソールに表示するだけの簡易的な処理になっています。これを改善し、ログファイルを開いて、整形されたデータを新しい行として追加し、書き込み後にファイルを閉じるようにします。ファイルを開くことができなかった場合には、コンソールにエラーメッセージを表示して、ログの記録に失敗したことを通知します。

void CLogifyHandlerFile::Emit(MqlLogifyModel &data)
  {
   //--- Checks if the configured level allows
   if(data.level >= this.GetLevel())
     {
      //--- Get the full path of the file
      string log_path = this.LogPath();
      
      //--- Open file
      ResetLastError();
                  int handle_file = FileOpen(log_path, FILE_READ|FILE_WRITE|FILE_ANSI, '\t', m_config.codepage);
                  if(handle_file == INVALID_HANDLE)
                    {
                     Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Log system error: Unable to open log file '"+log_path+"'. Ensure the directory exists and is writable. (Code: "+IntegerToString(GetLastError())+")");
                     return;
                    }
      
      //--- Write
      FileSeek(handle_file, 0, SEEK_END);
      FileWrite(handle_file, data.formated);
      
      //--- Close file
      FileClose(handle_file);
     }
  }

それでは、ログをファイルに書き込むという最もシンプルなバージョンのクラスが完成したので、基本的な動作が正しくおこなわれているかを確認するために、いくつか簡単なテストを実行してみましょう。


ファイルを使った最初のテスト

ここでは、前回の例でも使用したテスト用ファイルLogifyTest.mqhを引き続き利用します。目的は、ログ記録システムをファイルに保存するように構成し、CLogifyの基本クラスと、今回実装したファイルハンドラーを組み合わせて動作を確認することです。

  1. まず、ファイルハンドラ専用の設定を保存するために、MqlLogifyHandleFileConfig型の変数を作成します。
  2. そして、このハンドラに対して使用するフォーマットやルール(たとえば、ファイルサイズによるローテーションなど)を設定します。
  3. 次に、このハンドラをCLogifyの基本クラスに追加します。
  4. さらに、ログファイル内で各レコードがどのように表示されるかを決定するフォーマッターも設定します。

以下がその完全なコードです。

//+------------------------------------------------------------------+
//| Import CLogify                                                   |
//+------------------------------------------------------------------+
#include <Logify/Logify.mqh>
CLogify logify;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Configs
   MqlLogifyHandleFileConfig m_config;
   m_config.CreateSizeRotationConfig("expert","logs",LOG_FILE_EXTENSION_LOG,5,5,10);
   
   //--- Handler File
   CLogifyHandlerFile *handler_file = new CLogifyHandlerFile();
   handler_file.SetConfig(m_config);
   handler_file.SetLevel(LOG_LEVEL_DEBUG);
   
   //--- Add handler in base class
   logify.AddHandler(handler_file);
   logify.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname}] {msg}"));
   
   //--- Using logs
   logify.Debug("RSI indicator value calculated: 72.56", "Indicators", "Period: 14");
   logify.Infor("Buy order sent successfully", "Order Management", "Symbol: EURUSD, Volume: 0.1");
   logify.Alert("Stop Loss adjusted to breakeven level", "Risk Management", "Order ID: 12345678");
   logify.Error("Failed to send sell order", "Order Management", "Reason: Insufficient balance");
   logify.Fatal("Failed to initialize EA: Invalid settings", "Initialization", "Missing or incorrect parameters");
   
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

上記のコードを実行すると、設定されたディレクトリ(logs)に新しいログファイルが作成されます。これは、ナビゲータで確認できます。

メモ帳やテキストエディタでファイルを開くと、メッセージテストによって生成された内容が表示されます。

改善に進む前に、まずパフォーマンステストをおこないます。これにより、今後おこなう最適化がどれほど効果的なのかを比較するための基準が得られます。テスト方法としては、OnTick()関数の中にログ出力処理を追加します。つまり、ティックが発生するたびにログファイルを開いて、メッセージを1行書き込み、すぐに閉じるという処理を繰り返します。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   //--- Logs
   logify.Debug("Debug Message");
  }
//+------------------------------------------------------------------+

ストラテジーテスターを使ってこのテストを実行します。バックテスト中であっても、ファイル作成機能は通常どおり動作しますが、生成されたファイルは通常のログ保存フォルダとは異なる場所に保存されます。この保存先については、後ほどどこで確認できるかを詳しく説明します。今回のテストは、以下の設定で実施します。

[1分足のOHLC]モデリングを使用し、EURUSD銘柄で7日間のテストをおこなったところ、テスト完了までに5分11秒かかりました。この間、すべてのティックごとにログの新しいレコードを生成し、即座にファイルへ保存していました。


JSONファイルを使ったテスト

最後に、JSONログファイルの実際の使い方を示したいと思います。特定のシナリオでは非常に役立ちます。JSON形式で保存するには、設定でファイルタイプを変更し、JSON形式に適したフォーマッターを定義するだけです。以下に実装例を示します。

//+------------------------------------------------------------------+
//| Import CLogify                                                   |
//+------------------------------------------------------------------+
#include <Logify/Logify.mqh>
CLogify logify;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Configs
   MqlLogifyHandleFileConfig m_config;
   m_config.CreateSizeRotationConfig("expert","logs",LOG_FILE_EXTENSION_JSON,5,5,10);
   
   //--- Handler File
   CLogifyHandlerFile *handler_file = new CLogifyHandlerFile();
   handler_file.SetConfig(m_config);
   handler_file.SetLevel(LOG_LEVEL_DEBUG);
   
   //--- Add handler in base class
   logify.AddHandler(handler_file);
   logify.SetFormatter(new CLogifyFormatter("hh:mm:ss","{\\"datetime\\":\\"{date_time}\\", \\"level\\":\\"{levelname}\\", \\"msg\\":\\"{msg}\\"}"));
   
   //--- Using logs
   logify.Debug("RSI indicator value calculated: 72.56", "Indicators", "Period: 14");
   logify.Infor("Buy order sent successfully", "Order Management", "Symbol: EURUSD, Volume: 0.1");
   logify.Alert("Stop Loss adjusted to breakeven level", "Risk Management", "Order ID: 12345678");
   logify.Error("Failed to send sell order", "Order Management", "Reason: Insufficient balance");
   logify.Fatal("Failed to initialize EA: Invalid settings", "Initialization", "Missing or incorrect parameters");
   
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

同じログメッセージで、以下はチャート上でEAを実行した後のファイルの結果です。

{"datetime":"08:24:10", "level":"DEBUG", "msg":"RSI indicator value calculated: 72.56"}
{"datetime":"08:24:10", "level":"INFOR", "msg":"Buy order sent successfully"}
{"datetime":"08:24:10", "level":"ALERT", "msg":"Stop Loss adjusted to breakeven level"}
{"datetime":"08:24:10", "level":"ERROR", "msg":"Failed to send sell order"}
{"datetime":"08:24:10", "level":"FATAL", "msg":"Failed to initialize EA: Invalid settings"}


結論

本記事では、基本的なファイル操作(ファイルのオープン、内容の操作、クローズ)をシンプルに実装する方法を実践的に解説しました。また、ハンドラ構造の設定が重要であることを説明しました。この設定により、ファイルタイプ(テキスト、ログ、JSONなど)や保存先ディレクトリを柔軟に変更可能となり、ライブラリの汎用性が向上します。

さらに、CLogifyHandlerFileクラスを改良し、各メッセージを直接ログファイルに記録できるようにしました。実装後、パフォーマンステストをおこない、ソリューションの効率を評価しました。テストシナリオとしては、EURUSDの取引戦略を1週間分シミュレートし、新しいマーケットティックごとにログを記録しました。資産価格の変動ごとにファイルに新しい行を追加するため、非常にI/O集約的なテストとなっています。

最終的に、全処理は5分11秒で完了しました。この結果は、次回の記事で実装予定のキャッシュシステム(一時メモリ)の基準値として活用します。キャッシュの目的は、レコードを一時的に保存することでファイルへの頻繁なアクセスを減らし、全体のパフォーマンスを向上させることにあります。

次回は、システムの効率とパフォーマンスをさらに高めるための高度な手法を解説します。それでは、次回お会いしましょう。

ファイル名
詳細
Experts/Logify/LogiftTest.mq5
ライブラリの機能をテストするファイル。実用的な例が含まれています。
Include/Logify/Formatter/LogifyFormatter.mqh
ログレコードのフォーマット、プレースホルダーを特定の値に置き換えるクラス
Include/Logify/Handlers/LogifyHandler.mqh
レベル設定やログ送信を含むログハンドラを管理するための基本クラス
Include/Logify/Handlers/LogifyHandlerConsole.mqh
フォーマットされたログをMetaTraderの端末コンソールに直接送信するログハンドラ
Include/Logify/Handlers/LogifyHandlerDatabase.mqh
フォーマットされたログをデータベースに送信するログハンドラ(現在は出力のみが含まれているが、すぐに実際のSQLiteデータベースに保存する予定)
Include/Logify/Handlers/LogifyHandlerFile.mqh
フォーマットされたログをファイルに送るログハンドラ
Include/Logify/Logify.mqh
ログ管理、レベル、モデル、フォーマットの統合のためのコアクラス
Include/Logify/LogifyLevel.mqh
Logifyライブラリのログレベルを定義するファイル。詳細な制御が可能
Include/Logify/LogifyModel.mqh
レベル、メッセージ、タイムスタンプ、コンテキストなどの詳細を含むログレコードをモデル化する構造

MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/16986

添付されたファイル |
Logify0Part40.zip (14.21 KB)
最後のコメント | ディスカッションに移動 (1)
Alpha Dolcy
Alpha Dolcy | 29 1月 2025 において 13:29
特にバックテストや最適化には価値がありそうだ。
逆フェアバリューギャップ取引戦略 逆フェアバリューギャップ取引戦略
逆フェアバリューギャップ(IFVG)とは、価格が過去に特定されたフェアバリューギャップ(FVG)へ回帰した際に、通常想定されるサポートまたはレジスタンスとしての反応を示さず、その水準を無視して通過してしまう現象を指します。このような失敗は、市場の方向性の変調を示すサインである可能性があり、逆張り志向の取引アプローチにおいて優位性をもたらすシグナルとなることがあります。本記事では、MetaTrader 5エキスパートアドバイザー(EA)の戦略として、この逆フェアバリューギャップを定量的に捉え、取引ロジックに組み込むために私が独自に開発したアプローチを紹介します。
MQL5で自己最適化エキスパートアドバイザーを構築する(第4回):動的なポジションサイズ調整 MQL5で自己最適化エキスパートアドバイザーを構築する(第4回):動的なポジションサイズ調整
アルゴリズム取引を成功させるには、継続的かつ学際的な学習が必要です。しかし、その可能性は無限であるがゆえに、明確な成果が得られないまま、何年もの努力を費やしてしまうこともあります。こうした課題に対応するため、私たちは徐々に複雑さを導入するフレームワークを提案します。これにより、トレーダーは不確実な結果に対して無限の時間を費やすのではなく、戦略を反復的に洗練させることが可能になります。
アンサンブル学習におけるゲーティングメカニズム アンサンブル学習におけるゲーティングメカニズム
この記事では、アンサンブルモデルの検討をさらに進め、「ゲート」という概念に注目し、モデル出力を組み合わせることで予測精度や汎化性能の向上にどのように役立つかを解説します。
MQL5とMetaTrader 5のインジケーターの再定義 MQL5とMetaTrader 5のインジケーターの再定義
MQL5でインジケーター情報を収集する革新的なアプローチにより、開発者がカスタム入力をインジケーターに渡して即座に計算をおこなえるようになり、より柔軟で効率的なデータ分析が可能になります。この方法は、従来の制約を超えてインジケーターで処理される情報に対する制御性を高めるため、アルゴリズム取引において特に有用です。