MQL5开发专属调试与性能分析工具(第一部分):高级日志记录·综合运用
📘

MQL5开发专属调试与性能分析工具(第一部分):高级日志记录·综合运用

第 3/3 篇

日志轮转的临界判定与文件清扫

EA 在 MT5 里写日志最怕单文件无限膨胀把磁盘吃满,也怕历史文件堆成山。下面这段处理思路是先看当前句柄指向的文件尺寸,超过设定上限就触发轮转。 IsFileSizeExceeded 里用 FileSize 取当前位置即字节数,再和 m_max_size_kb * 1024 比大小;m_max_size_kb 是外部传入的 KB 数,乘 1024 转成字节。句柄无效时直接返回 false,不误判。 RotateLogFiles 走的是「列目录—排序—删最旧」三步。先用 TerminalInfoString(TERMINAL_DATA_PATH) 拼出数据目录,按 m_file_prefix + "_*.log" 通配符搜文件;搜到的文件名塞进动态数组,每找到一个就 ArrayResize 扩一位。 files_to_delete 算得很直接:已有数减 m_max_files 再加 1(给即将新建的那份留位)。若大于 0,就从排序后的数组头部开始 FileDelete,删不动就 Print 报错码。外汇与贵金属 EA 跑实盘时磁盘异常可能导致日志丢失,属高风险运维点,建议先在策略测试器里验证轮转阈值。

