查找错误和记录·进阶篇
📘

查找错误和记录·进阶篇

第 2/3 篇

◍ 把日志写入改成类封装

MQL5 是面向对象语言,运行速度和 C++ 差距不大,所以把记录逻辑直接做成 MQL5 类最划算,不用绕去调外部模块。 这个类要接管几件具体事:记住日志文件落盘路径、按「名称+日期时间」自动建文件、把传进来的参数拼成一行日志、给每条消息打上时间和分类、还能指定颜色,并且做消息缓存——每 n 秒或每 n 条才真正写盘一次,减少 IO 抖动。 作者另写了一个叫 LogMon 的 Windows 小工具(C++ 实现)专门看这类日志,但核心记录机制先在 MQL5 侧用类解决,后面直接复用。

把日志落盘的类怎么接进EA

把 CLogger 类单独存成 CLogger.mqh,放在 MQL5 的 Include 路径下,EA、指标或脚本里用 #include 引一下就能用。类内部用 MAX_CACHE_SIZE 10000 条做缓存上限,MAX_FILE_SIZEMB 10 限制单文件 10MB,超出就自动开带序号的新文件。 实例化后先调 SetSetting() 传项目名和文件名,可选参数里 default_category 给缺省分类、cache_time_limit 是缓存秒数;传 0 表示每条都直接写盘。示例里设了缺省分类 "Comment" 和 60 秒缓存,日志最终落到 Client_Terminal_dir\MQL5\Files\log\log_MyProject\log_myLog_date.txt。 SetSetting 之后必须调 init() 才会真正建文件路径、开句柄并做大小检查,指针移到末尾待写。程序退出前调 flush() 和 deinit() 把残余缓存刷进文件并关句柄,否则可能丢最后一段日志。 实测一个脚本 3 秒内循环写 10 万条消息,生成 2 个文件,全程未卡终端,说明这种缓存+翻文件机制在高频记录下够轻量。外汇和贵金属策略跑这类日志需注意:高频写盘在 VPS 磁盘 IO 差时可能拖慢 tick 处理,属高风险环境下的隐性成本。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                      Clogger.mqh |
class=class="str">"cmt">//|                                                             ProF |
class=class="str">"cmt">//|                                                          http:// |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "ProF"
class="macro">#class="kw">property link      "http:class=class="str">"cmt">//"
class=class="str">"cmt">// 高速缓存的最大尺寸 (数量)
class="macro">#define MAX_CACHE_SIZE   class="num">10000
class=class="str">"cmt">// 以MB为单位最大文件尺寸
class="macro">#define MAX_FILE_SIZEMB class="num">10
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|   Logger                                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CLogger
{
class="kw">private:
   class="type">class="kw">string            project,file;             class=class="str">"cmt">// 项目的名称和日志文件
   class="type">class="kw">string            logCache[MAX_CACHE_SIZE]; class=class="str">"cmt">// 最大缓存大小
   class="type">int               sizeCache;                class=class="str">"cmt">// 缓存计数器
   class="type">int               cacheTimeLimit;           class=class="str">"cmt">// 缓存时间
   class="type">class="kw">datetime          cacheTime;                class=class="str">"cmt">// 最后刷缓存至文件的时间
   class="type">int               handleFile;               class=class="str">"cmt">// 日志文件句柄
   class="type">class="kw">string            defCategory;              class=class="str">"cmt">// 省缺类别
   class="type">void              writeLog(class="type">class="kw">string log_msg); class=class="str">"cmt">// 写消息到日志或文件, 并刷缓存
class="kw">public:
   class="type">void              CLogger(class="type">void){cacheTimeLimit=class="num">0; cacheTime=class="num">0; sizeCache=class="num">0;};    class=class="str">"cmt">// 构造器
   class="type">void             ~CLogger(class="type">void){};                                               class=class="str">"cmt">// 析构器
   class="type">void              SetSetting(class="type">class="kw">string project,class="type">class="kw">string file_name,
                     class="type">class="kw">string default_category="",class="type">int cache_time_limit=class="num">0); class=class="str">"cmt">// 设置
   class="type">void              init();                  class=class="str">"cmt">// 初始化, 打开文件写
   class="type">void              deinit();                class=class="str">"cmt">// 关闭文件
   class="type">void              write(class="type">class="kw">string msg,class="type">class="kw">string category="");                                         class=class="str">"cmt">// 生成消息
   class="type">void              write(class="type">class="kw">string msg,class="type">class="kw">string category,class="type">class="kw">color colorOfMsg,class="type">class="kw">string file="",class="type">int line=class="num">0); class=class="str">"cmt">// 生成消息
   class="type">void              write(class="type">class="kw">string msg,class="type">class="kw">string category,class="type">uchar red,class="type">uchar green,class="type">uchar blue,
                     class="type">class="kw">string file="",class="type">int line=class="num">0);                                            class=class="str">"cmt">// 生成消息
   class="type">void              flush(class="type">void);             class=class="str">"cmt">// 刷缓存至文件
};

