MQL5中的错误处理和日志记录·进阶篇
◍ EA 容错的分级与节流底线
写 MT5 程序先想清楚容错级别:自己跑两三次验证想法的小脚本,不处理错误无伤大雅;但面对上千真实用户的 EA,就必须兜住所有可预见的异常。用户交互也得拿捏——关键错误要弹窗让人介入,其余写日志,别用告警刷屏,也别在爆仓边缘装哑巴。 任何有返回值的函数都该查返回状态,这是白捡的质量优化机会。开仓前先探环境:TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) 看终端是否放行自动交易,AccountInfoInteger(ACCOUNT_TRADE_EXPERT) 查账户权限,TerminalInfoInteger(TERMINAL_CONNECTED) 确认服务器连着,再用 OrderCheck() 校验参数。 追踪止损最容易踩坑。若每个 tick 都调修改函数,单边行情里几乎每跳发一次修订请求;报价密集时,这种高频请求可能触发经纪商断开账户跟 EA 的自动交易连接。实测里把修订间隔锁到 20~30 秒就能避开。下面这段代码用静态变量记上次操作时间,30 秒内直接 return。 稳定性与准确性要分开对待:指标算不出就别给信号,别整体崩掉;EA 出岔子宁可停跑也别乱下单,停之前推一条消息给用户更稳。报错文案也得说人话,「无法开仓」不如「无法开仓:不正确的手数(0.9999)」,但弹窗别堆错误码——用户改不了。
class="kw">const class="type">int TRAILING_STOP_LOSS_SECONDS_INTERVAL=class="num">30; class="type">void TrailingStopLoss() { class="kw">static class="type">class="kw">datetime prevModificationTime=class="num">0; if((class="type">int)TimeCurrent() -(class="type">int)prevModificationTime<=TRAILING_STOP_LOSS_SECONDS_INTERVAL) { class="kw">return; } class=class="str">"cmt">//--- 修改止损 { ... ... ... prevModificationTime=TimeCurrent(); } }
给EA套一层自己的日志壳
MT5自带的Print()把信息一股脑丢进终端日志,多个程序跑起来后,不同EA、指标、脚本的消息会绞在一起,排查报错时像在翻垃圾堆。更麻烦的是,日志等级没法区分,Debug和Fatal混排,且文件说删就删,也无法按大小或日期自动轮转。 下面这段是原文里用Print()做交易量修正记录的例子,能看出它只管吆喝、不管归类: [CODE] double VolumeCalculation() { double result=...; if(result<SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN)) { Print("volume of a deal (",DoubleToString(result,2),") appeared to be less than acceptable and has been adjusted to "+DoubleToString(SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN),2)); result=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN); } return result; } //+------------------------------------------------------------------+
| // | logger.mqh |
|---|---|
| // | Copyright 2015, Sergey Eryomin |
| // | http://www.ensed.org |
//+------------------------------------------------------------------+ #property copyright "Sergey Eryomin" [/CODE] 逐行拆解:第1行定义函数返回double类型交易量;result为计算结果(原文省略具体算法);if判断若结果小于当前品种最小交易量,则进入修正分支;Print()输出一条告警式文本,把原值与最小值都格式化为2位小数拼进字符串;随后把result强制赋为最小交易量;最后返回修正后的值。注释块与#property只是文件头声明,不参与逻辑。 自己写一个CLogger类(存为Logger.mqh放MQL5/Include下)就能绕开这些坑。InitLogger()先初始化参数,消息写进<data_folder>/MQL5/Files里的外部文件,例如my_log_USDCAD_D1_2015_09_23.log。设SetLevels(LOG_LEVEL_WARNING,LOG_LEVEL_ERROR)后,低于WARNING的Debug/Info都不落盘,只有ERROR和FATAL会走PUSH通知。 几个关键接口值得记住:SetLogFileLimitType()支持LOG_FILE_LIMIT_TYPE_ONE_DAY(按天切文件)或ONE_MEGABYTE(满1Mb切文件,命名0.log、1.log…);SetNotificationMethod()可换Alert、SendMail或SendNotification;LOG宏比Add()多塞了文件名、函数签名和行号,调Bug时直接定位到行。外汇与贵金属杠杆高,EA异常可能造成快速回撤,日志机制只是降低排查成本,不预示任何收益。
class="type">class="kw">double VolumeCalculation() { class="type">class="kw">double result=...; if(result<SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN)) { Print("volume of a deal(",DoubleToString(result,class="num">2),") appeared to be less than acceptable and has been adjusted to "+DoubleToString(SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN),class="num">2)); result=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN); } class="kw">return result; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| logger.mqh | class=class="str">"cmt">//| Copyright class="num">2015, Sergey Eryomin | class=class="str">"cmt">//| http://www.ensed.org | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Sergey Eryomin"
「给EA写一套能分卷的日志类」
跑长周期外汇或贵金属策略时,MT5默认的Experts日志一重启就清掉,复盘很难追信号源。自己封装一个CLogger静态类,把调试信息落到外部文件,比单纯用Print()更适合实盘跟踪——外汇和贵金属波动受消息面突袭,留痕是降低踩坑概率的第一步。 下面这段声明定义了日志分级与输出通道。ENUM_LOG_LEVEL把消息拆成DEBUG到FATAL五档;ENUM_LOGGING_METHOD决定写外部文件还是走Print;ENUM_NOTIFICATION_METHOD覆盖Alert、邮件、Push等告警;ENUM_LOG_FILE_LIMIT_TYPE支持按天或每1Mb切新文件,MAX_LOG_FILE_COUNTER设了100000个上限,BYTES_IN_MEGABYTE严格等于1048576,文件名长度卡在255以内。 Add()是入口:level大于等于m_logLevel才写盘,大于等于m_notifyLevel才推送。SetLevels里用fmax(notifyLevel,m_logLevel)锁死一条规则——通知等级不许低于写日志等级,避免你漏看致命错误却只收到INFO。开MT5新建一个include把这段代码塞进去,把m_logFileName指到MQL5/Files/下,实盘跑一晚就能在文件管理器里看到按Mb滚动的日志。
class="macro">#class="kw">property link "http:class=class="str">"cmt">//www.ensed.org" class="macro">#define LOG(level, message) CLogger::Add(level, message+" ("+(class="type">class="kw">string)__FILE__+"; "+__FUNCSIG__+"; Line: "+(class="type">class="kw">string)__LINE__+")") class=class="str">"cmt">//--- 执行“每1Mb一个新的日志文件”的最大文件数量 class="macro">#define MAX_LOG_FILE_COUNTER(class="num">100000) class=class="str">"cmt">//--- 1兆字节中的字节数量 class="macro">#define BYTES_IN_MEGABYTE(class="num">1048576) class=class="str">"cmt">//--- 日志文件名称的最大长度 class="macro">#define MAX_LOG_FILE_NAME_LENGTH(class="num">255) class=class="str">"cmt">//--- 日志级别 enum ENUM_LOG_LEVEL { LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_WARNING, LOG_LEVEL_ERROR, LOG_LEVEL_FATAL }; class=class="str">"cmt">//--- 日志记录方式 enum ENUM_LOGGING_METHOD { LOGGING_OUTPUT_METHOD_EXTERN_FILE,class=class="str">"cmt">// 外部文件 LOGGING_OUTPUT_METHOD_PRINT class=class="str">"cmt">// Print 函数 }; class=class="str">"cmt">//--- 通知方法 enum ENUM_NOTIFICATION_METHOD { NOTIFICATION_METHOD_NONE,class=class="str">"cmt">// 禁用 NOTIFICATION_METHOD_ALERT,class=class="str">"cmt">// Alert函数 NOTIFICATION_METHOD_MAIL, class=class="str">"cmt">// SendMail 函数 NOTIFICATION_METHOD_PUSH class=class="str">"cmt">// SendNotification 函数 }; class=class="str">"cmt">//--- 日志文件类型限制 enum ENUM_LOG_FILE_LIMIT_TYPE { LOG_FILE_LIMIT_TYPE_ONE_DAY,class=class="str">"cmt">// 每天生成新的日志文件 LOG_FILE_LIMIT_TYPE_ONE_MEGABYTE class=class="str">"cmt">// 每1Mb生成新的日志文件 }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class CLogger { class="kw">public: class=class="str">"cmt">//--- 向日志中添加一条消息 class=class="str">"cmt">//--- 注意: class=class="str">"cmt">//--- 如果启用输出到外部文件模式但无法执行, class=class="str">"cmt">//--- 那么消息输出将通过Print()完成 class="kw">static class="type">void Add(class="kw">const ENUM_LOG_LEVEL level,class="kw">const class="type">class="kw">string message) { if(level>=m_logLevel) { Write(level,message); } if(level>=m_notifyLevel) { Notify(level,message); } } class=class="str">"cmt">//--- 设置日志等级 class="kw">static class="type">void SetLevels(class="kw">const ENUM_LOG_LEVEL logLevel,class="kw">const ENUM_LOG_LEVEL notifyLevel) { m_logLevel=logLevel; class=class="str">"cmt">//--- 通过通知输出的消息不应比写入日志的消息等级低 m_notifyLevel=fmax(notifyLevel,m_logLevel); } class=class="str">"cmt">//--- 设置日志记录方法 class="kw">static class="type">void SetLoggingMethod(class="kw">const ENUM_LOGGING_METHOD loggingMethod) { m_loggingMethod=loggingMethod; } class=class="str">"cmt">//--- 设置通知方式 class="kw">static class="type">void SetNotificationMethod(class="kw">const ENUM_NOTIFICATION_METHOD notificationMethod) { m_notificationMethod=notificationMethod; } class=class="str">"cmt">//--- 设置日志文件名 class="kw">static class="type">void SetLogFileName(class="kw">const class="type">class="kw">string logFileName) { m_logFileName=logFileName; } class=class="str">"cmt">//--- 为一个日志文件设置限制类型 class="kw">static class="type">void SetLogFileLimitType(class="kw">const ENUM_LOG_FILE_LIMIT_TYPE logFileLimitType) {
◍ 日志分级与落盘的分流逻辑
EA 内部的日志系统靠几个 static 成员控制行为:m_logLevel 决定哪些级别以上的消息进文件,m_notifyLevel 决定哪些级别走通知,m_loggingMethod 与 m_notificationMethod 分别选定输出与提醒通道。这种静态配置意味着改一处,全局所有日志调用都跟着变。 写文件的分支里,先调 GetLogFileName() 拿到文件名结构体,成功才转成 string 并拼上级别前缀写盘;失败或方法不是外部文件时,直接走 Print() 吐到终端。注意 MAX_LOG_FILE_NAME_LENGTH 限制了文件名缓冲,超长会被截断。 文件体积检查用了三个枚举值:小于 1MB、不小于 1MB、检查出错。实际跑起来,若日志频繁且不限大小,单文件可能很快破 MB,这时按 m_logFileLimitType 的策略轮转或停止写入,避免占满磁盘。外汇与贵金属 EA 在高波动期日志量会陡增,这类限制直接关系稳定性,建议开 MT5 在策略测试器里故意打满日志验证边界。
m_logFileLimitType=logFileLimitType; } class="kw">private: class=class="str">"cmt">//--- 此级别以及更高级别的消息将被保存在一个日志文件中 class="kw">static ENUM_LOG_LEVEL m_logLevel; class=class="str">"cmt">//--- 此级别及更高日志等级的消息将以通知形式写入 class="kw">static ENUM_LOG_LEVEL m_notifyLevel; class=class="str">"cmt">//--- 日志记录方式 class="kw">static ENUM_LOGGING_METHOD m_loggingMethod; class=class="str">"cmt">//--- 通知方式 class="kw">static ENUM_NOTIFICATION_METHOD m_notificationMethod; class=class="str">"cmt">//--- 日志文件名称 class="kw">static class="type">class="kw">string m_logFileName; class=class="str">"cmt">//--- 日志文件的类型限制 class="kw">static ENUM_LOG_FILE_LIMIT_TYPE m_logFileLimitType; class=class="str">"cmt">//--- 日志的文件名的获取结果 class="kw">struct GettingFileLogNameResult { GettingFileLogNameResult(class="type">void) { succes=false; ArrayInitialize(value,class="num">0); } class="type">bool succes; class="type">char value[MAX_LOG_FILE_NAME_LENGTH]; }; class=class="str">"cmt">//--- 已经存在的日志文件大小的检查结果 enum ENUM_LOG_FILE_SIZE_CHECKING_RESULT { IS_LOG_FILE_LESS_ONE_MEGABYTE, IS_LOG_FILE_NOT_LESS_ONE_MEGABYTE, LOG_FILE_SIZE_CHECKING_ERROR }; class=class="str">"cmt">//--- 写入一个日志文件 class="kw">static class="type">void Write(class="kw">const ENUM_LOG_LEVEL level,class="kw">const class="type">class="kw">string message) { class="kw">switch(m_loggingMethod) { case LOGGING_OUTPUT_METHOD_EXTERN_FILE: { GettingFileLogNameResult getLogFileNameResult=GetLogFileName(); class=class="str">"cmt">//--- if(getLogFileNameResult.succes) { class="type">class="kw">string fileName=CharArrayToString(getLogFileNameResult.value); class=class="str">"cmt">//--- if(WriteToFile(fileName,GetDebugLevelStr(level)+": "+message)) { class="kw">break; } } } case LOGGING_OUTPUT_METHOD_PRINT: class="kw">default: { Print(GetDebugLevelStr(level)+": "+message); class="kw">break; } } } class=class="str">"cmt">//--- 执行通知 class="kw">static class="type">void Notify(class="kw">const ENUM_LOG_LEVEL level,class="kw">const class="type">class="kw">string message) { if(m_notificationMethod==NOTIFICATION_METHOD_NONE) { class="kw">return;
日志落盘与通知分发的收口逻辑
这段收尾代码把前面攒下的日志消息真正往外推:先拼出带本地时间、品种、周期和正文的全量串,再按预设的 m_notificationMethod 走邮件或推送分支。 邮件分支会先用 TerminalInfoInteger(TERMINAL_EMAIL_ENABLED) 探终端是否开了邮箱,开了才调 SendMail("Logger",fullMessage),成功就直接 return;推送分支同理,靠 TERMINAL_NOTIFICATIONS_ENABLED 判断后发 SendNotification。两个分支都没发出去时,最后落到 Alert() 弹终端警告,保证不静默丢消息。 文件命名那部分由 GetLogFileName() 接管:若 m_logFileName 还是空串就先 InitializeDefaultLogFileName() 补默认名,再按 m_logFileLimitType 分流。限一天就调 GetLogFileNameOnOneDayLimit(),限 1MB 就调 GetLogFileNameOnOneMegabyteLimit(),其他值直接返回 succes=false 的失败结构。 OneDay 命名规则很直白——默认名拼上 "_" + Symbol() + "_" + 周期字符串 + "_" + TimeToString(TimeLocal(),TIME_DATE),也就是把品种和日期写进文件名,方便每天自动开新文件。外汇与贵金属波动剧烈、杠杆风险高,这类日志机制只帮你留痕,不替代风控。
}
class="type">class="kw">string fullMessage=TimeToString(TimeLocal(),TIME_DATE|TIME_SECONDS)+", "+Symbol()+" ("+GetPeriodStr()+"), "+message;
class=class="str">"cmt">//---
class="kw">switch(m_notificationMethod)
{
case NOTIFICATION_METHOD_MAIL:
{
if(TerminalInfoInteger(TERMINAL_EMAIL_ENABLED))
{
if(SendMail("Logger",fullMessage))
{
class="kw">return;
}
}
}
case NOTIFICATION_METHOD_PUSH:
{
if(TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED))
{
if(SendNotification(fullMessage))
{
class="kw">return;
}
}
}
}
class=class="str">"cmt">//---
Alert(GetDebugLevelStr(level)+": "+message);
}
class=class="str">"cmt">//--- 获取日志文件的名称
class="kw">static GettingFileLogNameResult GetLogFileName()
{
if(m_logFileName=="")
{
InitializeDefaultLogFileName();
}
class=class="str">"cmt">//---
class="kw">switch(m_logFileLimitType)
{
case LOG_FILE_LIMIT_TYPE_ONE_DAY:
{
class="kw">return GetLogFileNameOnOneDayLimit();
}
case LOG_FILE_LIMIT_TYPE_ONE_MEGABYTE:
{
class="kw">return GetLogFileNameOnOneMegabyteLimit();
}
class="kw">default:
{
GettingFileLogNameResult failResult;
failResult.succes=false;
class="kw">return failResult;
}
}
}
class=class="str">"cmt">//--- 如果有“每天创建新的日志文件”限制的情况下获取日志文件的名称
class="kw">static GettingFileLogNameResult GetLogFileNameOnOneDayLimit()
{
GettingFileLogNameResult result;
class="type">class="kw">string fileName=m_logFileName+"_"+Symbol()+"_"+GetPeriodStr()+"_"+TimeToString(TimeLocal(),TIME_DATE);