English Русский Deutsch
preview
MQL5で他の言語の実用的なモジュールを実装する(第5回):PythonのLoggingモジュールによるプロ仕様のログ

MQL5で他の言語の実用的なモジュールを実装する(第5回):PythonのLoggingモジュールによるプロ仕様のログ

MetaTrader 5トレーディング |
17 0
Omega J Msigwa
Omega J Msigwa

内容


はじめに

ログは、あらゆる現代のデバイスやプログラム、ソフトウェアにおいて極めて重要な要素です。ログとは、ある処理の実行期間中に発生したすべての出来事を記録する仕組みを指します。

  • コンピュータは、ソフトウェアの使用状況、接続履歴、システムイベントなどを記録します。
  • ブラウザは、閲覧したサイトやその操作履歴を保持します。

これらの記録は、トラブルシューティング、デバッグ、監査、パフォーマンス監視、そしてシステムの挙動を長期的に理解するために不可欠です。

画像出典:pexels.com

アルゴリズム取引の分野においても、ログは非常に重要です。ログにより、以下のようなことが可能になります。

  1. 取引判断を監視することで、エキスパートアドバイザー(EA)がいつ、どのような理由でポジションをオープン、変更、クローズしたかを把握できます。
  2. あらゆる市場状況において、意図した通りにロジックが動作しているかを確認できます。
  3. 計算ミスや注文拒否の原因など、複雑なロジックの問題点を特定できます。

MetaTrader 5には標準のログ機能が備わっており、基本的な用途には十分対応できますが、いくつかの制約も存在します。


MetaTrader 5のログ記録メカニズムに関する問題

すべてのログがシステム生成ログと混在する

[エキスパート]タブでは特定のプログラムに関する情報だけが表示されるわけではなく、すべてのログが同一のコンソールに出力され、同一の日付のログファイルにまとめて保存されます。

その結果、特定のプログラムや機能に関するログを監視することが困難になります。

フォーマットが難しい

MQL5では情報の出力方法に明確な規定がないため、ログの形式がバラバラになりがちです。この一貫性の欠如により、エラーの発見や不具合の特定が難しくなります。

冗長性をほとんど制御できない

Print関数の呼び出しの前に複数のif文を明示的に記述しない限り、[エキスパート]タブに出力される情報を制御する方法がありません。

これらは、MetaTrader 5に組み込まれているログ機能の制限のほんの一部です。一方、Pythonにはloggingという標準モジュールが用意されており、上述した制約のいくつかを解消することができます。本記事では、このモジュールの概要を解説するとともに、MQL5プログラミング言語において非常に類似したライブラリを実装していきます。


MQL5におけるPythonのログ記録機能

Pythonドキュメントによると

loggingと呼ばれるモジュールは、アプリケーションやライブラリにおける柔軟なイベントログシステムを実装するための関数およびクラスを提供します。

このモジュールは、ログの基本原則を維持しつつ高い柔軟性に重点を置いており、Pythonアプリケーション内で発生するイベントを記録するためのシンプルかつ汎用的な手段をユーザーに提供します。

import logging
logger = logging.getLogger(__name__)

def do_something():
    logger.info('Doing something important')

def main():
    logging.basicConfig(filename='myapp.log', level=logging.INFO)
    
    logger.info('Started')
    do_something()
    logger.info('Finished')

if __name__ == '__main__':
    main()

出力ファイル(myapp.log)

INFO:__main__:Finished
INFO:__main__:Started
INFO:__main__:Doing something important
INFO:__main__:Finished

わずか数行のコードで、ログを保存するファイルの指定と、そのファイルへのログ出力を実現することができました。

一方で、これに対応するMQL5のクラスでは、 getLoggerという関数は不要です。この関数は単に特定の名前を持つロガーを取得(または作成)するだけの役割だからです。

この機能は、クラスのコンストラクタ内で、名前を割り当てるオプションとして処理することができます。コンストラクタはCLoggerオブジェクトを返す設計とすることが可能です。