MQL5 / C++
class=class="str">"cmt">//| IsFileSizeExceeded                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool FileLogHandler::IsFileSizeExceeded()
  {
   if(m_file_handle != INVALID_HANDLE)
     {
      class=class="str">"cmt">// Get current position(file size)
      class="type">ulong size = FileSize(m_file_handle);
      
      class=class="str">"cmt">// Check if size exceeds limit(convert KB to bytes)
      class="kw">return (size > (class="type">ulong)m_max_size_kb * class="num">1024);
     }
   
   class="kw">return false;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| RotateLogFiles                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void FileLogHandler::RotateLogFiles()
  {
   class=class="str">"cmt">// Get list of log files
   class="type">class="kw">string terminal_path = TerminalInfoString(TERMINAL_DATA_PATH);
   class="type">class="kw">string full_path = terminal_path + "\\" + m_file_path;
   class="type">class="kw">string file_pattern = m_file_prefix + "_*.log";
   
   class="type">class="kw">string files[];
   class="type">int file_count = class="num">0;
   
   class="type">long search_handle = FileFindFirst(full_path + "\\" + file_pattern, files[file_count]);
   if(search_handle != INVALID_HANDLE)
     {
      file_count++;
      
      class=class="str">"cmt">// Find all matching files
      while(FileFindNext(search_handle, files[file_count]))
        {
         file_count++;
         ArrayResize(files, file_count + class="num">1);
        }
      
      class=class="str">"cmt">// Close search handle
      FileFindClose(search_handle);
     }
   
   class=class="str">"cmt">// Resize array to actual number of found files before sorting
   ArrayResize(files, file_count);
   class=class="str">"cmt">// Sort the class="type">class="kw">string array using the custom sorter
   SortStringArray(files);
   
   class=class="str">"cmt">// Delete oldest files if we have too many
   class="type">int files_to_delete = file_count - m_max_files + class="num">1; class=class="str">"cmt">// +class="num">1 for the new file we&class="macro">#x27;ll create
   
   if(files_to_delete > class="num">0)
     {
      for(class="type">int i = class="num">0; i < files_to_delete; i++)
        {
         if(!FileDelete(m_file_path + "\\" + files[i]))
            Print("FileLogHandler: Failed to class="kw">delete old log file: ", files[i], ", error: ", GetLastError());
        }
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| SortStringArray                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void FileLogHandler::SortStringArray(class="type">class="kw">string &arr[])
  {

「日志路径清洗与单例调度的底层写法」

在 MT5 的 EA 日志模块里,文件路径分隔符经常混用 '/' 和 '\\',直接拼文件名会踩坑。下面这段 CleanPath 用 StringReplace 把所有的 '/' 统一替换成 Windows 风格的反斜杠,返回干净路径,复制进你的 FileLogHandler 类就能用。

MQL5 / C++
class="type">class="kw">string FileLogHandler::CleanPath(const class="type">class="kw">string path)
  {
   class="type">class="kw">string result = path;
   class=class="str">"cmt">// Replace all "/" with "\\"
   StringReplace(result, "/", "\\");
   class="kw">return result;
  }
逐行拆解:第1行定义成员函数,接收原路径;第3行把入参拷到局部变量 result,避免改到外部字符串;第5行调用 StringReplace 做全局替换;第6行返回规整后的路径。 CLogger 被设计成单例(static CLogger* s_instance),私有构造函数堵死了外部 new 的可能,只能走 Instance() 拿唯一对象。它内部用 m_handlers[] 动态数组挂多个日志处理器,AddHandler 往里塞,ClearHandlers 一次清空。 实盘挂多个货币对时,把 m_expert_magic 和 m_expert_name 通过 SetExpertInfo 写进单例,每条日志就能带上魔法码和 EA 名,排查跨品种异常更直观。外汇与贵金属波动剧烈,日志只是辅助,任何信号都有失效概率。

MQL5 / C++
class="type">class="kw">string FileLogHandler::CleanPath(const class="type">class="kw">string path)
  {
   class="type">class="kw">string result = path;
   class=class="str">"cmt">// Replace all "/" with "\\"
   StringReplace(result, "/", "\\");
   class="kw">return result;
  }

◍ 日志类的单例接口与分级方法

CLogger 把输出拆成 6 个等级:Debug、Info、Warn、Error、Fatal 加一个通用 Log(level, ...)。每级都配了普通版和 Format 版,Format 版接收已经拼好的 formatted_message,适合在调用前用 StringFormat 组好带变量内容的串。 静态指针 s_instance 初始为 NULL,靠 Instance() 做懒加载:第一次调 CLogger::Instance() 才 new 出来,之后每次返回同一个指针。这样 EA 里任意 .mqh 模块都能拿到同一份日志对象,不会各写各的文件。 释放走 Release():判非空后 delete s_instance,清掉堆上的实例。MT5 回测里如果 OnInit 调了 Instance()、OnDeinit 没调 Release(),可能倾向在多次加载脚本后留下悬空内存,建议在 Deinit 显式释放。

MQL5 / C++
class="type">void               Log(const LogLevel level, const class="type">class="kw">string origin, const class="type">class="kw">string message);
class="type">void               Debug(const class="type">class="kw">string origin, const class="type">class="kw">string message);
class="type">void               Info(const class="type">class="kw">string origin, const class="type">class="kw">string message);
class="type">void               Warn(const class="type">class="kw">string origin, const class="type">class="kw">string message);
class="type">void               Error(const class="type">class="kw">string origin, const class="type">class="kw">string message);
class="type">void               Fatal(const class="type">class="kw">string origin, const class="type">class="kw">string message);

class=class="str">"cmt">//--- Formatted logging methods
class="type">void               LogFormat(const LogLevel level, const class="type">class="kw">string origin, const class="type">class="kw">string formatted_message);
class="type">void               DebugFormat(const class="type">class="kw">string origin, const class="type">class="kw">string formatted_message);
class="type">void               InfoFormat(const class="type">class="kw">string origin, const class="type">class="kw">string formatted_message);
class="type">void               WarnFormat(const class="type">class="kw">string origin, const class="type">class="kw">string formatted_message);
class="type">void               ErrorFormat(const class="type">class="kw">string origin, const class="type">class="kw">string formatted_message);
class="type">void               FatalFormat(const class="type">class="kw">string origin, const class="type">class="kw">string formatted_message);
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Static instance initialization                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
CLogger *CLogger::s_instance = NULL;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Get Singleton Instance                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
CLogger* CLogger::Instance()
  {
   if(s_instance == NULL)
     {
      s_instance = new CLogger();
     }
   class="kw">return s_instance;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Release Singleton Instance                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CLogger::Release()
  {
   if(s_instance != NULL)
     {
      class="kw">delete s_instance;

日志类的构造、回收与处理器挂载

这段 CLogger 的实现把单例生命周期和输出通道管理拆得很干净。私有构造函数里把全局最低日志级别默认设为 LOG_LEVEL_DEBUG,magic 与 expert name 留空,意味着不绑定具体 EA 时也能跑基础调试输出。 析构函数直接调 ClearHandlers(),在对象销毁时顺手把所有挂载的 handler 关停并释放。ClearHandlers 里先用 CheckPointer 过滤掉无效指针,再逐个 Shutdown 并 delete,最后 ArrayResize(m_handlers, 0) 把动态数组归零——这套写法能避免 MT5 中止 EA 时的句柄泄漏。 AddHandler 用 ArraySize 取当前长度,ArrayResize 扩 1,再把指针塞进末尾,返回 bool 方便调用方判断注册是否成功。SetGlobalMinLevel 和 SetExpertInfo 是两个轻量 setter,后者把 magic(long)和 name(string)写进成员变量,后续日志条目就能带上 EA 标识。 在 MT5 里接这套日志框架时,注意 handler 一旦 AddHandler 成功就被 ClearHandlers 接管了,别在外面再手动 delete 同一指针,否则可能触发双释放。

MQL5 / C++
s_instance = NULL;
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor(Private)                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
CLogger::CLogger()
  {
   m_global_min_level = LOG_LEVEL_DEBUG;
   m_expert_magic = class="num">0;
   m_expert_name = "";
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Destructor                                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
CLogger::~CLogger()
  {
   ClearHandlers();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| AddHandler                                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CLogger::AddHandler(ILogHandler *handler)
  {
   if(CheckPointer(handler) == POINTER_INVALID)
     {
      Print("CLogger::AddHandler - Error: Invalid handler pointer.");
      class="kw">return false;
     }
   class="type">int size = ArraySize(m_handlers);
   ArrayResize(m_handlers, size + class="num">1);
   m_handlers[size] = handler;
   class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| ClearHandlers                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CLogger::ClearHandlers()
  {
   for(class="type">int i = class="num">0; i < ArraySize(m_handlers); i++)
     {
      ILogHandler *handler = m_handlers[i];
      if(CheckPointer(handler) != POINTER_INVALID)
        {
         handler.Shutdown();
         class="kw">delete handler;
        }
     }
   ArrayResize(m_handlers, class="num">0);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| SetGlobalMinLevel                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CLogger::SetGlobalMinLevel(const LogLevel level)
  {
   m_global_min_level = level;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| SetExpertInfo                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CLogger::SetExpertInfo(const class="type">long magic, const class="type">class="kw">string name)
  {
   m_expert_magic = magic;
   m_expert_name = name;
  }

「日志分发的核心实现与级别闸门」

EA 里做日志系统,最怕级别乱写把盘后排查变成噪音海。CLogger::Log 第一道闸门就是全局级别判断:传入 level 小于 m_global_min_level 或达到 LOG_LEVEL_OFF 直接 return,一条都不往外抛。 时间锚点用 TimeCurrent() 取当前服务器时间,origin 若挂了专家名会自动拼成 'Expert::模块' 格式,方便多策略同终端跑时区分来源。 真正落地是遍历 m_handlers 数组,用 CheckPointer 确认句柄有效后才调 handler.Log。这意味着你注册几个输出端(文件/邮件/终端),一次调用全部分发,ArraySize 返回几就循环几次。

MQL5 / C++
class="type">void CLogger::Log(const LogLevel level, const class="type">class="kw">string origin, const class="type">class="kw">string message)
  {
   class=class="str">"cmt">// Check global level first
   if(level < m_global_min_level || level >= LOG_LEVEL_OFF)
      class="kw">return;
   class="type">class="kw">datetime current_time = TimeCurrent();
   class="type">class="kw">string effective_origin = origin;
   if(m_expert_name != "")
      effective_origin = m_expert_name + "::" + origin;

   class=class="str">"cmt">// Dispatch to all registered handlers
   for(class="type">int i = class="num">0; i < ArraySize(m_handlers); i++)
     {
      ILogHandler *handler = m_handlers[i];
      if(CheckPointer(handler) != POINTER_INVALID)
        {
         handler.Log(current_time, level, effective_origin, message, m_expert_magic);
        }
     }
  }
上面代码逐行拆:第4行级别闸门,不合规直接退出;第6行取当前时间;第8-10行拼来源前缀;第13行起循环所有 handler,第15行指针校验防崩溃,第17行把时间/级别/来源/消息/魔术码一并推给具体处理器。 Debug/Info/Warn/Error/Fatal 五个便捷方法只是把固定级别塞进 Log,LogFormat 则先过同一道级别闸再转调 Log。外汇与贵金属杠杆高,EA 异常时日志级别设错可能漏掉致命信号,建议回测时开 Debug、实盘只留 Warn 以上。

MQL5 / C++
class="type">void CLogger::Log(const LogLevel level, const class="type">class="kw">string origin, const class="type">class="kw">string message)
  {
   class=class="str">"cmt">// Check global level first
   if(level < m_global_min_level || level >= LOG_LEVEL_OFF)
      class="kw">return;
   class="type">class="kw">datetime current_time = TimeCurrent();
   class="type">class="kw">string effective_origin = origin;
   if(m_expert_name != "")
      effective_origin = m_expert_name + "::" + origin;

   class=class="str">"cmt">// Dispatch to all registered handlers
   for(class="type">int i = class="num">0; i < ArraySize(m_handlers); i++)
     {
      ILogHandler *handler = m_handlers[i];
      if(CheckPointer(handler) != POINTER_INVALID)
        {
         handler.Log(current_time, level, effective_origin, message, m_expert_magic);
        }
     }
  }
class="type">void CLogger::Debug(const class="type">class="kw">string origin, const class="type">class="kw">string message) { Log(LOG_LEVEL_DEBUG, origin, message); }
class="type">void CLogger::Info(const class="type">class="kw">string origin, const class="type">class="kw">string message)  { Log(LOG_LEVEL_INFO, origin, message); }
class="type">void CLogger::Warn(const class="type">class="kw">string origin, const class="type">class="kw">string message)  { Log(LOG_LEVEL_WARN, origin, message); }
class="type">void CLogger::Error(const class="type">class="kw">string origin, const class="type">class="kw">string message) { Log(LOG_LEVEL_ERROR, origin, message); }
class="type">void CLogger::Fatal(const class="type">class="kw">string origin, const class="type">class="kw">string message) { Log(LOG_LEVEL_FATAL, origin, message); }
class="type">void CLogger::LogFormat(const LogLevel level, const class="type">class="kw">string origin, const class="type">class="kw">string formatted_message)
  {
   class=class="str">"cmt">// Check global level first
   if(level < m_global_min_level || level >= LOG_LEVEL_OFF)
      class="kw">return;
   Log(level, origin, formatted_message);
  }
class="type">void CLogger::DebugFormat(const class="type">class="kw">string origin, const class="type">class="kw">string formatted_message) { LogFormat(LOG_LEVEL_DEBUG, origin, formatted_message); }

◍ 给日志接口按级别分装四个入口

在 MT5 的 EA 或指标工程里,把日志输出按级别拆开能省掉很多调试时的噪音。下面这组方法把 Info、Warn、Error、Fatal 四类消息统一转交给 LogFormat,调用方只需关心内容和来源。

MQL5 / C++
class="type">void CLogger::InfoFormat(const class="type">class="kw">string origin, const class="type">class="kw">string formatted_message)  { LogFormat(LOG_LEVEL_INFO, origin, formatted_message); }
class="type">void CLogger::WarnFormat(const class="type">class="kw">string origin, const class="type">class="kw">string formatted_message)  { LogFormat(LOG_LEVEL_WARN, origin, formatted_message); }
class="type">void CLogger::ErrorFormat(const class="type">class="kw">string origin, const class="type">class="kw">string formatted_message) { LogFormat(LOG_LEVEL_ERROR, origin, formatted_message); }
class="type">void CLogger::FatalFormat(const class="type">class="kw">string origin, const class="type">class="kw">string formatted_message) { LogFormat(LOG_LEVEL_FATAL, origin, formatted_message); }
class=class="str">"cmt">//+------------------------------------------------------------------+
逐行看:第1行定义 InfoFormat,接收来源串 origin 与已格式化消息,转调 LogFormat 并写死级别为 LOG_LEVEL_INFO。第2行 WarnFormat 同理,级别换成 LOG_LEVEL_WARN,用于非致命异常。第3行 ErrorFormat 对应 LOG_LEVEL_ERROR,第4行 FatalFormat 对应 LOG_LEVEL_FATAL,后者一般留给会导致 EA 停止的逻辑。 最后一行是 MQL5 常见的分隔注释,无执行意义。实盘里建议在 Warn 以上级别才向文件落盘,Info 只走终端,能降低黄金或欧美高频策略的日志 IO 开销。

MQL5 / C++
class="type">void CLogger::InfoFormat(const class="type">class="kw">string origin, const class="type">class="kw">string formatted_message)  { LogFormat(LOG_LEVEL_INFO, origin, formatted_message); }
class="type">void CLogger::WarnFormat(const class="type">class="kw">string origin, const class="type">class="kw">string formatted_message)  { LogFormat(LOG_LEVEL_WARN, origin, formatted_message); }
class="type">void CLogger::ErrorFormat(const class="type">class="kw">string origin, const class="type">class="kw">string formatted_message) { LogFormat(LOG_LEVEL_ERROR, origin, formatted_message); }
class="type">void CLogger::FatalFormat(const class="type">class="kw">string origin, const class="type">class="kw">string formatted_message) { LogFormat(LOG_LEVEL_FATAL, origin, formatted_message); }
class=class="str">"cmt">//+------------------------------------------------------------------+

把日志框架接进EA的实战路径

在MT5里跑EA,最怕出事之后翻不出痕迹。把自定义日志框架(CLogger加Console/File两类Handler)塞进标准EA结构,能在不碰核心策略代码的前提下,把初始化、报价、信号、退出原因全部分级留痕。 输入参数里有两个开关直接管日志详略:ConsoleLogLevel默认LOG_LEVEL_INFO,意味着控制台只吐INFO及以上,DEBUG被压住;FileLogLevel默认LOG_LEVEL_DEBUG,所有调试细节进文件。这样盘面不刷屏,事后又能翻文件排错。 OnInit里先拿CLogger::Instance()单例存进全局指针g_logger,再用new挂上ConsoleLogHandler和FileLogHandler。文件Handler前缀绑了EA名加MagicNumber,目录MQL5/Files/MQL5/Logs/EA_Logs,上限2048KB、留10个文件。任何new或AddHandler失败都走delete防泄漏。 OnDeinit的收尾不能省:调CLogger::Release()会触发析构,挨个Shutdown处理器并关文件,最后把g_logger置NULL。若日志器已失效,至少用Print留一句去初始化消息,保证终端不哑火。 OnTick用静态变量last_account_log控节奏,每3600秒才记一次余额净值保证金,避免每小时几千次tick把日志淹掉。MA交叉检测复制数据不足就Warn并返回,交叉成立才Info级报信号——这套分级在live盘里能帮你快速分辨是报价异常还是真信号。 OnChartEvent同样先查g_logger指针,无效直接return。switch把CHARTEVENT_KEYDOWN、MOUSE_MOVE、OBJECT_CLICK甚至CHARTEVENT_CUSTOM+1解析成名号,Debug级记lparam/dparam/sparam,开发期追踪图表交互流非常实用。

MQL5 / C++
class=class="str">"cmt">// Input parameters
input class="type">int      MagicNumber = class="num">654321;        class=class="str">"cmt">// EA Magic Number
input class="type">class="kw">double   LotSize     = class="num">0.01;          class=class="str">"cmt">// Fixed lot size
input class="type">int      StopLossPips = class="num">50;           class=class="str">"cmt">// Stop Loss in pips
input class="type">int      TakeProfitPips = class="num">100;        class=class="str">"cmt">// Take Profit in pips

「EA初始化时挂上双路日志」

在 MT5 的 EA 初始化阶段把日志系统接好,能让你后续排查外汇或贵金属策略问题时有据可查。下面这段 OnInit 逻辑先拿到全局 CLogger 单例,校验指针无效就直接返回 INIT_FAILED,避免空指针跑后续。 控制台与文件两套 handler 分开配置级别:输入参数里 ConsoleLogLevel 默认 LOG_LEVEL_INFO,FileLogLevel 默认 LOG_LEVEL_DEBUG,意味着文件里能拿到更细的调试轨迹,终端只显示概要。 文件 handler 的构造值得盯一下:目录相对 MQL5/Files 设在 MQL5/Logs/EA_Logs,前缀用程序名加 MagicNumber 拼出,单文件上限 2048 KB(约 2MB),最多保留 10 个滚动文件。这套参数直接决定了你回测或实盘跑一周后,日志会不会把磁盘写爆。 别把日志级别当摆设 把 FileLogLevel 从 DEBUG 降到 INFO,能砍掉大量Tick级噪声,但也可能让你漏掉某次止损判定前的临界变量。外汇与贵金属波动快,建议先 DEBUG 跑三天再按需调级。

MQL5 / C++
input LogLevel ConsoleLogLevel = LOG_LEVEL_INFO; class=class="str">"cmt">// Minimum level for console output
input LogLevel FileLogLevel = LOG_LEVEL_DEBUG;   class=class="str">"cmt">// Minimum level for file output
class=class="str">"cmt">// Global logger pointer(optional, can use CLogger::Instance() directly)
CLogger *g_logger = NULL;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- Get the logger instance
   g_logger = CLogger::Instance();
   if(CheckPointer(g_logger) == POINTER_INVALID)
     {
       Print("Critical Error: Failed to get Logger instance!");
       class="kw">return(INIT_FAILED);
     }
class=class="str">"cmt">//--- Set EA information for context in logs
   g_logger.SetExpertInfo(MagicNumber, MQL5InfoString(MQL5_PROGRAM_NAME));
class=class="str">"cmt">//--- Configure Handlers ---
   class=class="str">"cmt">// class="num">1. Console Handler
   ConsoleLogHandler *console_handler = new ConsoleLogHandler(ConsoleLogLevel);
   if(CheckPointer(console_handler) != POINTER_INVALID)
     {
       class=class="str">"cmt">// Optionally customize format
       class=class="str">"cmt">// console_handler.SetFormat("[{level}] {message}"); 
       if(!g_logger.AddHandler(console_handler))
         {
           Print("Warning: Failed to add ConsoleLogHandler.");
           class="kw">delete console_handler; class=class="str">"cmt">// Clean up if not added
         }
     }
   else
     {
       Print("Warning: Failed to create ConsoleLogHandler.");
     }
   class=class="str">"cmt">// class="num">2. File Handler
   class="type">class="kw">string log_prefix = MQL5InfoString(MQL5_PROGRAM_NAME) + "_" + IntegerToString(MagicNumber);
   FileLogHandler *file_handler = new FileLogHandler("MQL5/Logs/EA_Logs", class=class="str">"cmt">// Directory relative to MQL5/Files
                                                    log_prefix,           class=class="str">"cmt">// File name prefix
                                                    FileLogLevel,         class=class="str">"cmt">// Minimum level to log to file
                                                    "[{time}] {level} ({origin}): {message}", class=class="str">"cmt">// Format
                                                    class="num">2048, class=class="str">"cmt">// Max file size in KB(e.g., 2MB)
                                                    class="num">10);  class=class="str">"cmt">// Max number of log files to keep
   if(CheckPointer(file_handler) != POINTER_INVALID)
     {

◍ EA初始化时的日志处理器挂载细节

在 MT5 的 EA 初始化阶段,把日志 handler 挂进全局 logger 时,必须判断 AddHandler 的返回值。若文件日志句柄加不进去,当场 delete 掉防止内存泄漏,这种防御写法在跑长周期黄金策略时很关键。 文件 handler 构造里有个容易被忽略的点:相对目录是 MQL5/Files 下的子路径,代码里写的是 "MQL5/Logs/EA_Logs",实际文件会落到 数据文件夹/Files/MQL5/Logs/EA_Logs 里。单文件上限 2048 KB(约 2MB),最多保留 10 个轮转文件,外汇与贵金属波动大、日志频繁,这个容量偏向不够用,可改大。 初始化成功后会用 g_logger.Info 打一条 "Expert Advisor initialized successfully.",再用 Debug 级把 Lots、SL、TP、ConsoleLevel、FileLevel 全打印出来。开 MT5 跑一眼Experts标签下的日志,就能确认参数是否按输入值生效。

MQL5 / C++
if(!g_logger.AddHandler(file_handler))
  {
   Print("Warning: Failed to add FileLogHandler.");
   class="kw">delete file_handler; class=class="str">"cmt">// Clean up if not added
  }
 }
else
 {
  Print("Warning: Failed to create FileLogHandler.");
 }
class=class="str">"cmt">//--- Log initialization message
g_logger.Info(__FUNCTION__, "Expert Advisor initialized successfully.");
g_logger.Debug(__FUNCTION__, StringFormat("Settings: Lots=%.2f, SL=%d, TP=%d, ConsoleLevel=%s, FileLevel=%s",
                                        LotSize, StopLossPips, TakeProfitPips,
                                        EnumToString(ConsoleLogLevel),
                                        EnumToString(FileLogLevel)));
class=class="str">"cmt">//--- succeed
 class="kw">return(INIT_SUCCEEDED);
}
g_logger = CLogger::Instance();
g_logger.SetExpertInfo(MagicNumber, MQL5InfoString(MQL5_PROGRAM_NAME));
ConsoleLogHandler *console_handler = new ConsoleLogHandler(ConsoleLogLevel);
if(!g_logger.AddHandler(console_handler))
 class=class="str">"cmt">// class="num">2. File Handler
 class="type">class="kw">string log_prefix = MQL5InfoString(MQL5_PROGRAM_NAME) + "_" + IntegerToString(MagicNumber);
 FileLogHandler *file_handler = new FileLogHandler("MQL5/Logs/EA_Logs", class=class="str">"cmt">// Directory relative to MQL5/Files
                                                 log_prefix,             class=class="str">"cmt">// File name prefix
                                                 FileLogLevel,           class=class="str">"cmt">// Minimum level to log to file
                                                 "[{time}] {level} ({origin}): {message}", class=class="str">"cmt">// Format
                                                 class="num">2048, class=class="str">"cmt">// Max file size in KB(e.g., 2MB)
                                                 class="num">10);  class=class="str">"cmt">// Max number of log files to keep
g_logger.Info(__FUNCTION__, "Expert Advisor initialized successfully.");
g_logger.Debug(__FUNCTION__, StringFormat("Settings: ..."));

EA 卸载时的日志与指针回收

在 MQL5 的 EA 生命周期里,OnDeinit 是最后一道关口。很多自行封装日志模块的写法,会在这里先判断全局 logger 指针是否还有效,再决定走自定义通道还是退回系统 Print。 下面这段是典型的卸载处理:先 CheckPointer(g_logger) 排除 POINTER_INVALID,随后用 switch 把 reason 码映射成可读字符串——REASON_ACCOUNT 到 REASON_CLOSE 共 9 种官方退出原因,覆盖换账户、图表周期切换、模板加载、终端关闭等场景。映射完通过 g_logger.Info 写一行带函数名和原始码号的记录,再调 CLogger::Release() 释放所有 handler,最后把 g_logger 置 NULL。 若指针已失效,则直接用 Print 提示 logger 在 Deinit 阶段已无效,并在末尾补一句标准日志输出程序名与 reason 码,防止自定义日志链断裂后完全失声。 实盘里外汇与贵金属品种跳空频繁,终端因断线重连触发的 REASON_ACCOUNT 可能一天出现数次,这类退出原因被记全,后面排查 EA 莫名停跑会更省时间。

MQL5 / C++
if(CheckPointer(g_logger) != POINTER_INVALID)
  {
    class="type">class="kw">string reason_str = "Unknown reason";
    class="kw">switch(reason)
      {
       case REASON_ACCOUNT: reason_str = "Account change"; class="kw">break;
       case REASON_CHARTCHANGE: reason_str = "Chart symbol or period change"; class="kw">break;
       case REASON_CHARTCLOSE: reason_str = "Chart closed"; class="kw">break;
       case REASON_PARAMETERS: reason_str = "Input parameters changed"; class="kw">break;
       case REASON_RECOMPILE: reason_str = "Recompiled"; class="kw">break;
       case REASON_REMOVE: reason_str = "EA removed from chart"; class="kw">break;
       case REASON_TEMPLATE: reason_str = "Template applied"; class="kw">break;
       case REASON_CLOSE: reason_str = "Terminal closed"; class="kw">break;
      }
    g_logger.Info(__FUNCTION__, "Expert Advisor shutting down. Reason: " + reason_str + " (" + IntegerToString(reason) + ")");
    
    class=class="str">"cmt">// Release the logger instance(this calls Shutdown() on all handlers)
    CLogger::Release();
    g_logger = NULL; class=class="str">"cmt">// Set pointer to NULL after release
  }
 else
  {
    Print("Logger instance was already invalid during Deinit.");
  }
class=class="str">"cmt">//--- Print to standard log just in case logger failed
  Print(MQL5InfoString(MQL5_PROGRAM_NAME) + ": Deinitialized. Reason code: " + IntegerToString(reason));
}

「OnTick 里的日志兜底与双均线交叉判定」

EA 在 OnTick 中要先防住日志实例意外失效的情况。若检测到 logger 无效,代码会尝试重新跑一次 OnInit,若仍返回非 INIT_SUCCEEDED,则直接 ExpertRemove 并 return,把 EA 停掉——这是防御式写法,正常情况 OnInit 成功就不会走到这。 拿到 tick 用 SymbolInfoTick 填 MqlTick 结构,把 time、bid、ask、volume_real 打进 Debug 日志;失败则走 Warn 并附 GetLastError 码,方便你开 MT5 看专家标签排查报价通道中断。 示例逻辑用两条 SMA 做交叉:快线周期 10、慢线周期 50,均取收盘价,CopyBuffer 各取最近 3 根。若任一拷贝不足 3 根直接 return,等数据齐。 交叉判定只看索引 1 与 2:ma_fast[1]>ma_slow[1] 且 ma_fast[2]<=ma_slow[2] 为向上交叉,反之为向下。命中后仅写 Info 日志提示买卖倾向信号,真实下单得你自己补。外汇与贵金属波动剧烈,这类裸均线交叉在震荡市假信号概率偏高,仅作验证用。

MQL5 / C++
  class=class="str">"cmt">// Attempt to re-initialize logger if it became invalid unexpectedly
  class=class="str">"cmt">// This is defensive coding, ideally it shouldn&class="macro">#x27;t happen if OnInit succeeded.
  Print("Error: Logger instance invalid in OnTick! Attempting re-init...");
  if(OnInit() != INIT_SUCCEEDED)
    {
     Print("Critical Error: Failed to re-initialize logger in OnTick. Stopping EA.");
     ExpertRemove(); class=class="str">"cmt">// Stop the EA
     class="kw">return;
    }
  }
class=class="str">"cmt">//--- Log tick arrival
  class="type">MqlTick latest_tick;
  if(SymbolInfoTick(_Symbol, latest_tick))
   {
    g_logger.Debug(__FUNCTION__, StringFormat("New Tick: Time=%s, Bid=%.5f, Ask=%.5f, Volume=%d",
                                                TimeToString(latest_tick.time, TIME_DATE|TIME_SECONDS),
                                                latest_tick.bid, latest_tick.ask, (class="type">int)latest_tick.volume_real));
   }
  else
   {
    g_logger.Warn(__FUNCTION__, "Failed to get latest tick info. Error: " + IntegerToString(GetLastError()));
   }
class=class="str">"cmt">//--- Example Logic: Check for a simple crossover
  class=class="str">"cmt">// Note: Use more robust indicator handling in a real EA
  class="type">class="kw">double ma_fast[], ma_slow[];
  class="type">int copied_fast = CopyBuffer(iMA(_Symbol, _Period, class="num">10, class="num">0, MODE_SMA, PRICE_CLOSE), class="num">0, class="num">0, class="num">3, ma_fast);
  class="type">int copied_slow = CopyBuffer(iMA(_Symbol, _Period, class="num">50, class="num">0, MODE_SMA, PRICE_CLOSE), class="num">0, class="num">0, class="num">3, ma_slow);
  
  if(copied_fast < class="num">3 || copied_slow < class="num">3)
   {
    g_logger.Warn(__FUNCTION__, "Failed to copy enough indicator data.");
    class="kw">return; class=class="str">"cmt">// Not enough data yet
   }
   
  class=class="str">"cmt">// ArraySetAsSeries might be needed depending on how you access indices
  class=class="str">"cmt">// ArraySetAsSeries(ma_fast, true);
  class=class="str">"cmt">// ArraySetAsSeries(ma_slow, true);
  
  class="type">bool cross_up = ma_fast[class="num">1] > ma_slow[class="num">1] && ma_fast[class="num">2] <= ma_slow[class="num">2];
  class="type">bool cross_down = ma_fast[class="num">1] < ma_slow[class="num">1] && ma_fast[class="num">2] >= ma_slow[class="num">2];
  if(cross_up)
   {
    g_logger.Info(__FUNCTION__, "MA Cross Up detected. Potential Buy Signal.");
    class=class="str">"cmt">// --- Add trading logic here ---
    class=class="str">"cmt">// Example: SendBuyOrder();
   }
  else if(cross_down)
   {
    g_logger.Info(__FUNCTION__, "MA Cross Down detected. Potential Sell Signal.");

◍ EA 心跳日志与防御性停机的写法

在 OnTick 里周期性落账是排查滑点与外盘断连的最笨也最稳的办法。下面这段用静态变量 last_account_log 卡住 3600 秒间隔,每小时打一次余额、净值、已用保证金、空闲保证金,四个数齐了才知道 EA 是不是在裸奔。 静态变量只初始化一次,TimeCurrent() 减去它若大于等于 3600 就触发日志并刷新时间戳,避免在每跳里刷屏把终端卡死。外汇与贵金属杠杆高,保证金字段异常往往比价格异动早几秒暴露风险,这类日志值得常开。 日志器本身也可能在极端情况下变成无效指针。代码里用 CheckPointer(g_logger) == POINTER_INVALID 做兜底,一旦命中就重跑 OnInit(),若仍失败直接 ExpertRemove() 把 EA 摘掉,比让它带着坏指针继续发单安全得多。 每跳再用 SymbolInfoTick 抓最新买卖价与真实成交量,Bid/Ask 留 5 位小数才够看贵金属点差;取不到就走 Warn 把 GetLastError() 码打出来,别让静默失败糊弄你。

MQL5 / C++
   class=class="str">"cmt">// --- Add trading logic here ---
   class=class="str">"cmt">// Example: SendSellOrder();
   }
   
  class=class="str">"cmt">// Log account info periodically
  class="kw">static class="type">class="kw">datetime last_account_log = class="num">0;
  if(TimeCurrent() - last_account_log >= class="num">3600) class=class="str">"cmt">// Log every hour
   {
      g_logger.Info(__FUNCTION__, StringFormat("Account Update: Balance=%.2f, Equity=%.2f, Margin=%.2f, FreeMargin=%.2f",
                                                                 AccountInfoDouble(ACCOUNT_BALANCE),
                                                                 AccountInfoDouble(ACCOUNT_EQUITY),
                                                                 AccountInfoDouble(ACCOUNT_MARGIN),
                                                                 AccountInfoDouble(ACCOUNT_MARGIN_FREE)));
      last_account_log = TimeCurrent();
   }
}
class=class="str">"cmt">//--- Ensure logger is valid
   if(CheckPointer(g_logger) == POINTER_INVALID)
    {
      class=class="str">"cmt">// Attempt to re-initialize logger if it became invalid unexpectedly
      class=class="str">"cmt">// This is defensive coding, ideally it shouldn&class="macro">#x27;t happen if OnInit succeeded.
      Print("Error: Logger instance invalid in OnTick! Attempting re-init...");
      if(OnInit() != INIT_SUCCEEDED)
        {
         Print("Critical Error: Failed to re-initialize logger in OnTick. Stopping EA.");
         ExpertRemove(); class=class="str">"cmt">// Stop the EA
         class="kw">return;
        }
     }
class=class="str">"cmt">//--- Log tick arrival
   class="type">MqlTick latest_tick;
   if(SymbolInfoTick(_Symbol, latest_tick))
    {
      g_logger.Debug(__FUNCTION__, StringFormat("New Tick: Time=%s, Bid=%.5f, Ask=%.5f, Volume=%d",
                                                                 TimeToString(latest_tick.time, TIME_DATE|TIME_SECONDS),
                                                                 latest_tick.bid, latest_tick.ask, (class="type">int)latest_tick.volume_real));
    }
   else
    {
      g_logger.Warn(__FUNCTION__, "Failed to get latest tick info. Error: " + IntegerToString(GetLastError()));
    }
class=class="str">"cmt">//--- Example Logic: Check for a simple crossover
   class=class="str">"cmt">// Note: Use more robust indicator handling in a real EA
   class="type">class="kw">double ma_fast[], ma_slow[];

双均线交叉的缓冲拷贝与信号判定

在 EA 里抓 10/50 周期 SMA 交叉,第一步是把指标缓冲拉进数组。下面这段代码对当前图表品种和周期分别建了 10 期与 50 期收盘价 SMA,各取最近 3 根 K 线数据到 ma_fast、ma_slow。 复制环节必须校验返回值:若 copied_fast 或 copied_slow 小于 3,说明数据没拉满,直接打日志并返回,避免用残缺数组去比大小。这一道防御能挡掉不少初启动时的越界报错。 交叉逻辑只看最近两根:cross_up 定义为 ma_fast[1] > ma_slow[1] 且 ma_fast[2] <= ma_slow[2],即上一根完成金叉;cross_down 反之。命中后在日志标出潜在买/卖信号,具体下单函数留空给你接。 账户巡检另算节奏:用静态变量 last_account_log 卡住间隔,每满 3600 秒(1 小时)才输一次余额、净值、占用保证金、自由保证金。外汇与贵金属杠杆高,这类定时快照有助于复盘滑点与回撤,但信号本身只代表概率倾向,不承诺方向。

MQL5 / C++
class="type">int copied_fast = CopyBuffer(iMA(_Symbol, _Period, class="num">10, class="num">0, MODE_SMA, PRICE_CLOSE), class="num">0, class="num">0, class="num">3, ma_fast);
class="type">int copied_slow = CopyBuffer(iMA(_Symbol, _Period, class="num">50, class="num">0, MODE_SMA, PRICE_CLOSE), class="num">0, class="num">0, class="num">3, ma_slow);

if(copied_fast < class="num">3 || copied_slow < class="num">3)
  {
    g_logger.Warn(__FUNCTION__, "Failed to copy enough indicator data.");
    class="kw">return; class=class="str">"cmt">// Not enough data yet
  }

class=class="str">"cmt">// ArraySetAsSeries might be needed depending on how you access indices
class=class="str">"cmt">// ArraySetAsSeries(ma_fast, true);
class=class="str">"cmt">// ArraySetAsSeries(ma_slow, true);

class="type">bool cross_up = ma_fast[class="num">1] > ma_slow[class="num">1] && ma_fast[class="num">2] <= ma_slow[class="num">2];
class="type">bool cross_down = ma_fast[class="num">1] < ma_slow[class="num">1] && ma_fast[class="num">2] >= ma_slow[class="num">2];
if(cross_up)
  {
    g_logger.Info(__FUNCTION__, "MA Cross Up detected. Potential Buy Signal.");
    class=class="str">"cmt">// --- Add trading logic here ---
    class=class="str">"cmt">// Example: SendBuyOrder();
  }
else if(cross_down)
  {
    g_logger.Info(__FUNCTION__, "MA Cross Down detected. Potential Sell Signal.");
    class=class="str">"cmt">// --- Add trading logic here ---
    class=class="str">"cmt">// Example: SendSellOrder();
  }
class=class="str">"cmt">// Log account info periodically
class="kw">static class="type">class="kw">datetime last_account_log = class="num">0;
if(TimeCurrent() - last_account_log >= class="num">3600) class=class="str">"cmt">// Log every hour
  {
    g_logger.Info(__FUNCTION__, StringFormat("Account Update: Balance=%.2f, Equity=%.2f, Margin=%.2f, FreeMargin=%.2f",
                                            AccountInfoDouble(ACCOUNT_BALANCE),
                                            AccountInfoDouble(ACCOUNT_EQUITY),
                                            AccountInfoDouble(ACCOUNT_MARGIN),
                                            AccountInfoDouble(ACCOUNT_MARGIN_FREE)));
    last_account_log = TimeCurrent();
  }

「把图表事件接进日志流」

在 MT5 的 EA 或指标里,OnChartEvent 是捕获键盘、鼠标、对象点击以及自定义事件的入口。若想用 AIGC 工具复盘自己的手动操作习惯,第一步就是把这些事件落日志,而不是只在屏幕上闪一下。 下面这段处理函数先判了全局日志器指针是否有效,无效直接 return,避免空指针崩脚本。随后用 switch 把常见图表事件映射成可读字符串:CHARTEVENT_KEYDOWN 记为 KeyDown,CHARTEVENT_MOUSE_MOVE 记为 MouseMove,CHARTEVENT_OBJECT_CLICK 记为 ObjectClick,CHARTEVENT_CUSTOM+1 作为自定义事件示例记为 CustomEvent_1。 真正写盘的是 g_logger.Debug 那行,用 StringFormat 把事件 ID、名称、lparam(整型参数,如按键码)、dparam(双精度,保留 5 位小数)、sparam(字符串,如对象名)拼成一条记录。回测或实盘时,你能从日志里看到某次鼠标移动到 X 坐标对应的 dparam 精度到 0.00001,据此判断自己是不是总在波动率突增的瞬间手贱平仓。 外汇与贵金属杠杆高,手动干预频繁,把事件流接出来后,用小布知识库跑一遍,可能倾向发现某些点击集中在欧美盘交接的滑点高发段。

MQL5 / C++
class="type">void OnChartEvent(const class="type">int id,
                     const class="type">long &lparam,
                     const class="type">class="kw">double &dparam,
                     const class="type">class="kw">string &sparam)
  {
class=class="str">"cmt">//--- Ensure logger is valid
   if(CheckPointer(g_logger) == POINTER_INVALID) class="kw">return;

class=class="str">"cmt">//--- Log chart events
   class="type">class="kw">string event_name = "Unknown Chart Event";
   class="kw">switch(id)
     {
      case CHARTEVENT_KEYDOWN: event_name = "KeyDown"; class="kw">break;
      case CHARTEVENT_MOUSE_MOVE: event_name = "MouseMove"; class="kw">break;
      class=class="str">"cmt">// Add other CHARTEVENT cases as needed
      case CHARTEVENT_OBJECT_CLICK: event_name = "ObjectClick"; class="kw">break;
      case CHARTEVENT_CUSTOM+class="num">1: event_name = "CustomEvent_1"; class="kw">break; class=class="str">"cmt">// Example custom event
     }

   g_logger.Debug(__FUNCTION__, StringFormat("Chart Event: ID=%d(%s), lparam=%d, dparam=%.5f, sparam=&class="macro">#x27;%s&class="macro">#x27;",
                                             id, event_name, lparam, dparam, sparam));
  }

◍ 给EA日志做一层可控封装

直接用 Print() 丢信息,EA跑久了控制台就是一锅粥。自己包一层日志框架,先把严重性分级做掉:DEBUG、INFO、WARN、ERROR 按优先级过滤,低权重消息在实盘里可以直接静默,只留真正要命的。 多输出也得打通。同一行日志既能进 MT5 控制台,也能落本地文件,甚至转发到 socket;文件侧再挂一个轮转逻辑,按大小或按天切分,避免单文件无限膨胀把 VPS 磁盘吃满。 自动带时间戳、EA名、魔法码这些上下文,排错时不用反推‘这条是谁打的’。全局开关一个宏搞定,单个 handler 也能单独禁掉——复杂策略联调时,这种集中控制能省掉大量改代码重编译的时间。

把工具请下神坛

自定义日志框架搭好后,零碎的 Print() 调用可以退场了。DEBUG 到 OFF 的 LogLevel 枚举、ILogHandler 接口、Console 与 File 两类处理器、单例 CLogger,以及 9.22 KB 的 LoggingExampleEA.mq5 示例,已经构成一套可切换输出目标、按日轮转或限大小落盘、保留可配置历史文件的闭环。 文件体积很实:LogLevels.mqh 0.84 KB、ILogHandler.mqh 1.08 KB、ConsoleLogHandler.mqh 5.12 KB、FileLogHandler.mqh 14.38 KB、CLogger.mqh 8.51 KB。开 MT5 把 ZIP 里的六个文件拖进 Include 与 EA 目录,编译 LoggingExampleEA 就能看到 Experts 标签页与日志文件双路输出。 后续若引入性能分析和单元测试,逻辑漏洞与性能瓶颈可能在实盘前就被捞出。外汇与贵金属波动剧烈、杠杆高风险,任何框架都不替你扛回撤,但能让排错从瞎猜变成翻记录。 下一步值得自己改一处:把 FileLogHandler 的历史文件保留数从默认调小,观察磁盘占用与复盘成本的平衡点。

常见问题

设定单文件大小或行数临界值(如超过 5MB 或 5000 行),触发后更名归档并清扫超限的旧日志,避免磁盘被撑爆。
在写入前用字符串替换统一分隔符、剔除首尾空格与非法符号,并用单例调度保证全程序只跑一个路径清洗实例。
可以。把日志接入小布后,它能按级别筛选 ERROR/WARN 并推送异动摘要,省去你手动翻文件。
构造函数里挂好输出处理器,析构时 flush 残留缓冲并释放文件句柄,防止进程退出丢日志或占坑。
分装 DEBUG/INFO/WARN/ERROR 入口后,调用处一眼知轻重,且级别闸门能在底层直接拦掉低优先级写入,省 IO。