MQL5で他の言語の実用的なモジュールを実装する(第5回):PythonのLoggingモジュールによるプロ仕様のログ
内容
- はじめに
- MetaTrader 5のログ記録メカニズムに関する問題
- MQL5におけるPythonのログ記録機能
- ロガーの基本設定
- 情報の一部をログに記録する
- 各ログメッセージの具体的な機能
- ログ記録プロセスの最適化
- ログ記録をはるかに簡単にする — Python流アプローチ
- 結論
はじめに
ログは、あらゆる現代のデバイスやプログラム、ソフトウェアにおいて極めて重要な要素です。ログとは、ある処理の実行期間中に発生したすべての出来事を記録する仕組みを指します。
- コンピュータは、ソフトウェアの使用状況、接続履歴、システムイベントなどを記録します。
- ブラウザは、閲覧したサイトやその操作履歴を保持します。
これらの記録は、トラブルシューティング、デバッグ、監査、パフォーマンス監視、そしてシステムの挙動を長期的に理解するために不可欠です。

アルゴリズム取引の分野においても、ログは非常に重要です。ログにより、以下のようなことが可能になります。
- 取引判断を監視することで、エキスパートアドバイザー(EA)がいつ、どのような理由でポジションをオープン、変更、クローズしたかを把握できます。
- あらゆる市場状況において、意図した通りにロジックが動作しているかを確認できます。
- 計算ミスや注文拒否の原因など、複雑なロジックの問題点を特定できます。
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
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
利益強化アーキテクチャ:多層型口座保護
取引戦略の開発:出来高制限アプローチの使用
共和分株式による統計的裁定取引(第8回):ポートフォリオのリバランスのためのローリングウィンドウ固有ベクトル比較
MQL5入門(第30回):MQL5のAPIとWebRequest関数の習得(IV)
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索