ログレコードをマスターする(第5回):キャッシュとローテーションによるハンドラの最適化
はじめに
本連載最初の記事、「ログレコードをマスターする(第1回):MQL5の基本概念と最初のステップ」では、エキスパートアドバイザー(EA)開発向けのカスタムログライブラリの構築に着手しました。この記事では、なぜ独自のログツールが必要なのかという動機を明らかにし、MetaTrader5の標準ログ機能の限界を克服するために、MQL5環境において堅牢でカスタマイズ可能かつ強力なロギングソリューションを実現することを目指しました。
前回の記事で取り上げた主なポイントを振り返ると、私たちは次のような基本要件をもとにライブラリの基盤を構築しました。
- シングルトンパターンによる堅牢な構造で、コードコンポーネント間の一貫性を確保
- 高度な永続性を備えたデータベースログ保存機能により、詳細な監査や分析に対応可能
- 出力の柔軟性:ログの出力先をコンソール、ファイル、ターミナル、データベースなど用途に応じて選択可能
- ログレベルによる分類:情報、警告、エラーなどのメッセージを明確に区別
- 出力形式のカスタマイズにより、各開発者やプロジェクトのニーズに応じたログ出力を実現
このような強固な基盤を構築したことで、私たちのログフレームワークは単なるイベントログではなく、EAの動作をリアルタイムで把握・監視・最適化するための戦略的ツールとしての価値を持つことが明らかになりました。
これまでに、ログの基本を学び、それらの書式を整える方法や、ハンドラによってメッセージの出力先を制御する仕組みについて理解してきました。さらに前回の記事では、ログを.txt、.log、.jsonファイルとして保存する方法を習得しました。そして今回の第5回では、キャッシュ機能とファイルローテーションを実装することで、ログのファイル保存処理をさらに最適化していきます。それでは始めましょう。
各ハンドラにフォーマッタを追加する
これまで、私たちのロギングライブラリでは、CFormatterクラスのインスタンス1つでメッセージの書式設定を管理しており、このフォーマッタはライブラリのベースであるCLogifyに集中管理されていました。この設計はシンプルなシナリオには適しているものの、ハンドラごとの柔軟性に欠けるという課題があります。
問題は、単一のグローバルなフォーマッタを使用しているため、すべてのハンドラが同じフォーマットを共有する点にあります。これは、異なる出力先が異なる書式を必要とする場合には理想的とは言えません。たとえば、ログをJSON形式で保存するハンドラは特定の構造を求める一方で、コンソールに出力するハンドラはより人間が読みやすい形式を必要とするかもしれません。この問題の解決策は、フォーマッタの責務をハンドラの基底クラス(CLogifyHandler)に移すことです。こうすることで、各ハンドラが独自のフォーマッタを持つことができ、ログメッセージの書式に対してより細かい制御が可能になります。この変更を実装してライブラリの柔軟性がどのように向上するか見ていきましょう。
コードへ直接進みます。今回は、CLogifyHandlerクラスにCFormatterのインスタンスを追加するだけの簡単な作業です。これまでの記事を読んできた方であればすでに理解されている内容かと思いますので、追加された最終コードだけを示し、変更点をハイライトします。
//+------------------------------------------------------------------+ //| LogifyHandler.mqh | //| joaopedrodev | //| https://www.mql5.com/ja/users/joaopedrodev | //+------------------------------------------------------------------+ #property copyright "joaopedrodev" #property link "https://www.mql5.com/ja/users/joaopedrodev" //+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "../LogifyModel.mqh" #include "../Formatter/LogifyFormatter.mqh" //+------------------------------------------------------------------+ //| class : CLogifyHandler | //| | //| [PROPERTY] | //| Name : CLogifyHandler | //| Heritage : No heritage | //| Description : Base class for all log handlers. | //| | //+------------------------------------------------------------------+ class CLogifyHandler { protected: string m_name; ENUM_LOG_LEVEL m_level; CLogifyFormatter *m_formatter; public: CLogifyHandler(void); ~CLogifyHandler(void); //--- Handler methods virtual void Emit(MqlLogifyModel &data); // Processes a log message and sends it to the specified destination virtual void Flush(void); // Clears or completes any pending operations virtual void Close(void); // Closes the handler and releases any resources //--- Set/Get void SetLevel(ENUM_LOG_LEVEL level); void SetFormatter(CLogifyFormatter *format); string GetName(void); ENUM_LOG_LEVEL GetLevel(void); CLogifyFormatter *GetFormatter(void); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CLogifyHandler::CLogifyHandler(void) { } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CLogifyHandler::~CLogifyHandler(void) { //--- Delete formatter if(m_formatter != NULL) { delete m_formatter ; } } //+------------------------------------------------------------------+ //| Processes a log message and sends it to the specified destination| //+------------------------------------------------------------------+ void CLogifyHandler::Emit(MqlLogifyModel &data) { } //+------------------------------------------------------------------+ //| Clears or completes any pending operations | //+------------------------------------------------------------------+ void CLogifyHandler::Flush(void) { } //+------------------------------------------------------------------+ //| Closes the handler and releases any resources | //+------------------------------------------------------------------+ void CLogifyHandler::Close(void) { } //+------------------------------------------------------------------+ //| Set level | //+------------------------------------------------------------------+ void CLogifyHandler::SetLevel(ENUM_LOG_LEVEL level) { m_level = level; } //+------------------------------------------------------------------+ //| Set object formatter | //+------------------------------------------------------------------+ void CLogifyHandler::SetFormatter(CLogifyFormatter *format) { m_formatter = GetPointer(format); } //+------------------------------------------------------------------+ //| Get name | //+------------------------------------------------------------------+ string CLogifyHandler::GetName(void) { return(m_name); } //+------------------------------------------------------------------+ //| Get level | //+------------------------------------------------------------------+ ENUM_LOG_LEVEL CLogifyHandler::GetLevel(void) { return(m_level); } //+------------------------------------------------------------------+ //| Get object formatter | //+------------------------------------------------------------------+ CLogifyFormatter *CLogifyHandler::GetFormatter(void) { return(m_formatter); } //+------------------------------------------------------------------+
最も簡単な変更から進めていきます。まずはCLogifyクラスからCFormatterインスタンスを削除しました。クラスから削除された部分は赤で、追加された部分は緑でハイライトされています。
//+------------------------------------------------------------------+ //| Logify.mqh | //| joaopedrodev | //| https://www.mql5.com/ja/users/joaopedrodev | //+------------------------------------------------------------------+ #property copyright "joaopedrodev" #property link "https://www.mql5.com/ja/users/joaopedrodev" #property version "1.00" #include "LogifyModel.mqh" #include "Formatter/LogifyFormatter.mqh" #include "Handlers/LogifyHandler.mqh" #include "Handlers/LogifyHandlerConsole.mqh" #include "Handlers/LogifyHandlerDatabase.mqh" #include "Handlers/LogifyHandlerFile.mqh" //+------------------------------------------------------------------+ //| class : CLogify | //| | //| [PROPERTY] | //| Name : Logify | //| Heritage : No heritage | //| Description : Core class for log management. | //| | //+------------------------------------------------------------------+ class CLogify { private: CLogifyFormatter *m_formatter; CLogifyHandler *m_handlers[]; public: CLogify(); ~CLogify(); //--- Handler void AddHandler(CLogifyHandler *handler); bool HasHandler(string name); CLogifyHandler *GetHandler(string name); CLogifyHandler *GetHandler(int index); int SizeHandlers(void); //--- Generic method for adding logs bool Append(ENUM_LOG_LEVEL level,string msg, string origin = "", string args = "",string filename="",string function="",int line=0); //--- Specific methods for each log level bool Debug(string msg, string origin = "", string args = "",string filename="",string function="",int line=0); bool Infor(string msg, string origin = "", string args = "",string filename="",string function="",int line=0); bool Alert(string msg, string origin = "", string args = "",string filename="",string function="",int line=0); bool Error(string msg, string origin = "", string args = "",string filename="",string function="",int line=0); bool Fatal(string msg, string origin = "", string args = "",string filename="",string function="",int line=0); //--- Get/Set object formatter void SetFormatter(CLogifyFormatter *format); CLogifyFormatter *GetFormatter(void); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CLogify::CLogify() { } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CLogify::~CLogify() { //--- Delete formatter if(m_formatter != NULL) { delete m_formatter; } //--- Delete handlers int size_handlers = ArraySize(m_handlers); for(int i=0;i<size_handlers;i++) { delete m_handlers[i]; } } //+------------------------------------------------------------------+ //| Add handler to handlers array | //+------------------------------------------------------------------+ void CLogify::AddHandler(CLogifyHandler *handler) { int size = ArraySize(m_handlers); ArrayResize(m_handlers,size+1); m_handlers[size] = GetPointer(handler); } //+------------------------------------------------------------------+ //| Checks if handler is already in the array by name | //+------------------------------------------------------------------+ bool CLogify::HasHandler(string name) { int size = ArraySize(m_handlers); for(int i=0;i<size;i++) { if(m_handlers[i].GetName() == name) { return(true); } } return(false); } //+------------------------------------------------------------------+ //| Get handler by name | //+------------------------------------------------------------------+ CLogifyHandler *CLogify::GetHandler(string name) { int size = ArraySize(m_handlers); for(int i=0;i<size;i++) { if(m_handlers[i].GetName() == name) { return(m_handlers[i]); } } return(NULL); } //+------------------------------------------------------------------+ //| Get handler by index | //+------------------------------------------------------------------+ CLogifyHandler *CLogify::GetHandler(int index) { return(m_handlers[index]); } //+------------------------------------------------------------------+ //| Gets the total size of the handlers array | //+------------------------------------------------------------------+ int CLogify::SizeHandlers(void) { return(ArraySize(m_handlers)); } //+------------------------------------------------------------------+ //| Generic method for adding logs | //+------------------------------------------------------------------+ bool CLogify::Append(ENUM_LOG_LEVEL level,string msg, string origin = "", string args = "",string filename="",string function="",int line=0) { //--- If the formatter is not configured, the log will not be recorded. if(m_formatter == NULL) { return(false); } //--- Textual name of the log level string levelStr = ""; switch(level) { case LOG_LEVEL_DEBUG: levelStr = "DEBUG"; break; case LOG_LEVEL_INFOR: levelStr = "INFOR"; break; case LOG_LEVEL_ALERT: levelStr = "ALERT"; break; case LOG_LEVEL_ERROR: levelStr = "ERROR"; break; case LOG_LEVEL_FATAL: levelStr = "FATAL"; break; } //--- Creating a log template with detailed information datetime time_current = TimeCurrent(); MqlLogifyModel data("",levelStr,msg,args,time_current,time_current,level,origin,filename,function,line); data.formated = m_formatter.FormatLog(data); //--- Call handlers int size = this.SizeHandlers(); for(int i=0;i<size;i++) { data.formated = m_handlers[i].GetFormatter().FormatLog(data); m_handlers[i].Emit(data); } return(true); } //+------------------------------------------------------------------+ //| Debug level message | //+------------------------------------------------------------------+ bool CLogify::Debug(string msg, string origin = "", string args = "",string filename="",string function="",int line=0) { return(this.Append(LOG_LEVEL_DEBUG,msg,origin,args,filename,function,line)); } //+------------------------------------------------------------------+ //| Infor level message | //+------------------------------------------------------------------+ bool CLogify::Infor(string msg, string origin = "", string args = "",string filename="",string function="",int line=0) { return(this.Append(LOG_LEVEL_INFOR,msg,origin,args,filename,function,line)); } //+------------------------------------------------------------------+ //| Alert level message | //+------------------------------------------------------------------+ bool CLogify::Alert(string msg, string origin = "", string args = "",string filename="",string function="",int line=0) { return(this.Append(LOG_LEVEL_ALERT,msg,origin,args,filename,function,line)); } //+------------------------------------------------------------------+ //| Error level message | //+------------------------------------------------------------------+ bool CLogify::Error(string msg, string origin = "", string args = "",string filename="",string function="",int line=0) { return(this.Append(LOG_LEVEL_ERROR,msg,origin,args,filename,function,line)); } //+------------------------------------------------------------------+ //| Fatal level message | //+------------------------------------------------------------------+ bool CLogify::Fatal(string msg, string origin = "", string args = "",string filename="",string function="",int line=0) { return(this.Append(LOG_LEVEL_FATAL,msg,origin,args,filename,function,line)); } //+------------------------------------------------------------------+ //| Set object formatter | //+------------------------------------------------------------------+ void CLogify::SetFormatter(CLogifyFormatter *format) { m_formatter = GetPointer(format); } //+------------------------------------------------------------------+ //| Get object formatter | //+------------------------------------------------------------------+ CLogifyFormatter *CLogify::GetFormatter(void) { return(m_formatter); } //+------------------------------------------------------------------+
フォーマット処理をおこなう箇所を見直しました。以前はクラス内で直接フォーマッタを使っていましたが、今回の変更により、各ハンドラごとにハンドラ自身が提供するフォーマッタを使うようになりました。これにより、単一フォーマットに縛られる制限がなくなり、ライブラリはより多様な用途に柔軟に対応できるようになります。つまり、各出力先は固有のログスタイルを持つことが可能になり、その出力が使われるコンテキストに最適化された形式でログを残せるようになります。次のトピックでは、定期サイクルでログ出力を管理するCIntervalWatcherクラスを使って、ファイルローテーションを補助する仕組みを見ていきます。
CIntervalWatcherクラスの作成
CIntervalWatcherの主な目的は、前回の呼び出しから一定の時間が経過したかどうかを確認することです。これは、特定の時間間隔で確認・出力すべきログを生成する際に不可欠な仕組みです。ログの書き込み過多を避けたり、記録をより構造的に整理したりするためには、ティックごとに処理を走らせるのではなく、適切なサイクル制御が必要になります。このクラスを使用することで、以下の設定が可能になります。
- 監視の時間間隔(秒単位)
- 使用する時刻のソース(現在時刻、GMT、ローカル時刻、取引サーバーの時刻など)
- 初回の実行時にtrueを返すかどうか
このようにして、CIntervalWatcherはライブラリ内で定期的な処理を実行すべきタイミングを判断するための便利なユーティリティとなります。Utilsという新しいフォルダを作成し、このクラスのソースファイルをそこに配置しましょう。最終的に、ファイルブラウザは次のようになります。