「给日志类加上文件轮转与缓存控制」

在 MT5 里跑长周期黄金或欧美策略时,日志文件如果不加限制会无限膨胀,拖慢 FileWrite 效率。下面这段 CLogger 实现把单文件上限钉在 MAX_FILE_SIZEMB(以 MB 计,代码内按 *1000000 换算成字节)这个常量上,超了就自动序号新建。

构造函数里 default_category 留空时,默认归到「注释」类;cache_time_limit 传 0 代表禁用缓存、每次直接落盘。init() 用 TimeCurrent 取系统时间拼出 log_项目名/log_文件名_年月日.txt 的路径,并以 FILE_WRITEFILE_READFILE_UNICODEFILE_TXTFILE_SHARE_READ 打开,允许其他程序同时读。

writeLog() 的缓存逻辑值得抄:cacheTimeLimit 非零且距上次未超时限、缓存数组没满,就先塞进 logCache;否则 flush 清盘。每次写完后用 FileTell 比对文件大小,超了就 deinit+init 滚动到新文件。 让小布替你跑这套 把 MAX_FILE_SIZEMB 设成 5,挂一个每 tick 写日志的 EA 在 XAUUSD 上,半天就能看到 .txt 自动跳到 _1、_2,验证轮转是否按字节触发而非按时间。外汇与贵金属波动剧烈,日志 IO 虽小但高频仍可能卡顿,实盘前请在策略测试器跑满 24 小时。

