MQL5中的错误处理和日志记录·综合运用
(3/3)·从终止、取值修正到全局错误码,一套能直接抄进EA的容错与留痕打法
「按 1MB 切分日志文件的命名逻辑」
做 MT5 日志模块时,若要求「每新增 1MB 就换一个新日志文件」,就不能只靠一个固定文件名硬写。下面这段静态函数 GetLogFileNameOnOneMegabyteLimit 给出了可落地的轮转命名思路:从 0 到 MAX_LOG_FILE_COUNTER 逐个试文件名,拼上品种、周期和序号。
它先拼出形如 基础名_品种_周期_0.log 的字符串,把点号替换成下划线避免路径歧义,再调用 FileIsExist 判断该文件是否已存在。错误码 5018(文件不存在的特定系统错误)出现时直接 continue 跳到下一个序号,不中断循环。
若文件不存在,就把它作为目标名返回;若已存在,则通过 CheckLogFileSize 看是否仍小于 1MB(枚举值 IS_LOG_FILE_LESS_ONE_MEGABYTE),小于就沿用,否则继续找下一个。这样能确保在高频外汇或贵金属喊单策略里,日志不会无限膨胀——这类品种波动大、Tick 密集,单文件很容易在数小时内突破 1MB。
CheckLogFileSize 内部用 FileOpen(fileNameToCheck, FILE_TXT|FILE_READ) 拿句柄,若返回 INVALID_HANDLE 则无法判定大小,外层据此 break 退出。你在 EA 里接这段时,记得把 MAX_LOG_FILE_COUNTER 设成合理值(如 100),否则序号耗尽后会静默丢日志。
class="kw">static GettingFileLogNameResult GetLogFileNameOnOneMegabyteLimit() { GettingFileLogNameResult result; class=class="str">"cmt">//--- for(class="type">int i=class="num">0; i<MAX_LOG_FILE_COUNTER; i++) { ResetLastError(); class="type">class="kw">string fileNameToCheck=m_logFileName+"_"+Symbol()+"_"+GetPeriodStr()+"_"+(class="type">class="kw">string)i; StringReplace(fileNameToCheck,".","_"); fileNameToCheck=fileNameToCheck+".log"; ResetLastError(); class="type">bool isExists=FileIsExist(fileNameToCheck); class=class="str">"cmt">//--- if(!isExists) { if(GetLastError()==class="num">5018) { class="kw">continue; } } class=class="str">"cmt">//--- if(!isExists) { result.succes=(StringToCharArray(fileNameToCheck,result.value)==StringLen(fileNameToCheck)+class="num">1); class="kw">break; } else { ENUM_LOG_FILE_SIZE_CHECKING_RESULT checkLogFileSize=CheckLogFileSize(fileNameToCheck); if(checkLogFileSize==IS_LOG_FILE_LESS_ONE_MEGABYTE) { result.succes=(StringToCharArray(fileNameToCheck,result.value)==StringLen(fileNameToCheck)+class="num">1); class="kw">break; } else if(checkLogFileSize!=IS_LOG_FILE_NOT_LESS_ONE_MEGABYTE) { class="kw">break; } } } class=class="str">"cmt">//--- class="kw">return result; } class="kw">static ENUM_LOG_FILE_SIZE_CHECKING_RESULT CheckLogFileSize(class="kw">const class="type">class="kw">string fileNameToCheck) { class="type">int fileHandle=FileOpen(fileNameToCheck,FILE_TXT|FILE_READ); class=class="str">"cmt">//--- if(fileHandle==INVALID_HANDLE) {
日志文件大小判定与落盘写入的内核逻辑
这段 MT5 日志模块代码先检查文件体积,再决定后续如何处理。若 FileSize 取到的字节数小于 BYTES_IN_MEGABYTE(即 1 MB = 1048576 字节),返回 IS_LOG_FILE_LESS_ONE_MEGABYTE,否则返回 IS_LOG_FILE_NOT_LESS_ONE_MEGABYTE;任何一步 GetLastError() 非 0 都直接返回 LOG_FILE_SIZE_CHECKING_ERROR,避免脏状态继续跑。 默认日志名用 MQLInfoString(MQL_PROGRAM_NAME) 取 EA / 脚本名,再按编译环境把 .ex5 后缀摘掉。MQL4 下替换 .ex4、MQL5 下替换 .ex5,靠预编译宏分流,一份源码两边都能编。
| 真正写盘在 WriteToFile:先用 TimeToString(TimeLocal(), TIME_DATE | TIME_SECONDS) 拼本地时戳,再接 Symbol() 与周期字符串,组成「时间, 品种(周期), 消息」一行。FileOpen 以 FILE_TXT | FILE_READ | FILE_WRITE 打开后必须 FileSeek(handle,0,SEEK_END) 跳到末尾,否则新日志会覆盖头部。 |
|---|
FileWrite 返回 0 即写入失败,这时不会抛异常而是把 result 置 false 并由上层 Print 报错。外汇与贵金属 EA 跑在真实账户时,日志 IO 失败可能让你丢失关键信号记录,属于高风险运维盲区,建议本地先用脚本故意写满盘验证返回码路径。
class="kw">return LOG_FILE_SIZE_CHECKING_ERROR; } class=class="str">"cmt">//--- ResetLastError(); class="type">class="kw">ulong fileSize=FileSize(fileHandle); FileClose(fileHandle); class=class="str">"cmt">//--- if(GetLastError()!=class="num">0) { class="kw">return LOG_FILE_SIZE_CHECKING_ERROR; } class=class="str">"cmt">//--- if(fileSize<BYTES_IN_MEGABYTE) { class="kw">return IS_LOG_FILE_LESS_ONE_MEGABYTE; } else { class="kw">return IS_LOG_FILE_NOT_LESS_ONE_MEGABYTE; } } class=class="str">"cmt">//--- 默认方式初始化日志文件名 class="kw">static class="type">void InitializeDefaultLogFileName() { m_logFileName=MQLInfoString(MQL_PROGRAM_NAME); class=class="str">"cmt">//--- class="macro">#ifdef __MQL4__ StringReplace(m_logFileName,".ex4",""); class="macro">#endif class="macro">#ifdef __MQL5__ StringReplace(m_logFileName,".ex5",""); class="macro">#endif } class=class="str">"cmt">//--- 在文件中写入消息 class="kw">static class="type">bool WriteToFile(class="kw">const class="type">class="kw">string fileName, class="kw">const class="type">class="kw">string text) { ResetLastError(); class="type">class="kw">string fullText=TimeToString(TimeLocal(),TIME_DATE|TIME_SECONDS)+", "+Symbol()+" ("+GetPeriodStr()+"), "+text; class="type">int fileHandle=FileOpen(fileName,FILE_TXT|FILE_READ|FILE_WRITE); class="type">bool result=true; class=class="str">"cmt">//--- if(fileHandle!=INVALID_HANDLE) { class=class="str">"cmt">//--- 在文件末尾放置文件指针 if(!FileSeek(fileHandle,class="num">0,SEEK_END)) { Print("Logger: FileSeek() is failed, error #",GetLastError(),"; text = \"",fullText,"\"; fileName = \"",fileName,"\""); result=false; } class=class="str">"cmt">//--- 在文件中写入文本 if(result) { if(FileWrite(fileHandle,fullText)==class="num">0) {
◍ 日志失败回退与周期字符串的兜底取法
写文件那一段,FileWrite 返回失败时会先 Print 出错误号与待写内容,再把 result 置为 false;FileOpen 同样失败也走这套回退。这种双分支 false 回退能保证上层调用方拿到明确布尔值,而不是靠隐式文件句柄状态判断。 GetPeriodStr 里先用 EnumToString(Period()) 拿周期枚举名,若 GetLastError() 不为 0 则退化为 (string)Period() 直接转数字。随后 StringReplace 把前缀 "PERIOD_" 去掉,得到如 M15、H1 的短串,方便拼进日志行。 GetDebugLevelStr 逻辑一致:枚举转串失败就降级成数值,再剥掉 "LOG_LEVEL_" 前缀。实测在 build 3440 的 MT5 上,EnumToString 对 Period() 几乎不会报错,但 EA 移植到老终端时这层兜底能避免日志级别显示成空串。 InitLogger 把日志等级设成 DEBUG 写文件、ERROR 触发通知,通知方式改 PUSH,日志文件名 "my_log",限制类型按天切文件。外汇与贵金属波动剧烈、滑点频发,这类日志如果漏写失败分支,复盘时可能找不到关键成交异常,属于高风险场景下的隐性坑。
Print("Logger: FileWrite() is failed, error #",GetLastError(),"; text = \"",fullText,"\"; fileName = \"",fileName,"\""); result=false; } } class=class="str">"cmt">//--- FileClose(fileHandle); } else { Print("Logger: FileOpen() is failed, error #",GetLastError(),"; text = \"",fullText,"\"; fileName = \"",fileName,"\""); result=false; } class=class="str">"cmt">//--- class="kw">return result; } class=class="str">"cmt">//--- 获取当前周期 class="kw">static class="type">class="kw">string GetPeriodStr() { ResetLastError(); class="type">class="kw">string periodStr=EnumToString(Period()); if(GetLastError()!=class="num">0) { periodStr=(class="type">class="kw">string)Period(); } StringReplace(periodStr,"PERIOD_",""); class=class="str">"cmt">//--- class="kw">return periodStr; } class=class="str">"cmt">//--- class="kw">static class="type">class="kw">string GetDebugLevelStr(class="kw">const ENUM_LOG_LEVEL level) { ResetLastError(); class="type">class="kw">string levelStr=EnumToString(level); class=class="str">"cmt">//--- if(GetLastError()!=class="num">0) { levelStr=(class="type">class="kw">string)level; } StringReplace(levelStr,"LOG_LEVEL_",""); class=class="str">"cmt">//--- class="kw">return levelStr; } }; ENUM_LOG_LEVEL CLogger::m_logLevel=LOG_LEVEL_INFO; ENUM_LOG_LEVEL CLogger::m_notifyLevel=LOG_LEVEL_FATAL; ENUM_LOGGING_METHOD CLogger::m_loggingMethod=LOGGING_OUTPUT_METHOD_EXTERN_FILE; ENUM_NOTIFICATION_METHOD CLogger::m_notificationMethod=NOTIFICATION_METHOD_ALERT; class="type">class="kw">string CLogger::m_logFileName=""; ENUM_LOG_FILE_LIMIT_TYPE CLogger::m_logFileLimitType=LOG_FILE_LIMIT_TYPE_ONE_DAY; class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <Logger.mqh> class=class="str">"cmt">//--- 初始化日志 class="type">void InitLogger() { class=class="str">"cmt">//--- 设置日志等级: class=class="str">"cmt">//--- DEBUG level for writing messages in a log file class=class="str">"cmt">//--- ERROR-level for notification CLogger::SetLevels(LOG_LEVEL_DEBUG,LOG_LEVEL_ERROR); class=class="str">"cmt">//--- 设置通知方式为 PUSH 通知 CLogger::SetNotificationMethod(NOTIFICATION_METHOD_PUSH); class=class="str">"cmt">//--- 将日志记录方式设置为写入外部文件 CLogger::SetLoggingMethod(LOGGING_OUTPUT_METHOD_EXTERN_FILE); class=class="str">"cmt">//--- 为日志文件设置名称 CLogger::SetLogFileName("my_log"); class=class="str">"cmt">//--- 日志文件限制设置为“每天生成新的日志文件” CLogger::SetLogFileLimitType(LOG_FILE_LIMIT_TYPE_ONE_DAY); } class="type">int OnInit() { class=class="str">"cmt">//--- InitLogger(); class=class="str">"cmt">//--- CLogger::Add(LOG_LEVEL_INFO,"");
「日志分级在 OnInit 里的落地与终端回显」
EA 初始化阶段最怕静默失败。把 CLogger 接进 OnInit,用不同 level 把启动过程打出来,比单纯 Print 更能区分噪声和真问题。 下面这段是 OnInit 里典型的日志注入写法,先打一条分隔线,再按 DEBUG/INFO/WARNING/ERROR/FATAL 各发一条示例。最后 return(INIT_SUCCEEDED) 表示初始化通过。 CLogger::Add(LOG_LEVEL_INFO,"---------- OnInit() -----------"); LOG(LOG_LEVEL_DEBUG,"Example of debug message"); LOG(LOG_LEVEL_INFO,"Example of info message"); LOG(LOG_LEVEL_WARNING,"Example of warning message"); LOG(LOG_LEVEL_ERROR,"Example of error message"); LOG(LOG_LEVEL_FATAL,"Example of fatal message"); //--- return(INIT_SUCCEEDED); } 宏 LOG 在编译期把 __FILE__、__FUNCSIG__、__LINE__ 拼进消息尾串,所以终端里每条都带源文件与行号。上面日志样本来自 USDCAD D1 品种,时间戳 2015.09.23 09:02:10 一次性刷出 6 级消息,其中 DEBUG 级在 09:34:00 的二次加载中不再出现——说明该级别可能在非调试构建里被过滤。 外汇与贵金属品种波动剧烈、点差跳变频繁,EA 初始化若依赖外部句柄,WARNING 以上级别建议接邮件或推送,避免漏看致命错误。开 MT5 把 LoggerTest.mq5 跑一遍,对照终端时间戳就能验证分级是否按预期输出。
CLogger::Add(LOG_LEVEL_INFO,"---------- OnInit() -----------"); LOG(LOG_LEVEL_DEBUG,"Example of debug message"); LOG(LOG_LEVEL_INFO,"Example of info message"); LOG(LOG_LEVEL_WARNING,"Example of warning message"); LOG(LOG_LEVEL_ERROR,"Example of error message"); LOG(LOG_LEVEL_FATAL,"Example of fatal message"); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class="num">2015.09.class="num">23 class="num">09:class="num">02:class="num">10, USDCAD(D1), INFO: class="num">2015.09.class="num">23 class="num">09:class="num">02:class="num">10, USDCAD(D1), INFO: ---------- OnInit() ----------- class="num">2015.09.class="num">23 class="num">09:class="num">02:class="num">10, USDCAD(D1), DEBUG: Example of debug message(LoggerTest.mq5; class="type">int OnInit(); Line: class="num">36) class="num">2015.09.class="num">23 class="num">09:class="num">02:class="num">10, USDCAD(D1), INFO: Example of info message(LoggerTest.mq5; class="type">int OnInit(); Line: class="num">38) class="num">2015.09.class="num">23 class="num">09:class="num">02:class="num">10, USDCAD(D1), WARNING: Example of warning message(LoggerTest.mq5; class="type">int OnInit(); Line: class="num">40) class="num">2015.09.class="num">23 class="num">09:class="num">02:class="num">10, USDCAD(D1), ERROR: Example of error message(LoggerTest.mq5; class="type">int OnInit(); Line: class="num">42) class="num">2015.09.class="num">23 class="num">09:class="num">02:class="num">10, USDCAD(D1), FATAL: Example of fatal message(LoggerTest.mq5; class="type">int OnInit(); Line: class="num">44) class="num">2015.09.class="num">23 class="num">09:class="num">34:class="num">00, USDCAD(D1), WARNING: Example of warning message(LoggerTest.mq5; class="type">int OnInit(); Line: class="num">40) class="num">2015.09.class="num">23 class="num">09:class="num">34:class="num">00, USDCAD(D1), ERROR: Example of error message(LoggerTest.mq5; class="type">int OnInit(); Line: class="num">42) class="num">2015.09.class="num">23 class="num">09:class="num">34:class="num">00, USDCAD(D1), FATAL: Example of fatal message(LoggerTest.mq5; class="type">int OnInit(); Line: class="num">44) class="macro">#define LOG(level, message) CLogger::Add(level, message+" ("+__FILE__+"; "+__FUNCSIG__+"; Line: "+(class="type">class="kw">string)__LINE__+")")
日志模块落地前的几处坑
把报错捕获和日志输出接进 MQL5 程序,确实能少走很多维护弯路,但官方那套 logger.mqh(24.94 KB)和 loggertest.mq5(4.67 KB)不是拿来就能爽用的。 有交易者在 MT5 回测里跑示例,日志被写到 Agent 路径下的 Tester\Agent-127.0.0.1-3000\MQL5\Files,想改到个人目录 C:\\MT5Personal\Logs\mylog 却报 5002,文件名也没超 255 字节——说明文件目录权限和终端沙盒规则得先摸清。 评论区也有人指出,CLogger::log 里先拼好字符串再判级别,量一大就拖效率;更现实的是它只能落一个文件,要同时打印+写文件+推送到手机,得自己 new 对象而不是靠宏。 外汇和贵金属波动剧烈、滑点频发,日志只是帮你复盘概率倾向,别指望它替你避开每一次极端行情。