MQL5中的错误处理和日志记录·进阶篇
📘

MQL5中的错误处理和日志记录·进阶篇

第 2/3 篇

◍ EA 容错的分级与节流底线

写 MT5 程序先想清楚容错级别:自己跑两三次验证想法的小脚本,不处理错误无伤大雅;但面对上千真实用户的 EA,就必须兜住所有可预见的异常。用户交互也得拿捏——关键错误要弹窗让人介入,其余写日志,别用告警刷屏,也别在爆仓边缘装哑巴。 任何有返回值的函数都该查返回状态,这是白捡的质量优化机会。开仓前先探环境:TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) 看终端是否放行自动交易,AccountInfoInteger(ACCOUNT_TRADE_EXPERT) 查账户权限,TerminalInfoInteger(TERMINAL_CONNECTED) 确认服务器连着,再用 OrderCheck() 校验参数。 追踪止损最容易踩坑。若每个 tick 都调修改函数,单边行情里几乎每跳发一次修订请求;报价密集时,这种高频请求可能触发经纪商断开账户跟 EA 的自动交易连接。实测里把修订间隔锁到 20~30 秒就能避开。下面这段代码用静态变量记上次操作时间,30 秒内直接 return。 稳定性与准确性要分开对待:指标算不出就别给信号,别整体崩掉;EA 出岔子宁可停跑也别乱下单,停之前推一条消息给用户更稳。报错文案也得说人话,「无法开仓」不如「无法开仓:不正确的手数(0.9999)」,但弹窗别堆错误码——用户改不了。

MQL5 / C++
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异常可能造成快速回撤,日志机制只是降低排查成本,不预示任何收益。

MQL5 / C++
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滚动的日志。

MQL5 / C++
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 在策略测试器里故意打满日志验证边界。

MQL5 / C++
  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),也就是把品种和日期写进文件名,方便每天自动开新文件。外汇与贵金属波动剧烈、杠杆风险高,这类日志机制只帮你留痕,不替代风控。

MQL5 / C++
      }
      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);

常见问题

给EA套一层自己的日志壳,在关键函数入口和出口记级别标签,复现后翻日志即可定位断点,而不是只看平台默认报错。
写一套能分卷的日志类,按大小或时间切文件,保留最近N卷,超出的自动删旧卷,避免单文件膨胀拖慢IO。
可以,小布能接入你的日志分流逻辑,对ERROR级以上落盘并触发通知,你不用自己守着终端看盘口。
设分级与节流底线,同类型错误单位时间内只通知一次,其余仅落盘,既留痕又不打断决策。
用日志分级与分流逻辑,FATAL/ERROR写独立告警文件,INFO/DEBUG进常规卷,收口统一由分发函数处理。