MQL5 / C++
class="type">class="kw">string default_category="",class="type">int cache_time_limit=class="num">0)
{
project=project_name;             class=class="str">"cmt">// 项目名
file=file_name;                   class=class="str">"cmt">// 文件名
cacheTimeLimit=cache_time_limit;  class=class="str">"cmt">// 缓存时间
if(default_category=="")          class=class="str">"cmt">// 设置省缺类别
{  defCategory="注释";   }
else
{defCategory = default_category;}
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  初始化                                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CLogger::init(class="type">void)
{
class="type">class="kw">string path;
class="type">MqlDateTime date;
class="type">int i=class="num">0;
TimeToStruct(TimeCurrent(),date);                            class=class="str">"cmt">// 得到当前时间
StringConcatenate(path,"log\\log_",project,"\\log_",file,"_",
date.year,date.mon,date.day);              class=class="str">"cmt">// 生成文件名和路径
handleFile=FileOpen(path+".txt",FILE_WRITE|FILE_READ|
FILE_UNICODE|FILE_TXT|FILE_SHARE_READ);  class=class="str">"cmt">// 打开或创建新文件
class="kw">while(FileSize(handleFile)>(MAX_FILE_SIZEMB*class="num">1000000))        class=class="str">"cmt">// 检查文件大小
{
class=class="str">"cmt">// 打开或创建新日志文件
i++;
FileClose(handleFile);
handleFile=FileOpen(path+"_"+(class="type">class="kw">string)i+".txt",
FILE_WRITE|FILE_READ|FILE_UNICODE|FILE_TXT|FILE_SHARE_READ);
}
FileSeek(handleFile,class="num">0,SEEK_END);                             class=class="str">"cmt">// 设置指针到文件尾
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|   去初始化                                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CLogger::deinit(class="type">void)
{
FileClose(handleFile); class=class="str">"cmt">// 关闭文件
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|   写信息至文件或缓存                                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CLogger::writeLog(class="type">class="kw">string log_msg)
{
if(cacheTimeLimit!=class="num">0) class=class="str">"cmt">// 检查缓存是否允许
{
if((sizeCache<MAX_CACHE_SIZE-class="num">1 && TimeCurrent()-cacheTime<cacheTimeLimit)
|| sizeCache==class="num">0) class=class="str">"cmt">// 检查缓存时间是否超出或到达缓存限制
{
class=class="str">"cmt">// 写文件至缓存
logCache[sizeCache++]=log_msg;
}
else
{
class=class="str">"cmt">// 写文件至缓存并刷缓存至文件
logCache[sizeCache++]=log_msg;
flush();
}
}
else
{
class=class="str">"cmt">// 缓存被禁止, 立即写入文件
FileWrite(handleFile,log_msg);
}
if(FileTell(handleFile)>(MAX_FILE_SIZEMB*class="num">1000000)) class=class="str">"cmt">// 检查当前文件大小
{
class=class="str">"cmt">// 文件大小超出允许限制, 关闭当前文件并打开新的文件
deinit();
init();
}
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|   生成信息和写入日志                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CLogger::write(class="type">class="kw">string msg,class="type">class="kw">string category="")
{
class="type">class="kw">string msg_log;
if(category=="")                class=class="str">"cmt">// 检查传递的类别是否存在
{   category=defCategory;   } class=class="str">"cmt">// 设置省缺类别
class=class="str">"cmt">// 生成行并调用写消息方法
StringConcatenate(msg_log,category,":|: ",TimeToString(TimeCurrent(),TIME_SECONDS),"    ",msg);
writeLog(msg_log);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|    生成信息和写入日志                                               |
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 正文

<span class="keyword">void</span> CLogger::write(<span class="keyword">string</span> msg,<span class="keyword">string</span> category,<span class="keyword">color</span> colorOfMsg,<span class="keyword">string</span> file=<span class="string">""</span>,<span class="keyword">int</span> line=<span class="number">0</span>) { <span class="keyword">string</span> msg_log; <span class="keyword">int</span> red,green,blue; red=(colorOfMsg &amp;<span>Red</span>); <span class="comment"> // 从常量选择红色</span> green=(colorOfMsg &amp;<span class="number">0x00FF00</span>)&gt;&gt;<span class="number">8</span>; <span class="comment">// 从常量选择绿色</span> blue=(colorOfMsg &amp;<span>Blue</span>)&gt;&gt;<span class="number">16</span>; <span class="comment"> // 从常量选择蓝色</span> <span class="comment"> // 检查文件或行是否已传递,生成行并调用写消息方法</span> <span class="keyword">if</span>(file!=<span class="string">""</span> &amp;&amp; line!=<span class="number">0</span>) { <span class="functions">StringConcatenate</span>(msg_log,category,<span class="string">":|:"</span>,red,<span class="string">","</span>,green,<span class="string">","</span>,blue, <span class="string">":|:"</span>,<span class="functions">TimeToString</span>(<span class="functions">TimeCurrent</span>(),TIME_SECONDS),<span class="string">" "</span>, <span class="string">"文件: "</span>,file,<span class="string">" 行: "</span>,line,<span class="string">" "</span>,msg); } <span class="keyword">else</span> { <span class="functions">StringConca

给日志上色并定位到行

在 MT5 里自定义日志类时,write 方法支持直接传 RGB 三通道值来高亮某类消息。比如传 173,255,47 这组参数,终端会以绿黄色把“错误”类别的消息标出来,方便你在密集输出里一眼抓到异常。 带 __FILE__ 和 __LINE__ 的写法会在日志里补上源文件名与行号。上面样例输出中,行 29 的测试消息被记成“文件: testLogger.mq5 行: 29”,排查 EA 或脚本的问题时不用再手动翻代码。 压力测试里用 for 循环连写 100000 条带颜色的注释消息,单条 RGB 设为 100,222,100。跑完记得调 logger.flush() 强制落盘、logger.deinit() 关文件,否则缓存可能丢最后几行。外汇与贵金属行情波动剧烈,这类诊断日志仅用于本地验证,不构成任何交易信号。

MQL5 / C++
logger.write("测试消息", "错误",class="num">173,class="num">255,class="num">47);
class=class="str">"cmt">// 以“错误”类别写消息,将以绿黄色突出显示在日志监视
class=class="str">"cmt">// 但现在我们指定每种独立颜色为: 红, 绿, 蓝. class="num">0-黑, class="num">255 - 白
class=class="str">"cmt">// 同样,消息将包括当前文件名和当前行
logger.write("测试消息", "错误",class="num">173,class="num">255,class="num">47,__FILE__,__LINE__);
注释:|:class="num">23:class="num">13:class="num">12    测试消息
错误:|:class="num">23:class="num">13:class="num">12    测试消息
错误:|:class="num">255,class="num">0,class="num">0:|:class="num">23:class="num">13:class="num">12    测试消息
错误:|:class="num">255,class="num">0,class="num">0:|:class="num">23:class="num">13:class="num">12    文件: testLogger.mq5   行: class="num">27   测试消息
错误:|:class="num">173,class="num">255,class="num">47:|:class="num">23:class="num">13:class="num">12    测试消息
错误:|:class="num">173,class="num">255,class="num">47:|:class="num">23:class="num">13:class="num">12    文件: testLogger.mq5   行: class="num">29   测试消息
logger.flush();  class=class="str">"cmt">// 强制刷缓存至硬盘
logger.deinit(); class=class="str">"cmt">// 关闭文件
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                   testLogger.mq5 |
class=class="str">"cmt">//|                                                             ProF |
class=class="str">"cmt">//|                                                          http:// |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "ProF"
class="macro">#class="kw">property link      "http:class=class="str">"cmt">//"
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#include <Сlogger.mqh>
CLogger logger;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 脚本程序初始函数                                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
{
class=class="str">"cmt">//---
logger.SetSetting("proj","lfile");      class=class="str">"cmt">// 设置
logger.init();                          class=class="str">"cmt">// 初始化
logger.write("启动脚本","系统");
for(class="type">int i=class="num">0;i<class="num">100000;i++)               class=class="str">"cmt">// 写 class="num">100000 消息到日志
{
logger.write("日志: "+(class="type">class="kw">string)i,"注释",class="num">100,class="num">222,class="num">100,__FILE__,__LINE__);
}
logger.write("停止脚本","系统");
logger.flush();                         class=class="str">"cmt">// 刷缓存
logger.deinit();                        class=class="str">"cmt">// 卸载
}
class=class="str">"cmt">//+------------------------------------------------------------------

常见问题

建一个CLogger类,构造时传文件名与模式,提供Write()方法内部处理打开追加关闭;EA里只new一次后随处调用,避免重复代码。
在EA的OnInit里实例化日志类,把原来Print调试信息换成logger.Write(),其余交易逻辑不动,最小改动就能落盘。
可以,把EA代码贴给小布,让它识别Print点并生成CLogger接入片段与轮转配置,你复制进工程即可跑。
类里记录当前大小,超阈值建新文件按日期命名;Write先写内存缓冲,定时或满行数Flush,降低IO卡顿。
Write前拼ANSI色码按级别区分,并宏包裹__LINE__和__FILE__传参,输出即带颜色与行号,定位更快。