class CLogger
  {
private:

   string            m_name;
   LogLevels         m_loglevel;
   string            m_filename;
   int               m_filehandle; // Handle of log file
   bool              m_iscommon;
   string            m_logs_format;
   bool              m_console_on;

   int               m_fileflags;
   bool              is_configured;

public:

   void              CLogger(const string name);
   void              CLogger(void); // Constructor
   void             ~CLogger(void); // Destructor
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CLogger::CLogger(void):
   m_filename("logs.log"),
   is_configured(false),
   m_filehandle(-1),
   m_console_on(true),
   m_iscommon(false),
   m_logs_format(DEFAULT_MSG_FORMAT),
   m_loglevel(LOG_LEVEL_INFO)
  {

  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CLogger::CLogger(const string name):
   m_name(name)
  {
   CLogger();
  }

このクラスにおいて最も重要な関数の一つが、basicConfigと呼ばれる関数です。


ロガーの基本設定

ログにどのような内容を出力するかを定義することは非常に重要であり、それはこの関数内でのみおこなうことができます。以下に、設定すべき主な項目(変数)を示します。

ファイル名(filename)

これは、すべてのログを書き込むファイルの名前です。

ファイル名の拡張子は.txtまたは.logのいずれかでなければなりません。

bool CLogger::basicConfig(LogLevels log_level = LOG_LEVEL_INFO,
                          string filename = "logs.log",
                          bool console_on = true,
                          string format = DEFAULT_MSG_FORMAT,
                          bool file_common = false)
  {
   m_filename = filename;
   m_logs_format = format;
   m_console_on = console_on;
   m_iscommon = file_common;
   m_loglevel = log_level;


//--- Before reading the file check if the extension is ok

   if(!checkFileExtenstion(filename))
     {
      is_configured = false;
      return false;
     }

ファイル名の拡張子を確認します。

bool CLogger::checkFileExtenstion(string filename)
  {
   if(StringFind(filename, ".txt") > 0 || StringFind(filename, ".log") > 0)
      return true;

   printf("Unsupported file extension, the logger expects a file [.txt, .log] file extensions (types)");

   return false;
  }

繰り返しになりますが、ログとは指定したファイルに情報を保存するプロセスです。では、指定したテキストファイルを開いてみましょう。

bool CLogger::basicConfig(LogLevels log_level = LOG_LEVEL_INFO,
                          string filename = "logs.log",
                          bool console_on = true,
                          string format = DEFAULT_MSG_FORMAT,
                          bool file_common = false)
  {
   m_filename = filename;
   m_logs_format = format;
   m_console_on = console_on;
   m_iscommon = file_common;
   m_loglevel = log_level;


//--- Before reading the file check if the extension is ok

   if(!checkFileExtenstion(filename))
     {
      is_configured = false;
      return false;
     }

//--- Open the file for writting

   m_fileflags = FILE_READ | FILE_WRITE | FILE_SHARE_WRITE | FILE_TXT | FILE_ANSI;

   if(m_iscommon)
      m_fileflags |= FILE_COMMON;

   m_filehandle = FileOpen(filename, m_fileflags); // Open or create file

   if(m_filehandle == INVALID_HANDLE)
     {
      printf("func=%s line=%d, failed to open a '%s'. Error = %d", __FUNCTION__, __LINE__, filename, GetLastError());
      is_configured = false;
      return false;
     }

   FileSeek(m_filehandle, 0, SEEK_END); //Move to the end of the file

//--- Handle large files than accepted

   fileRotate(m_filehandle, m_filename, m_fileflags, m_iscommon);

   is_configured = true;
   return true;
  }

効率的かつ高速なログ記録を実現するためには、ログファイルの読み書き処理を最適化する必要があります。また、ログファイルのサイズには厳密な制限を設けることが重要です(大きすぎるテキストファイルはメモリを過剰に消費し、読み書きの効率が低下する)

// Max file size in megabytes
#define MAX_FILE_SIZEMB 10
// The maximum number of files of FILE_SIZEMB to create before the system stop writting for good
#define MAX_LOG_FILES 1000

デフォルト値は10MBです。ご存じのとおり、10MBを超えるテキストファイルは非常に大きく、1行あたり数バイト程度の情報しか含まれないプレインテキストファイルとしては過剰なサイズです。

ファイルサイズがこの上限を超えるたびに、新しいログファイルが自動的に作成されます。このとき、新しいファイル名は「ベース名 + _[同名ログファイルの連番]」という形式になります。たとえば、既存のログファイルがmylogs.logである場合、新たにmylogs_1.logというファイルが作成されます。

また、同一のベース名に対して作成できるファイル数にも上限があり、デフォルトでは最大1000ファイルまでとなっています。

この処理は、fileRotateという関数によって実装されます。

void CLogger::fileRotate(int &handle, string &filename, int flags, bool is_common)
  {
   if (!isFileSizeLimitReached(handle))
      return;   // No rotation

//--- Close the current larger file

   FileClose(handle);
   
//---

   if(!checkFileExtenstion(filename))
      return;

//--- Get the base name of the file

   string extension = StringFind(filename, ".log") < 0 ? ".txt" : ".log";
   int ext_start = MathMax(StringFind(filename, ".log"), StringFind(filename, ".txt"));
   string base_name = StringSubstr(filename, 0, ext_start);

//--- Get the incremented file names

   string new_name = "";
   for(int i = 1; i <= MAX_LOG_FILES; i++)
     {
      new_name = base_name + "_" + string(i) + extension;

      if (!FileIsExist(new_name, is_common))
        {
         handle = FileOpen(new_name, flags);
         if(handle == INVALID_HANDLE)
           {
            printf("Failed to rotate into a new file");
            return;
           }
           
          break;
        }
      else //Check whether an existing file is full or not, if it's not log into that file until it's full
        {
         int temp_handle = FileOpen(new_name, flags);
         if(temp_handle == INVALID_HANDLE)
            continue;
            
         if (!isFileSizeLimitReached(temp_handle))
            {
               handle = temp_handle;
               break;
            }
         else
             FileClose(temp_handle); //Close a temporarily opened file
        }
     }

//---
   
   FileSeek(handle, 0, SEEK_END); //Move to the end of the file
  }
   bool              isFileSizeLimitReached(int handle)
     {
      int size = (int)FileSize(handle);
      if(size <= MAX_FILE_SIZEMB * 1000000)
         return false;   // No rotation
      
      //---
      
      return true;
     }

以下は出力例です。

ファイルサイズの推定は完全に正確とは限りませんが、非常に近い値で判断されます。ファイルサイズが10MBに近づくと、新しいログファイルが作成され、以降のログはその新しいファイルに書き込まれます。

console_on

trueに設定すると、ログは指定されたファイルに保存された後、すべてコンソール([エキスパート]タブ)にも出力されます。

これにより、情報を表示するためだけに追加のコードを書く必要がなくなります。

file_common

このブール変数は、指定された「ログファイル」がCommonディレクトリにあるか(trueに設定した場合)、通常のMQL5データパスにあるか(falseに設定した場合)を示します。

//--- Open the file for writting

   m_fileflags = FILE_READ | FILE_WRITE | FILE_SHARE_WRITE | FILE_TXT | FILE_ANSI;

   if(m_iscommon)
      m_fileflags |= FILE_COMMON;

log_level

この変数は、ログに出力する情報の詳細度(どの程度まで深く情報を記録するか)をロガーに指定します。 

enum LogLevels
  {
   LOG_LEVEL_DEBUG    = 10,
   LOG_LEVEL_INFO     = 20,
   LOG_LEVEL_WARNING  = 30,
   LOG_LEVEL_ERROR    = 40,
   LOG_LEVEL_CRITICAL = 50
  };

LOG_LEVELがクラスに与える影響

多くの人が考えるように、この変数は非常に重要です。というのも、ロガーがファイルへの書き込みやコンソール出力を実行する際の最小の重要度レベルを決定するためです。つまり、フィルターとして機能します。

ユーザーがLOG_LEVEL_INFOを選択した場合、それより下位のログレベルはすべて無視されます。

関数 レベル ログ/出力?
CLogger.debug()
10 いいえ
CLogger.info()
20 はい
CLogger.warning()
30 はい
CLogger.error()
40 はい
CLogger.critical()
50 はい 

つまり、たとえdebug()関数が呼び出されていても、そのログレベルが許可された最小レベルより低い場合には何も実行されません。

これは非常に有用な仕組みであり、設定によってログの冗長性を制御できるようになります。たとえば、開発モードではLOG_LEVEL_DEBUGを選択することで、すべてのログを出力し、プログラムのデバッグを効果的におこなうことができます。一方で、本番(ライブ取引)環境ではLOG_LEVEL_WARNINGを選択し、警告、エラー、致命的エラーのみを記録することが一般的です。

format

format変数は、このクラスにおいて最も重要な要素の一つです。なぜなら、[エキスパート]タブおよびログファイル内でのログの表示形式を制御できる唯一の箇所だからです。

以下の表は、フォーマットに使用できるプレースホルダーとその出力内容を示しています。

プレースホルダー 説明 注釈と例 
%(asctime)s
ログエントリのローカルタイムスタンプTimeLocal(YYY.MM.DD HH:MM:SS形式) 日時値:2025.01.01 00:00:05
%(levelname)s
ログレベル名(テキスト形式) INFO、DEBUG、ERROR、WARNING、CRITICALのいずれか
%(programname)s
プログラムまたはコンポーネントの名前。引数を指定したオプションのクラスコンストラクタで設定可能。 例:「My Indicator」
%(functionname)s
ログが生成される関数の名前。 OnTick、OnInitなどのログ機能を通じて手動で提供可能。
%(linenumber)d
ログが生成されるコードの行番号 (例:line 118)行番号が解析された場合のみ、空の値を返す。
%(programtype)s

実行中のプログラムの種類。カスタムコンストラクタを使用して設定可能。ENUM_PROGRAM_TYPEによって異なる。

CLogger::CLogger(const string name,ENUM_PROGRAM_TYPE program_type):
   m_name(name),
   m_program_type(ProgramTypeToSTring(program_type))
 {
 
 }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
string CLogger::ProgramTypeToSTring(ENUM_PROGRAM_TYPE prog_type)
{
   switch(prog_type)
   {
      case PROGRAM_SCRIPT:
         return "Script";

      case PROGRAM_EXPERT:
         return "EA";

      case PROGRAM_INDICATOR:
         return "Indicator";

      case PROGRAM_SERVICE:
         return "Service";

      default:
         return "Unknown";
   }
}
スクリプト、EA、インジケーター、サービス、不明のいずれか。
%(message)s
ログメッセージそのもの。 例:「Failed to create a pending order.

この記事の後半では、これらのフォーマットがログとどのように相互作用するかを詳しく見ていきます。

フォーマット例は以下のとおりりです。

string format = "%(asctime)s: %(levelname)s:%(programname)s:%(programtype)s:%(functionname)s:%(linenumber)d:%(message)s";
logger.basicConfig(LOG_LEVEL_DEBUG, "logs.log", false, format);

以下は出力例です。

2025.12.02 09:13:54:INFO:Logging Test:Script:OnStart:36:The script has started
2025.12.02 09:13:54:ERROR:Logging Test:Script:OnStart:40:Some operation has failed Error = 0

フォーマット例は以下のとおりです。

string format = "%(asctime)s | [%(levelname)s] [%(programname)s] [%(programtype)s] func:%(functionname)s line:%(linenumber)d --> [%(message)s]";
logger.basicConfig(LOG_LEVEL_DEBUG, "logs.log", false, format);

以下は出力例です。

2025.12.02 09:15:41 | [INFO] [Logging Test] [Script] func:OnStart line:37 --> [The script has started]
2025.12.02 09:15:41 | [ERROR] [Logging Test] [Script] func:OnStart line:41 --> [Some operation has failed Error = 0]


情報の一部をログに記録する

クラス内のprivate関数であるLogは、メッセージ(ログ)のフォーマット、生成、ファイルへの書き込み、そして[エキスパート]タブへの表示(出力)という中核的な処理を担当します。

void              Log(LogLevels level, string msg, string func_name = "", int line_no = -1);

すべてのクラスコンストラクタは任意であるため、ユーザーは必要に応じて最小限の設定でログ記録を開始することができます。

設定をおこなうbasicConfig関数も任意のメソッドです。そのため、ユーザーが明示的に設定を提供していない場合には、ログを書き込む前にデフォルト設定を必ず適用し、初期状態の動作を保証する必要があります。

void CLogger::Log(LogLevels level,
                  string msg,
                  string func_name = "",
                  int line_no = -1)
  {
//---

   if(!is_configured)  //Auto-configure if the function basicConfig wasn't called
      basicConfig();

ログレベルについて前述したように、現在のログレベルがユーザーによって設定された必要なレベルを下回っていないかどうかを確認する必要があります。

//--- Level filtering

   if(level < m_loglevel)
      return;

ログのフォーマット

プレースホルダーをすべて削除し、目的の値に置き換える必要があります。

// Standard placeholders

   StringReplace(entry, "%(asctime)s", t);
   StringReplace(entry, "%(levelname)s", LevelToString(level));
   StringReplace(entry, "%(message)s", msg);

// Custom placeholders

// ---- Custom placeholders ----

// Program name

   if(m_name != "")
       StringReplace(entry, "%(programname)s", m_name);
   else
      StringReplace(entry, "%(programname)s", "");

// Function name
   if(func_name != "")
      StringReplace(entry, "%(functionname)s", func_name);
   else
      StringReplace(entry, "%(functionname)s", "");

// Program type

   if(m_program_type != "")
      StringReplace(entry, "%(programtype)s", m_program_type);
   else
      StringReplace(entry, "%(programtype)s", "");

// Line number
   if(line_no >= 0)
      StringReplace(entry, "%(linenumber)d", IntegerToString(line_no));
   else
      StringReplace(entry, "%(linenumber)d", "");

   entry += "\n";

ファイルロテーションの処理

デフォルトで最大10MBのファイルサイズに達する可能性があるため、新しい情報をファイルへ追加する前に、毎回この条件を確認する必要があります。

//--- Handle file rotations before writing

   fileRotate(m_filehandle, m_filename, m_fileflags, m_iscommon);

ログの書き込みと出力

//--- Write to log file (plain text)

   FileWriteString(m_filehandle, entry);
   FileFlush(m_filehandle);

   if(m_console_on)
      Print(entry);

Logという名前の関数は、特定のログメッセージに関する他の関数を生成するためのものであるため、クラスの外部からはアクセスできません。以下のセクションでは、それらの関数について説明します。


各ログメッセージに対応する専用関数

デバッグ用途のログ

   void              debug(string msg, string func_name = "", int line_no = -1)
     {
      this.Log(LOG_LEVEL_DEBUG, msg, func_name, line_no);
     }

この関数は最も低いログレベルでの出力を目的としており、主に開発者がコードや関数の詳細な挙動を把握するために使用されます。

使用例

string format = "%(asctime)s | [%(levelname)s] [%(programname)s] [%(programtype)s] func:%(functionname)s line:%(linenumber)d --> [%(message)s]";
logger.basicConfig(LOG_LEVEL_DEBUG, "logs.log", false, format);

bool num_a = 10;
bool num_b = -10;
  
logger.debug("num_a>num_b "+(string)bool(num_a>num_b), __FUNCTION__, __LINE__);  

以下は出力例です。

2025.12.02 09:26:06 | [DEBUG] [Logging Test] [Script] func:OnStart line:43 --> [num_a>num_b false]

情報ログ

この種のログは、処理の進行状況や状態を示す目的で使用されます。

void OnStart()
  {
//---
      
   logger.info("The script has started");
   
   // some activity   

   logger.info("End of the script!");
  }

以下は出力例です。

2025.12.02 09:26:06 | [INFO] [Logging Test] [Script] func:OnStart line:38 --> [The script has started]
2025.12.02 09:26:06 | [INFO] [Logging Test] [Script] func: line: --> [End of the script!]

エラーログ

この関数は、プログラム内で発生した不具合をユーザーに通知するために使用されます。

   void              error(string msg, string func_name = "", int line_no = -1)
     {
      this.Log(LOG_LEVEL_ERROR, msg, func_name, line_no);
     }

出力

void OnStart()
  {
//---

   if (!doSomething())
      {
        logger.error(StringFormat("Some operation has failed Error = %d",GetLastError()), __FUNCTION__, __LINE__);
      }
     
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool doSomething()
 {
   return false;
 }

以下は出力例です。

2025.12.02 09:29:26 | [ERROR] [Logging Test] [Script] func:OnStart line:47 --> [Some operation has failed Error = 0]

警告ログ

警告ログは、致命的ではないものの注意が必要な状況をユーザーに知らせるために使用されます。

使用例

input int risk_per_trade = 50; //Risk Per Trade
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   
   string format = "%(asctime)s | [%(levelname)s] [%(programname)s] [%(programtype)s] func:%(functionname)s line:%(linenumber)d --> [%(message)s]";
   logger.basicConfig(LOG_LEVEL_DEBUG, "logs.log", false, format);
 
   logger.info("The script has started",__FUNCTION__,__LINE__);
   
   if (risk_per_trade>10) //if a user has set the risk higher than 10% of the account balance
      logger.warning(StringFormat("You have risked too much for a single trade. Risk percentage = %d", risk_per_trade));
  }

以下は出力例です。

2025.12.02 09:15:41 | [WARNING] [Logging Test] [Script] func: line: --> [You have risked too much for a single trade. Risk percentage = 50]

致命的または重大なログ

これらは、その深刻度から見て最も重要なログです。これらは、システムに重大な欠陥があることを示すためによく使用され、通常は特定の問題が解決されるまでプログラムの実行が続行できないことを意味します。

   void              critical(string msg, string func_name = "", int line_no = -1)
     {
      this.Log(LOG_LEVEL_CRITICAL, msg, func_name, line_no);
     }

戦略にとって非常に有用なインジケーターがあるとします。プログラムがそのインジケーターの読み込みに失敗すると、プログラム全体が停止するはずです。 

#include <PyMQL5\logging.mqh>
CLogger logger(PROG_NAME, PROG_TYPE);

int important_indicator_handle;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   
   string format = "%(asctime)s | [%(levelname)s] [%(programname)s] [%(programtype)s] func:%(functionname)s line:%(linenumber)d --> [%(message)s]";
   logger.basicConfig(LOG_LEVEL_DEBUG, "logs.log", false, format);
 
   important_indicator_handle = iMA(Symbol(), Period(), -1, 0, MODE_SMA, PRICE_CLOSE); //An indicator with a negative period
   
   if (important_indicator_handle == INVALID_HANDLE)
     {
       logger.critical("Failed to load the Moving Average indicator, Error = "+(string)GetLastError(), __FUNCTION__, __LINE__);
       return;
     }
 }

以下は出力例です。

2025.12.02 09:34:54 | [CRITICAL] [Logging Test] [Script] func:OnStart line:56 --> [Failed to load the Moving Average indicator, Error = 4002]


ログ処理の最適化

テキストファイルへの読み書き処理(I/O操作)は、MQL5において最もコストの高い処理の一つです。さらに、情報を[エキスパート]タブに表示するために使用されるPrint関数も同様に、システム負荷の観点では軽い処理ではありません。

そのため、秒単位で頻繁にファイルへ書き込みをおこなうのではなく、ログを一度メモリ上(キャッシュ)に一時的に保持し、必要に応じてまとめて保存する仕組みをユーザーに提供することが有効です。

手順は簡単です。ログを書き込むためのグローバル配列を用意し、その配列全体を指定されたファイルに書き込む関数を用意します。

class CLogger
  {
private:
   
   //--- Caching
   
   bool              m_cache_mode;
   string            m_logs_cache[];
   uint              m_logs_count;
   
public:

   void              CLogger(const string name);
   void              CLogger(const string name, ENUM_PROGRAM_TYPE program_type);
   void              CLogger(void); // Constructor
   void             ~CLogger(void); // Destructor

   bool              basicConfig(LogLevels log_level = LOG_LEVEL_INFO, 
                                 string filename = "logs.log",
                                 bool console_on = true,
                                 string format = DEFAULT_MSG_FORMAT,
                                 bool file_common = false,
                                 bool cache_mode = false);

   //---
   
   void              WriteCache()
     {
       for (uint i=0; i<m_logs_count; i++)
         { 
           if (m_filehandle==INVALID_HANDLE)
             DebugBreak();
           
           fileRotate(m_filehandle, m_filename, m_fileflags, m_iscommon);
             
           FileWriteString(m_filehandle, m_logs_cache[i]);
           FileFlush(m_filehandle);
         }
     }
  };
//+------------------------------------------------------------------+
//|         Basic configurations                                     |
//+------------------------------------------------------------------+
bool CLogger::basicConfig(LogLevels log_level = LOG_LEVEL_INFO,
                          string filename = "logs.log",
                          bool console_on = true,
                          string format = DEFAULT_MSG_FORMAT,
                          bool file_common = false,
                          bool cache_mode = false)
  {
   m_filename = filename;
   m_logs_format = format;
   m_console_on = console_on;
   m_iscommon = file_common;
   m_loglevel = log_level;
   m_cache_mode = cache_mode;

//--- some lines of code

   return true;
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CLogger::Log(LogLevels level,
                  string msg,
                  string func_name = "",
                  int line_no = -1)
  {
//---

// some lines of code....

//--- Write to log file (plain text)
   
   if (m_cache_mode) //Write to an array 
     {
       this.m_logs_count++;
       if (m_logs_count>m_logs_cache.Size()) 
         ArrayResize(m_logs_cache, m_logs_count+MAX_CACHE_SIZE); 
       
       //---
       
       m_logs_cache[m_logs_count-1] = entry;
     }
   else // write to a file
     {
       FileWriteString(m_filehandle, entry);
       FileFlush(m_filehandle);
     }

   if(m_console_on)
      Print(entry);
  }

関数WriteCache は、配列m_logs_cacheに保存されているすべての情報を指定されたファイルへ書き出します。これは、basicConfig関数内でcache_mode 変数を false に設定した場合に、自動的にファイルへ書き込まれる処理と同様の動作です。

ユーザーはこの関数を自由に呼び出すことができるため、basicConfig関数にwrite_cache_automaticallyという名前のブール変数を導入することで、処理をはるかに簡素化します。この変数がtrueに設定されている場合、クラスのデストラクタで、一時キャッシュ配列に格納されているすべての情報が指定されたファイルに書き込まれます。

すべての処理が完了した後にログを保存することを目的とすることが想定されています。これは、EAがチャートから削除された場合や、ストラテジーテスターの実行が終了した場合などです。

bool CLogger::basicConfig(LogLevels log_level = LOG_LEVEL_INFO,
                          string filename = "logs.log",
                          bool console_on = true,
                          string format = DEFAULT_MSG_FORMAT,
                          bool file_common = false,
                          bool cache_mode = false,
                          bool write_cache_automatically = false)
  {
   m_filename = filename;
   m_logs_format = format;
   m_console_on = console_on;
   m_iscommon = file_common;
   m_loglevel = log_level;
   m_cache_mode = cache_mode;
   m_write_cache_automatically = write_cache_automatically;
CLogger::~CLogger(void)
  {
   if (m_cache_mode && m_write_cache_automatically)
      WriteCache();
   
//---

   if(m_filehandle != INVALID_HANDLE)
      FileClose(m_filehandle); //Close the file, finally
  }

最後に、キャッシュを使用しないバージョンと比較して、ストラテジーテスターにおいて約50%のテスト時間短縮という改善を確認できました。 

これは、ファイルのローテーション処理を担当する関数に対して複数の変更を加えた後の結果でもあります。

void CLogger::fileRotate(int &handle, string &filename, int flags, bool is_common)
{
   //---If first time -> open main file
   if(handle == -1)
   {
      handle = OpenFile(filename, flags);
      if(handle == -1) 
         return;
   }

   //--- Check rotation trigger
   if(!isFileSizeLimitReached(handle))
      return;

   //--- Close current big file 
   FileClose(handle);
   
   //--- Rotate through numbered files
   for(int i = 1; i <= MAX_LOG_FILES; i++)
   {
      string new_name = m_base_name + "_" + (string)i + m_file_extension;

      // File exists → check if it still has space
      if(is_common?FileIsExist(new_name, FILE_COMMON):FileIsExist(new_name))
      {
         int temp = OpenFile(filename, flags);
         
         //---
         
         if (MQLInfoInteger(MQL_DEBUG))  
           printf("Filename %s size MB = %f",new_name, FileSize(temp)/1e6);
          
         if (temp != -1)
          {
            bool too_big = isFileSizeLimitReached(temp);
            FileClose(temp);

            if(too_big)
               continue;   //--- The fill is full try the next one
          }
      }

      // File does not exist or is small, use it
      if (filename == new_name)
        return;
        
      filename = new_name;
      handle = OpenFile(filename, flags);

      if(handle == -1)
         DebugBreak();

      FileSeek(handle, 0, SEEK_END);
      return;   // IMPORTANT: stop rotation here
   }
}

ログ機能を伴う最適なストラテジーテストについて

キャッシュモードがtrueに設定されていることを確認した後は、ストラテジーテスターにおいてconsole_on変数をfalseに設定することで出力を抑制する必要があります。ただし、これを有効にする明確な理由がある場合を除きます。この対応により、テスター全体の実行時間をさらに短縮できる可能性があります。

#define PROG_NAME MQLInfoString(MQL_PROGRAM_NAME)
#define PROG_TYPE (ENUM_PROGRAM_TYPE)MQLInfoInteger(MQL_PROGRAM_TYPE)
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
#include <PyMQL5\logging.mqh>
CLogger logger(PROG_NAME, PROG_TYPE);
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   
   string format = "%(asctime)s:%(programname)s:%(programtype)s:%(functionname)s:%(linenumber)d:%(message)s";
   
   bool is_tester = (bool)MQLInfoInteger(MQL_TESTER);
   logger.basicConfig(LOG_LEVEL_DEBUG, "logs.log", !is_tester, format, is_tester, true, true);
 
   logger.info("Program started!");
   
//---
   return(INIT_SUCCEEDED);
  }

ストラテジーテスターはファイル内に保存されたすべての情報を別のデータパスに格納するため、Commonフォルダ配下に保存されたすべてのログを取得できるようにするには、変数file_commonをtrueに設定する必要があります。

以下は、EAの残りの部分です。

void OnDeinit(const int reason)
  {
//---
   logger.info("Program stopped. Reason = "+UninitializeReasonDescription(reason), __FUNCTION__, __LINE__);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   
   logger.info("Program running");
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
string UninitializeReasonDescription(const int reason) 
  { 
   switch(reason) 
     { 
      //--- the EA has stopped working calling the ExpertRemove() function 
      case REASON_PROGRAM : 
        return("Expert Advisor terminated its operation by calling the ExpertRemove() function"); 
      //--- program removed from a chart 
      case REASON_REMOVE : 
        return("Program has been deleted from the chart"); 
      //--- program recompiled 
      case REASON_RECOMPILE : 
        return("Program has been recompiled"); 
      //--- symbol or chart period changed 
      case REASON_CHARTCHANGE : 
        return("Symbol or chart period has been changed"); 
      //--- chart closed 
      case REASON_CHARTCLOSE : 
        return("Chart has been closed"); 
      //--- inputs changed by user 
      case REASON_PARAMETERS : 
        return("Input parameters have been changed by a user"); 
      //--- another account has been activated or reconnection to the trade server has occurred due to changes in the account settings 
      case REASON_ACCOUNT : 
        return("Another account has been activated or reconnection to the trade server has occurred due to changes in the account settings"); 
      //--- another chart template applied 
      case REASON_TEMPLATE : 
        return("A new template has been applied"); 
      //--- OnInit() handler returned a non-zero value 
      case REASON_INITFAILED : 
        return("This value means that OnInit() handler has returned a nonzero value"); 
      //--- terminal closed 
      case REASON_CLOSE : 
        return("Terminal has been closed"); 
     } 
  
//--- deinitialization reason unknown 
   return("Unknown reason"); 
  }

出力

予想通り、Commonディレクトリの下に複数のファイルが作成され、各ファイルのサイズは約10MBでした。


ログ記録をはるかに簡単にする — Python流アプローチ

Pythonのloggingモジュールに慣れている方なら、エラーを発生させた関数名や特定の行番号を自分で解析する必要がないことに気づくかもしれません。

import logging
logger = logging.getLogger(__name__)

logging.basicConfig(filename='myapp.log', level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s - file:%(filename)s - line:%(lineno)d - func:%(funcName)s')

def some_function():
    logger.info('Doing something')    
    
some_function()

出力

2025-12-01 20:01:42,542 - INFO - Doing something - file:log.py - line:9 - func:some_function

MQL5では、ほとんどの値(各ログメッセージの行名と関数名、クラスコンストラクタ内のプログラム名(ファイル名)など)をハードコーディングする必要があります。この面倒で反復的なプロセスを避けるためには、#defineマクロを使用できます。

#define logger_info(msg) logger.info(msg, __FUNCTION__, __LINE__)
#define logger_debug(msg) logger.debug(msg, __FUNCTION__, __LINE__)
#define logger_warning(msg) logger.warning(msg, __FUNCTION__, __LINE__)
#define logger_error(msg) logger.error(msg, __FUNCTION__, __LINE__)
#define logger_critical(msg) logger.critical(msg, __FUNCTION__, __LINE__)

使用法

void OnStart()
  {
//---
   
   string format = "%(asctime)s | [%(levelname)s] [%(programname)s] [%(programtype)s] func:%(functionname)s line:%(linenumber)d --> [%(message)s]";
   logger.basicConfig(LOG_LEVEL_DEBUG, "logs.log", false, format);
 
   logger_info("The script has started");
   
   bool num_a = 10;
   bool num_b = -10;
     
   logger_info("num_a>num_b "+(string)bool(num_a>num_b));  

   if (!doSomething())
      {
        logger_error(StringFormat("Some operation has failed Error = %d",GetLastError()));
      }
      
   if (risk_per_trade>10) //if a user has set the risk higher than 10% of the account balance
      logger_warning(StringFormat("You have risked too much for a single trade. Risk percentage = %d", risk_per_trade));
      
   important_indicator_handle = iMA(Symbol(), Period(), -1, 0, MODE_SMA, PRICE_CLOSE); //An indicator with a negative period
   
   if (important_indicator_handle == INVALID_HANDLE)
     {
       logger_critical("Failed to load the Moving Average indicator, Error = "+(string)GetLastError());
       //return;
     }
     
//---

   logger_info("End of the script!");
  }

以下は出力例です。

2025.12.02 09:47:49 | [INFO] [Logging Test] [Script] func:OnStart line:43 --> [The script has started]
2025.12.02 09:47:49 | [INFO] [Logging Test] [Script] func:OnStart line:48 --> [num_a>num_b false]
2025.12.02 09:47:49 | [ERROR] [Logging Test] [Script] func:OnStart line:52 --> [Some operation has failed Error = 0]
2025.12.02 09:47:49 | [WARNING] [Logging Test] [Script] func:OnStart line:56 --> [You have risked too much for a single trade. Risk percentage = 50]
2025.12.02 09:47:49 | [CRITICAL] [Logging Test] [Script] func:OnStart line:62 --> [Failed to load the Moving Average indicator, Error = 4002]
2025.12.02 09:47:49 | [INFO] [Logging Test] [Script] func:OnStart line:68 --> [End of the script!]


最終的な考察

ログは単に[エキスパート]タブへ平文を出力するだけのものではありません。ログはソフトウェア開発における基本的な要素であり、プログラムの動作を理解し、問題を特定し、時間経過に伴うイベントを追跡するために役立ちます。

MQL5 において、Pythonのloggingライブラリのような構造化され再利用可能なログモジュールを実装することで、取引システムに現代的な開発手法を取り入れることができます。これにより、コードの保守性が向上し、デバッグが容易になり、PythonベースのシステムやWebサーバーなどで一般的に採用されているプロフェッショナルなログ管理手法と整合性のある形でログを保存し、解釈できるようになります。

信頼性の高いログモジュールは単なる利便性を提供するだけではなく、開発を整理された状態に保ち、効率性を高め、業界標準のプログラミング手法に沿った設計を維持するための重要なツールです。

本連載でで紹介したすべてのコードを含むリポジトリはhttps://github.com/MegaJoctan/PyMQL5にあります。貢献やバグ修正も歓迎です。


添付ファイルの表

ファイル名 説明と使用法
Include\PyMQL5\logging.mqh Python風のログ出力および保存をおこなうロギングクラス。CLogger クラスを含む
Scripts\Logging Test.mq5
CLoggerクラスのメソッドをテストするための簡単なスクリプト
Experts\Logging Test.mq5 実際の取引環境で CLogger の機能を検証するためのEA

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

添付されたファイル |
Attachments.zip (7.35 KB)
利益強化アーキテクチャ:多層型口座保護 利益強化アーキテクチャ:多層型口座保護
このディスカッションでは、積極的な利益目標を追求しながら、壊滅的な損失へのエクスポージャーを最小限に抑えることを目的とした、構造化された多層防御システムを紹介します。本システムの焦点は、取引パイプラインのあらゆるレベルにおいて、攻撃的な売買ロジックと保護的な安全機構を組み合わせることにあります。その狙いは、このEAを「リスクを認識する捕食者」のように設計することです。すなわち、高価値な機会を捉える能力を持ちながらも、突発的な市場ストレスに対して盲目的になることを防ぐための複数の防護層を常に備えている状態を目指します。
取引戦略の開発:出来高制限アプローチの使用 取引戦略の開発:出来高制限アプローチの使用
テクニカル分析の世界では、価格がしばしば中心的な役割を果たします。トレーダーはサポートやレジスタンス、パターンを綿密に描きますが、多くの場合、これらの動きを駆動する重要な力である「出来高」を見落としています。本記事では、新しい出来高分析のアプローチであるVolume Boundaryインジケーターについて解説します。この指標は、バタフライ曲線やトリプルサイン曲線といった高度な平滑化関数を用いることで変換をおこない、より明確な解釈と体系的な取引戦略の構築を可能にします。
共和分株式による統計的裁定取引(第8回):ポートフォリオのリバランスのためのローリングウィンドウ固有ベクトル比較 共和分株式による統計的裁定取引(第8回):ポートフォリオのリバランスのためのローリングウィンドウ固有ベクトル比較
本記事では、共和分関係にある株式を用いた平均回帰型統計裁定戦略において、早期の不均衡診断およびポートフォリオリバランスのために、ローリングウィンドウ固有ベクトル比較を用いる手法を提案します。この手法は、従来のインサンプル/アウトオブサンプルADF (IS/OOS ADF)検証と比較されており、固有ベクトルの変化が、IS/OOS ADFが依然としてスプレッドの定常性を示している場合であっても、リバランスの必要性を示唆し得ることを示します。本手法は主に実運用取引の監視を目的としていますが、結論として、固有ベクトル比較をスコアリングシステムに統合することも可能である一方で、その実際のパフォーマンスへの寄与については検証が必要であるとされています。
MQL5入門(第30回):MQL5のAPIとWebRequest関数の習得(IV) MQL5入門(第30回):MQL5のAPIとWebRequest関数の習得(IV)
APIレスポンスから取得したローソク足データの抽出、変換、整理を、MQL5環境において簡潔におこなうためのステップごとのチュートリアルを紹介します。本ガイドは、コーディングスキルを向上させたい初心者の方や、市場データを効率的に管理するための堅牢な手法を構築したい方に最適です。