クラスの構築に移ります。まずは異なる時間ソースをサポートする列挙型を作成し、これをENUM_TIME_ORIGINと呼びます。
//+------------------------------------------------------------------+ //| Enum for different time sources | //+------------------------------------------------------------------+ enum ENUM_TIME_ORIGIN { TIME_ORIGIN_CURRENT = 0, // [0] Current Time TIME_ORIGIN_GMT, // [1] GMT Time TIME_ORIGIN_LOCAL, // [2] Local Time TIME_ORIGIN_TRADE_SERVER // [3] Server Time }; //+------------------------------------------------------------------+
最後に記録された時刻(m_last_time)、設定された時間間隔(m_interval)、時間の基準(m_time_origin)、および最初の戻りを制御するフラグ(m_first_return)を格納するためのprivate変数をクラスに追加しました。これに伴い、各private属性に対してSetおよびGetメソッドを作成しました。さらに、時間間隔・時間基準・初回返却設定の構成を簡単におこなえるようにするために、このクラスにいくつかの追加コンストラクタを実装することにしました。これは開発者のためのものです。以下に、コンストラクタおよびprivateデータへのアクセサメソッドを含んだコードを示します。
//+------------------------------------------------------------------+ //| class : CIntervalWatcher | //| | //| [PROPERTY] | //| Name : CIntervalWatcher | //| Type : Report | //| Heritage : No heredirary. | //| Description : Monitoring new time periods | //| | //+------------------------------------------------------------------+ class CIntervalWatcher { private: //--- Auxiliary attributes ulong m_last_time; ulong m_interval; ENUM_TIME_ORIGIN m_time_origin; bool m_first_return; public: CIntervalWatcher(ENUM_TIMEFRAMES interval, ENUM_TIME_ORIGIN time_origin = TIME_ORIGIN_CURRENT, bool first_return = true); CIntervalWatcher(ulong interval, ENUM_TIME_ORIGIN time_origin = TIME_ORIGIN_CURRENT, bool first_return = true); CIntervalWatcher(void); ~CIntervalWatcher(void); //--- Setters void SetInterval(ENUM_TIMEFRAMES interval); void SetInterval(ulong interval); void SetTimeOrigin(ENUM_TIME_ORIGIN time_origin); void SetFirstReturn(bool first_return); //--- Getters ulong GetInterval(void); ENUM_TIME_ORIGIN GetTimeOrigin(void); bool GetFirstReturn(void); ulong GetLastTime(void); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CIntervalWatcher::CIntervalWatcher(ENUM_TIMEFRAMES interval, ENUM_TIME_ORIGIN time_origin = TIME_ORIGIN_CURRENT, bool first_return = true) { m_interval = PeriodSeconds(interval); m_time_origin = time_origin; m_first_return = first_return; m_last_time = 0; } //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CIntervalWatcher::CIntervalWatcher(ulong interval, ENUM_TIME_ORIGIN time_origin = TIME_ORIGIN_CURRENT, bool first_return = true) { m_interval = interval; m_time_origin = time_origin; m_first_return = first_return; m_last_time = 0; } //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CIntervalWatcher::CIntervalWatcher(void) { m_interval = 10; // 10 seconds m_time_origin = TIME_ORIGIN_CURRENT; m_first_return = true; m_last_time = 0; } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CIntervalWatcher::~CIntervalWatcher(void) { } //+------------------------------------------------------------------+ //| Set interval | //+------------------------------------------------------------------+ void CIntervalWatcher::SetInterval(ENUM_TIMEFRAMES interval) { m_interval = PeriodSeconds(interval); } //+------------------------------------------------------------------+ //| Set interval | //+------------------------------------------------------------------+ void CIntervalWatcher::SetInterval(ulong interval) { m_interval = interval; } //+------------------------------------------------------------------+ //| Set time origin | //+------------------------------------------------------------------+ void CIntervalWatcher::SetTimeOrigin(ENUM_TIME_ORIGIN time_origin) { m_time_origin = time_origin; } //+------------------------------------------------------------------+ //| Set initial return | //+------------------------------------------------------------------+ void CIntervalWatcher::SetFirstReturn(bool first_return) { m_first_return=first_return; } //+------------------------------------------------------------------+ //| Get interval | //+------------------------------------------------------------------+ ulong CIntervalWatcher::GetInterval(void) { return(m_interval); } //+------------------------------------------------------------------+ //| Get time origin | //+------------------------------------------------------------------+ ENUM_TIME_ORIGIN CIntervalWatcher::GetTimeOrigin(void) { return(m_time_origin); } //+------------------------------------------------------------------+ //| Set initial return | //+------------------------------------------------------------------+ bool CIntervalWatcher::GetFirstReturn(void) { return(m_first_return); } //+------------------------------------------------------------------+ //| Set last time | //+------------------------------------------------------------------+ ulong CIntervalWatcher::GetLastTime(void) { return(m_last_time); } //+------------------------------------------------------------------+
メインメソッドを支援するために、定義された起点に基づいて時間を返すGetTime関数を作成しましょう。
//+------------------------------------------------------------------+ //| Get time in miliseconds | //+------------------------------------------------------------------+ ulong CIntervalWatcher::GetTime(ENUM_TIME_ORIGIN time_origin) { switch(time_origin) { case(TIME_ORIGIN_CURRENT): return(TimeCurrent()); case(TIME_ORIGIN_GMT): return(TimeGMT()); case(TIME_ORIGIN_LOCAL): return(TimeLocal()); case(TIME_ORIGIN_TRADE_SERVER): return(TimeTradeServer()); } return(0); } //+------------------------------------------------------------------+
このクラスで最も重要なメソッドはInspect()です。この関数は、設定された時間間隔が経過したかどうかをチェックします。ロジックは次のようになっています。最初の呼び出し時に、m_last_timeが0(=新しくインスタンス化されたクラス)であるかを確認します。この場合、現在の時刻を記録し、m_first_returnの値をそのまま返します。それ以降の呼び出しでは、保存されたタイムスタンプと「現在の時刻+設定された間隔」が一致するかどうかを確認します。一致しない場合は、指定の時間が経過したと判断し、m_last_timeを現在の時刻に更新してtrueを返します。一致する場合は、まだ指定の時間に達していないと判断し、falseを返します。
//+------------------------------------------------------------------+ //| Check if there was an update | //+------------------------------------------------------------------+ bool CIntervalWatcher::Inspect(void) { //--- Get time ulong time_current = this.GetTime(m_time_origin); //--- First call, initial return if(m_last_time == 0) { m_last_time = time_current; return(m_first_return); } //--- Check interval if(time_current >= m_last_time + m_interval) { m_last_time = time_current; return(true); } return(false); } //+------------------------------------------------------------------+
CIntervalWatcherを使用すると、ログ生成をより細かく制御できるため、プログラム可能なサイクルと処理効率が向上します。この種のアプローチは、タスクの定期的な実行を必要とするログ記録ライブラリにとって不可欠です。ログアクションの定期的な実行が構成されたので、記録プロセスの最適化とシステムパフォーマンスの維持に集中できます。
ログ保存の最適化::キャッシュとファイルのローテーション
前回の記事で紹介した「ログをファイルに直接書き込む」方式は、機能としては十分ですが、ログの量が増加するにつれて非効率になってしまう可能性があります。パフォーマンスの低下を防ぐためには、ログ処理の最適化が不可欠です。このトピックでは、ストレージに過負荷をかけずにデータの整合性を維持しながら、ログが効率的に書き込まれるように、キャッシュおよびファイルローテーションシステムを実装する方法について説明します。
前回の記事では、これらの改善がどのように機能するか、そしてその利点についてさらに詳しく説明しました。
「たとえば、EAが数週間〜数ヶ月にわたり稼働し続け、すべてのイベントやエラー、通知を1つのファイルに記録していたとします。やがてそのログは巨大になり、情報を読み解くことが非常に困難になります。そこで登場するのが「ローテーション」です。ローテーションによってログを小さく整理された単位に分割することで、後からの確認や分析が圧倒的にしやすくなります。
ローテーションには主に以下の2つの方法があります。
- サイズによるローテーション:ファイルサイズの上限(通常はMB単位)を設定し、その上限に達するごとに新しいログファイルを自動で作成します。この方法は、スケジュールに縛られず、ログの肥大化を防ぐのに適しています。ファイルがサイズ上限(メガバイト単位)に達すると、現在のログファイルは「log1.log」のようにインデックス付きで名前変更されます。ディレクトリ内の既存ファイルも順に番号が繰り上がり、たとえば「log1.log」は「log2.log」に名前変更されます。保存できるファイル数の上限に達している場合は、最も古いファイルが削除されます。この方法は、ログが占有するディスク容量やファイル数を制御したい場合に非常に便利です。
- 日付によるローテーション:この方法では、毎日新しいログファイルが作成され、ファイル名に日付が含まれます。それぞれのログの名前には、作成された日付が含まれています(例:log_2025-01-19.log)。この方式は、特定の日のログを確認したいときに非常に便利で、巨大な1ファイルに迷い込むことがありません。私自身も、EAのログ保存にはこの方法をもっともよく使っています。整理されたログは確認や管理がしやすく、作業効率を高めます。
さらに、保存されるログファイルの数を制限することもできます。この管理は、古いログが不必要に蓄積されるのを防ぐために非常に重要です。たとえば「最新30ファイルのみ保持」と設定すれば、31個目のファイルが生成された際に、システムが自動で最も古いログを削除します。
もうひとつ重要なのは、キャッシュの使用です。ログメッセージを受け取るたびに都度ディスクへ書き込むのではなく、一時的にメモリ上のキャッシュに保持しておきます。そしてキャッシュが一定サイズに達した時点で、まとめて一括でファイルに書き出します。その結果、ディスクへの読み書きの回数が減ってパフォーマンスが向上し、ストレージデバイスの寿命が延びます。」
ログファイルのローテーションを実装するには、まずSearchForFilesInDirectory()というヘルパーメソッドが必要です。このメソッドは、特定のディレクトリに存在するすべてのファイルを検索し、その名前を配列で返す役割を担います。FileFindFirst()関数を使用して検索を開始し、ファイルが見つかると、その名前がこの配列に追加されます。プロセスが完了すると、メソッドはFileFindClose()を使用して検索ハンドラを閉じます。
しかし、なぜこの方法がそれほど重要なのでしょうか。単純です。これにより、既存のログファイルを一覧表示して、ログを管理するクラスが必要に応じて古いファイルを削除することが可能になります。
class CLogifyHandlerFile : public CLogifyHandler { private: bool SearchForFilesInDirectory(string directory, string &file_names[]); }; //+------------------------------------------------------------------+ //| Returns an array with the names of all files in the directory | //+------------------------------------------------------------------+ bool CLogifyHandlerFile::SearchForFilesInDirectory(string directory,string &file_names[]) { //--- Search for all log files in the specified directory with the given file extension string file_name; long search_handle = FileFindFirst(directory,file_name); ArrayFree(file_names); bool is_found = false; if(search_handle != INVALID_HANDLE) { do { //--- Add each file name found to the array of file names int size_file = ArraySize(file_names); ArrayResize(file_names,size_file+1); file_names[size_file] = file_name; is_found = true; } while(FileFindNext(search_handle,file_name)); FileFindClose(search_handle); } return(is_found); } //+------------------------------------------------------------------+
ファイルを取得する関数ができたので、これをログの出力を担当するメインメソッドEmit()に組み込むことができます。選択されたローテーション構成に応じて、ロジックが調整されます。
ログのローテーションがファイルサイズに基づいて構成されている場合、関数は以下をおこないます。
- ファイルサイズが構成された上限(m_config.max_file_size_mb)を超えているかどうかを確認します。
- ディレクトリ内のすべてのログファイルを検索します。
- 許可された最大ファイル数(m_config.max_file_count)を超える古いファイルを削除します。
- 古いファイルを名前変更し、インデックスを数値で増加させます(log1.txt、log2.txtなど)。
- 現在のログファイルを「log1」にリネームして、シーケンスを維持します。
ローテーションが日付に基づいている場合、関数は以下をおこないます。
- ディレクトリ内のすべてのログファイルを検索します。
- 許可された最大数(m_config.max_file_count)を超える古いファイルを削除します。
それでは、2つのローテーションロジックを含むEmit()メソッドの実装を見ていきましょう。
//+------------------------------------------------------------------+ //| Processes a log message and sends it to the specified destination| //+------------------------------------------------------------------+ 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 = m_file.Open(log_path, FILE_READ | FILE_WRITE | FILE_ANSI); 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 m_file.Seek(0, SEEK_END); m_file.WriteString(data.formated + "\n"); //--- Size in megabytes ulong size_mb = m_file.Size() / (1024 * 1024); //--- Close file m_file.Close(); string file_extension = this.LogFileExtensionToStr(m_config.file_extension); //--- Check if the log rotation mode is based on file size if(m_config.rotation_mode == LOG_ROTATION_MODE_SIZE) { //--- Check if the current file size exceeds the maximum configured size if(size_mb >= m_config.max_file_size_mb) { //--- Search files string file_names[]; if(this.SearchForFilesInDirectory(m_config.directory+"\\*"+file_extension,file_names)) { //--- Delete files exceeding the configured maximum number of log files int size_file = ArraySize(file_names); for(int i=size_file-1;i>=0;i--) { //--- Extract the numeric part of the file index string file_index = file_names[i]; StringReplace(file_index,file_extension,""); StringReplace(file_index,m_config.base_filename,""); //--- If the file index exceeds the maximum allowed count, delete the file if(StringToInteger(file_index) >= m_config.max_file_count) { FileDelete(m_config.directory + "\\" + file_names[i]); } } //--- Rename existing log files by incrementing their indices for(int i=m_config.max_file_count-1;i>=0;i--) { string old_file = m_config.directory + "\\" + m_config.base_filename + (i == 0 ? "" : StringFormat("%d", i)) + file_extension; string new_file = m_config.directory + "\\" + m_config.base_filename + StringFormat("%d", i + 1) + file_extension; if(FileIsExist(old_file)) { FileMove(old_file, 0, new_file, FILE_REWRITE); } } //--- Rename the primary log file to include the index "1" string new_primary = m_config.directory + "\\" + m_config.base_filename + "1" + file_extension; FileMove(log_path, 0, new_primary, FILE_REWRITE); } } } //--- Check if the log rotation mode is based on date else if(m_config.rotation_mode == LOG_ROTATION_MODE_DATE) { //--- Search files string file_names[]; if(this.SearchForFilesInDirectory(m_config.directory+"\\*"+file_extension,file_names)) { //--- Delete files exceeding the maximum configured number of log files int size_file = ArraySize(file_names); for(int i=size_file-1;i>=0;i--) { if(i < size_file - m_config.max_file_count) { FileDelete(m_config.directory + "\\" + file_names[i]); } } } } } } //+------------------------------------------------------------------+
パフォーマンス向上のためのブロック単位保存
次の改善に進みましょう。この記事の中で最も興味深いと私が考えるロジック、ブロック単位でのレコード保存を実装します。中心となるアイデアは、キャッシュ(一次メモリ)を実装することで、ログレコードを一時的に保存し、設定された上限に達した時点ですべてのレコードを一括でログファイルに保存するというものです。
このロジックは段階的に実装していきます。まず、CLogifyHandlerFileクラスにキャッシュ構造を作成します。クラスのprivateセクションに、ログレコードを一時的に保存するためのMqlLogifyModel型の配列を追加します。また、キャッシュ内で最後に保存された値のインデックスを管理する変数も追加します。新しいレコードが追加されるたびに、このインデックスがインクリメントされます。さらに、CIntervalWatcherクラスのインスタンスを作成し、コンストラクタ内で1日の時間間隔を設定します。以下のようになります。
class CLogifyHandlerFile : public CLogifyHandler { private: //--- Update utilities CIntervalWatcher m_interval_watcher; //--- Cache data MqlLogifyModel m_cache[]; int m_index_cache; }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CLogifyHandlerFile::CLogifyHandlerFile(void) { m_interval_watcher.SetInterval(PERIOD_D1); ArrayFree(m_cache); m_index_cache = 0; } //+------------------------------------------------------------------+
キャッシュおよび更新構造が作成されたので、次のステップに進みましょう。Emit()メソッドを修正してキャッシュを使用するようにします。
Emit()メソッドは、ログメッセージを処理し、設定された出力先(この場合はファイル)に送信する役割を担います。これを適応させ、データを直接ファイルに保存する代わりに、一時的にキャッシュに保存するようにします。キャッシュが設定された上限に達するか、定義された時間間隔(一日)に達した場合、メソッドはFlush()関数を呼び出し、蓄積されたレコードをファイルに保存します。このインターバルは、データがキャッシュ内に1日以上保持された場合でも確実に毎日保存されるようにし、さらにローテーションルーチンが毎日実行されることも可能にします。
以下は修正したコードです。
//+------------------------------------------------------------------+ //| Processes a log message and sends it to the specified destination| //+------------------------------------------------------------------+ void CLogifyHandlerFile::Emit(MqlLogifyModel &data) { //--- Checks if the configured level allows if(data.level >= this.GetLevel()) { //--- Resize cache if necessary int size = ArraySize(m_cache); if(size != m_config.messages_per_flush) { ArrayResize(m_cache, m_config.messages_per_flush); size = m_config.messages_per_flush; } //--- Add log to cache m_cache[m_index_cache++] = data; //--- Flush if cache limit is reached or update condition is met if(m_index_cache >= m_config.messages_per_flush || m_interval_watcher.Inspect()) { //--- Save cache Flush(); //--- Reset cache m_index_cache = 0; for(int i=0;i<size;i++) { m_cache[i].Reset(); } } } } //+------------------------------------------------------------------+
Flush()関数は、キャッシュデータをファイルに保存する役割を担います。このプロセスでは、ファイルを開き、ポインタを末尾に移動させ、キャッシュに保存されているすべてのレコードを書き込みます。
//+------------------------------------------------------------------+ //| Clears or completes any pending operations | //+------------------------------------------------------------------+ void CLogifyHandlerFile::Flush(void) { //--- 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; } //--- Loop through all cached messages int size = ArraySize(m_cache); for(int i=0;i<size;i++) { if(m_cache[i].timestamp > 0) { //--- Point to the end of the file and write the message FileSeek(handle_file, 0, SEEK_END); FileWrite(handle_file, m_cache[i].formated); } } //--- Size in megabytes ulong size_mb = FileSize(handle_file) / (1024 * 1024); //--- Close file FileClose(handle_file); string file_extension = this.LogFileExtensionToStr(m_config.file_extension); //--- Check if the log rotation mode is based on file size if(m_config.rotation_mode == LOG_ROTATION_MODE_SIZE) { //--- Check if the current file size exceeds the maximum configured size if(size_mb >= m_config.max_file_size_mb) { //--- Search files string file_names[]; if(this.SearchForFilesInDirectory(m_config.directory+"\\*"+file_extension,file_names)) { //--- Delete files exceeding the configured maximum number of log files int size_file = ArraySize(file_names); for(int i=size_file-1;i>=0;i--) { //--- Extract the numeric part of the file index string file_index = file_names[i]; StringReplace(file_index,file_extension,""); StringReplace(file_index,m_config.base_filename,""); //--- If the file index exceeds the maximum allowed count, delete the file if(StringToInteger(file_index) >= m_config.max_file_count) { FileDelete(m_config.directory + "\\" + file_names[i]); } } //--- Rename existing log files by incrementing their indices for(int i=m_config.max_file_count-1;i>=0;i--) { string old_file = m_config.directory + "\\" + m_config.base_filename + (i == 0 ? "" : StringFormat("%d", i)) + file_extension; string new_file = m_config.directory + "\\" + m_config.base_filename + StringFormat("%d", i + 1) + file_extension; if(FileIsExist(old_file)) { FileMove(old_file, 0, new_file, FILE_REWRITE); } } //--- Rename the primary log file to include the index "1" string new_primary = m_config.directory + "\\" + m_config.base_filename + "1" + file_extension; FileMove(log_path, 0, new_primary, FILE_REWRITE); } } } //--- Check if the log rotation mode is based on date else if(m_config.rotation_mode == LOG_ROTATION_MODE_DATE) { //--- Search files string file_names[]; if(this.SearchForFilesInDirectory(m_config.directory+"\\*"+file_extension,file_names)) { //--- Delete files exceeding the maximum configured number of log files int size_file = ArraySize(file_names); for(int i=size_file-1;i>=0;i--) { if(i < size_file - m_config.max_file_count) { FileDelete(m_config.directory + "\\" + file_names[i]); } } } } } //+------------------------------------------------------------------+
この実装により、大量のデータを処理してもEAのパフォーマンスを損なうことのない、効率的でスケーラブルなロギングソリューションが実現されました。最後に、プログラムが終了するときに、キャッシュされたすべてのデータが確実にファイルに保存されるようにする必要があります。そのためには、Close()メソッド内でFlush()メソッドを呼び出すだけで十分です。Close()メソッドはすでにCLogify基底クラスのデストラクタ内で呼び出されています。
//+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CLogifyHandlerFile::~CLogifyHandlerFile(void) { this.Close(); } //+------------------------------------------------------------------+ //| Closes the handler and releases any resources | //+------------------------------------------------------------------+ void CLogifyHandlerFile::Close(void) { //--- Save cache Flush(); } //+------------------------------------------------------------------+
キャッシュとファイルローテーションを実装することで、ディスクへの書き込み回数を減らし、ログをより効率的に保存できるようになりました。これにより、ライブラリのパフォーマンスとスケーラビリティが向上し、実際のアプリケーションにおいてより堅牢になります。しかし、これらの最適化は本当に効果があるのでしょうか。それをテストしてみましょう。
パフォーマンステスト:改善による効率の測定
最適化を実装したので、その実際の影響を測定する必要があります。パフォーマンステストをおこなうことで、キャッシュが書き込み負荷を軽減しているか、ファイルローテーションが期待どおりに機能しているかを確認できます。そのために、前回の記事で実施したのと同じテストを、ライブラリの元のバージョンと最適化されたバージョンで比較しながら実行します。
テストを実行するには、前回と同じファイルを使用しますが、フォーマッタにいくつかの変更を加える必要があります。というのも、現在では各ハンドラが独自のフォーマッタを持つようになったためです。変更点は以下のようにハイライトされています。
- 緑:コードへの追加
- 赤:コードからの削除
- 黄色:キャッシュサイズを定義するパラメータ。キャッシュが大きいほど、処理は高速になります。
//+------------------------------------------------------------------+ //| 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); handler_file.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname}] {msg}")); //--- 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); } //+------------------------------------------------------------------+
同じ日付と銘柄パラメータを使用して、ストラテジーテスターでテストを開始しましょう。

EURUSDに対して「1分足のOHLC」モデルを使用し、7日間の時間枠で実行したところ、実行時間は26秒でした。注目すべき点は、毎ティックごとに新しいログレコードが生成されるということ、そしてキャッシュは10件のメッセージを保存するよう設定されているという点です。ここで、キャッシュを100件に増やして、パフォーマンスの違いを確認してみましょう。

この変更により、同じモデル、日付、シンボル設定を維持しつつ、テスト時間を2秒短縮することができました。前回の記事で行った最初のテスト(実行時間5分11秒)と比較すると、その改善は驚くべきものです。
この結果は、小さな最適化でも効率に大きな向上をもたらす可能性があることを示しています。キャッシュとファイルローテーションの組み合わせにより、ログ管理がより機敏で信頼性の高いものとなり、これまでの選択が妥当であったことが実証されました。それでは、これらの改善を実際にどのように活用できるのかを、いくつかの使用例を通して見ていきましょう。
ログライブラリの使用例
これまでにログライブラリを改善してきましたが、いよいよ実際に活用する時です。ここでは、さまざまな形式や重要度レベルに応じたログファイルの作成方法について、実用的な例を見ていきます。
例1:ログを.logファイルと.jsonファイルに分割する
最初のシナリオでは、.log形式と.json形式の2つのログファイルを設定します。それぞれに固有のフォーマットと異なる重要度レベルを設定することで、ログの管理と分析が容易になります。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Configs MqlLogifyHandleFileConfig m_config; m_config.CreateSizeRotationConfig("expert","logs",LOG_FILE_EXTENSION_LOG,5,5,1); //--- Handler File (.log) CLogifyHandlerFile *handler_file_log = new CLogifyHandlerFile(); handler_file_log.SetConfig(m_config); handler_file_log.SetLevel(LOG_LEVEL_DEBUG); handler_file_log.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname}] {msg}")); //--- Handler File (.json) m_config.CreateNoRotationConfig("expert","logs",LOG_FILE_EXTENSION_JSON,1); CLogifyHandlerFile *handler_file_json = new CLogifyHandlerFile(); handler_file_json.SetConfig(m_config); handler_file_json.SetLevel(LOG_LEVEL_ALERT); handler_file_json.SetFormatter(new CLogifyFormatter("hh:mm:ss","{\"datetime\":\"{date_time}\", \"level\":\"{levelname}\", \"msg\":\"{msg}\"}")); //--- Add handler in base class logify.AddHandler(handler_file_log); logify.AddHandler(handler_file_json); //--- 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); } //+------------------------------------------------------------------+
ここでは、同じm_config設定変数を使い、両方のログフォーマットを定義するために必要な値だけを変更しています。これにより、設定がシンプルで再利用しやすくなります。
例2:JSONファイルにエラーのみを保存する
ここで、一歩進んで、エラーメッセージだけを保存する特定のログを設定しましょう。そのために、この.jsonファイルを保存するための別のフォルダを作成します。加えて、ログをターミナルに直接表示するためにコンソールハンドラも追加します。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Configs MqlLogifyHandleFileConfig m_config; m_config.CreateSizeRotationConfig("expert","logs",LOG_FILE_EXTENSION_LOG,5,5,1); //--- Handler File (.log) CLogifyHandlerFile *handler_file_log = new CLogifyHandlerFile(); handler_file_log.SetConfig(m_config); handler_file_log.SetLevel(LOG_LEVEL_DEBUG); handler_file_log.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname}] {msg}")); //--- Handler File (.json) m_config.CreateNoRotationConfig("expert","logs\\error",LOG_FILE_EXTENSION_JSON,1); CLogifyHandlerFile *handler_file_json = new CLogifyHandlerFile(); handler_file_json.SetConfig(m_config); handler_file_json.SetLevel(LOG_LEVEL_ERROR); handler_file_json.SetFormatter(new CLogifyFormatter("hh:mm:ss","{\"datetime\":\"{date_time}\", \"level\":\"{levelname}\", \"msg\":\"{msg}\"}")); //--- Handler Console CLogifyHandlerConsole *handler_console = new CLogifyHandlerConsole(); handler_console.SetLevel(LOG_LEVEL_DEBUG); handler_console.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname} | {origin}] {msg}")); //--- Add handler in base class logify.AddHandler(handler_file_log); logify.AddHandler(handler_file_json); logify.AddHandler(handler_console); //--- 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); } //+------------------------------------------------------------------+
この例では、3つのログハンドラを使用しています。
- .logファイル:従来のフォーマットでログを保存
- .jsonファイル:エラーメッセージのみを別フォルダに保存
- コンソール:ユーザーにとって読みやすい形でログを表示
コンソールにはより「人間向け」のフォーマッタを使うことで出力が理解しやすくなり、一方でエラーのJSONログは後から解析しやすくなります。
これらの例から、ログライブラリが実際のプロジェクトでどのように活用できるかがはっきり見えます。異なるフォーマットや重要度レベルを柔軟に作成できることで、ログ管理がしやすくなり、問題の特定やトラブルシューティングがスムーズになります。また、モジュール構造のおかげで、必要に応じてログシステムを簡単に拡張できる点も大きなメリットです。
あとは、この実装を自分のニーズに合わせてカスタマイズし、ログを常に整理された状態で簡単にアクセスできるようにするだけです。
結論
本記事では、ログライブラリを進化させ、より効率的でスケーラブル、かつ柔軟なものにしました。各ハンドラが独自のフォーマッタを持てるようにすることで、フォーマットの管理を洗練し、ローカルデバッグや監査など異なるニーズに応じてメッセージをより整理され柔軟に対応できるようになりました。
また、実行サイクルを管理するCIntervalWatcherクラスを実装し、ログの書き込みとローテーションを明確な間隔でおこなう仕組みを確立しました。さらにキャッシュを用いた書き込みの最適化により、ディスクへの操作回数を減らし、ファイルの肥大化を効果的に管理しています。これらの改善点はパフォーマンステストによって検証され、高負荷に耐えうるソリューションとして磨き上げられました。加えて、実用的な使用例も紹介し、ライブラリの導入を容易にしました。
本記事から得られる最も重要な教訓は、ログ管理をソフトウェア開発の重要な側面として扱うことの大切さです。適切に設計されたログシステムは、デバッグや監査の支援だけでなく、EAのセキュリティ、トレーサビリティ、信頼性向上にも寄与します。開発の早い段階で良いログ習慣を実装しておくことは、後々のメンテナンスやトラブルシューティングを大幅に楽にしてくれます。次回の記事では、より高度な解析のためにログをデータベースに保存する方法を探ります。それでは、次回お会いしましょう。
| ファイル名 | 詳細 |
|---|---|
| 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/Utils/IntervalWatcher.mqh | 時間間隔が経過したかどうかをチェックし、ライブラリ内でルーチンを作成できるようにする |
| Include/Logify/Logify.mqh | ログ管理、レベル、モデル、フォーマットの統合のためのコアクラス |
| Include/Logify/LogifyLevel.mqh | Logifyライブラリのログレベルを定義するファイル。詳細な制御が可能 |
| Include/Logify/LogifyModel.mqh | レベル、メッセージ、タイムスタンプ、コンテキストなどの詳細を含むログレコードをモデル化する構造 |
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/17137
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
エキスパートアドバイザーの堅牢性テスト
MQL5で取引管理者パネルを作成する(第9回):コード編成(I)
MQL5入門(第12回):初心者のためのカスタムインジケーター作成ガイド
PythonとMQL5を使用した特徴量エンジニアリング(第3回):価格の角度(2)極座標
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索