English Русский Español Deutsch 日本語 Português
MQL5中的错误处理和日志记录

MQL5中的错误处理和日志记录

MetaTrader 5示例 | 28 一月 2016, 10:27
5 666 0
Sergey Eremin
Sergey Eremin

简介

在大多数程序的执行过程中,错误总可能偶尔出现。对它们的恰当处理是高质量和稳定的软件重要的特征。本文将包含错误处理的主要方法,使用它们的一些建议以及通过MQL5工具包做日志记录。

错误处理是一个相对来说困难且有争议的话题。有许多错误处理的方法,每一种都有其特有的优点和缺点。很多方法可以合一起使用,但没有统一的标准 — 每一项特定的任务都需要一个合适的方法。


错误处理的基本方法

如果一个程序在执行过程中遇到错误,那么般来说为了能正常运行,需要执行某些操作(或者一些操作)。下面是这类操作的一些例子:

终止程序。如果有任何错误出现,最佳操作就是停止正在运行的程序。通常,这些都是禁止程序运行的严重错误,因为它要么使程序变得无意义或者干脆十分危险。MQL5为运行错误提供了中断机制:例如,在“除数为零”或者“数组越界”的情况下,程序触发中断。其他程序终止的情况开发者必须自行小心地处理。例如,对于EA来说,需要使用ExpertRemove()函数。

使用ExpertRemove()函数终止EA的例子:

void OnTick()
  {
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      Alert("fail");
      ExpertRemove();
      return;
     }
  }


将不正确的值转换到正确值所在范围。特定的值通常需落入指定的范围内。然而,在某些情况下值可能落在此范围外。那么就可能要将值强制返回可接受的边界内。开仓量的计算就可以作为一个例子。如果交易量在最小和最大允许值范围外,那么要将其强制转换到其中来:

将不正确的值转换到正确值的范围内的例子

double VolumeCalculation()
  {
   double result=...;

   result=MathMax(result,SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN));
   result=MathMin(result,SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MAX));

   return result;
  }

然而,如果由于某些原因交易量大于最大允许值,并且账户资金不够,那么最好用日志记录下来并放弃执行。这种特殊的错误经常对账户构成威胁。


返回一个错误值。在这种情况下如果一个错误发生,那么一个方法或者函数必须翻译一个预设的值来标记这个错误。例如,如果我们的方法或者函数要返回一个string,那么当错误发生时应该返回NULL

报错返回值的例子:

#define SOME_STR_FUNCTION_FAIL_RESULT (NULL)

string SomeStrFunction()
  {
   string result="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      return SOME_STR_FUNCTION_FAIL_RESULT;
     }
   return result;
  }

void OnTick()
  {
   string someStr=SomeStrFunction();

   if(someStr==SOME_STR_FUNCTION_FAIL_RESULT)
     {
      Print("fail");
      return;
     }
  }

然而,这个方式可能导致程序错误。如果这个操作没有记录下来,或者如果程序员自身对文档或者代码的执行不熟悉的话,那么他很可能对错误值没有意识。另外,在普通执行模式下如果一个函数或者方法能返回几乎任何值包括错误值,那就可能引起程序问题。


将执行结果分配给一个特殊的全局变量。通常这种方式应用于不返回任何值的方法和函数。想法是此方法或函数的返回值分配给一个特定的全局变量,然后这个变量的值在调用其的代码里做检查。为了实现这个功能,在MQL5里有一个默认函数(SetUserError())。

用SetUserError()分配错误代码的例子

#define SOME_STR_FUNCTION_FAIL_CODE (123)

string SomeStrFunction()
  {
   string result="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      SetUserError(SOME_STR_FUNCTION_FAIL_CODE);
      return "";
     }
   return result;
  }

void OnTick()
  {
   ResetLastError();
   string someStr=SomeStrFunction();

   if(GetLastError()==ERR_USER_ERROR_FIRST+SOME_STR_FUNCTION_FAIL_CODE)
     {
      Print("fail");
      return;
     }
  }

在这个例子中,编程者可能没有意识到可能出现的错误,然而,这个方法不仅能实现出错通知,还能指出特定的错误代码。如果有很多个错误源的话,这点尤其重要。


将执行结果作为bool型返回并将其作为参数的引用传入。这种方法要比前两者更加好一些,因为他能降低编程者出错的可能。一个方法或者函数可能无法正确的执行,这很难不引起关注。

将函数返回值作为bool型变量返回的例子

bool SomeStrFunction(string &value)
  {
   string resultValue="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      value="";
      return false;
     }
   value=resultValue;
   return true;
  }

void OnTick()
  {
   string someStr="";
   bool result=SomeStrFunction(someStr);

   if(!result)
     {
      Print("fail");
      return;
     }
  }

这个和之前提到的方法可以组合起来用,如果有多种不同的错误,我们就需要确定使用最合适的一种。返回false,全局变量被分配一个错误代码。

函数返回值为bool型变量并且使用SetUserError()分配错误代码的例子

#define SOME_STR_FUNCTION_FAIL_CODE_1 (123)
#define SOME_STR_FUNCTION_FAIL_CODE_2 (124)

bool SomeStrFunction(string &value)
  {
   string resultValue="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      value="";

      SetUserError(SOME_STR_FUNCTION_FAIL_CODE_1);

      return false;
     }

   bool resultOfSomeOperation2=SomeOperation2();

   if(!resultOfSomeOperation2)
     {
      value="";
      SetUserError(SOME_STR_FUNCTION_FAIL_CODE_2);
      return false;
     }
   value=resultValue;
   return true;
  }

void OnTick()
  {
   string someStr="";
   bool result=SomeStrFunction(someStr);

   if(!result)
     {
      Print("fail, code = "+(string)(GetLastError()-ERR_USER_ERROR_FIRST));
      return;
     }
  }

然而,这个方式对于后期解释(当读代码时)和维护代码有一定困难。


从枚举类型中返回结果值,结果变量(如果有)通过引用传递。如果有多种可能的错误类型,当出现错误时这个方式允许返回一个特定的错误类型,而不使用全局变量。仅有一个值对应正确的执行过程,其余都将被视为错误。

函数返回值为枚举值的例子

enum ENUM_SOME_STR_FUNCTION_RESULT
  {
   SOME_STR_FUNCTION_SUCCES,
   SOME_STR_FUNCTION_FAIL_CODE_1,
   SOME_STR_FUNCTION_FAIL_CODE_2
  };

ENUM_SOME_STR_FUNCTION_RESULT SomeStrFunction(string &value)
  {
   string resultValue="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      value="";
      return SOME_STR_FUNCTION_FAIL_CODE_1;
     }

   bool resultOfSomeOperation2=SomeOperation2();

   if(!resultOfSomeOperation2)
     {
      value="";
      return SOME_STR_FUNCTION_FAIL_CODE_2;
     }

   value=resultValue;
   return SOME_STR_FUNCTION_SUCCES;
  }

void OnTick()
  {
   string someStr="";

   ENUM_SOME_STR_FUNCTION_RESULT result=SomeStrFunction(someStr);

   if(result!=SOME_STR_FUNCTION_SUCCES)
     {
      Print("fail, error = "+EnumToString(result));
      return;
     }
  }

不使用全局变量是这个方法的最大好处,因为操作不当或疏漏可能带来严重的问题。


返回一个结构体,包含一个布尔型变量或者一个枚举值和一个结果值。这个方式和之前无需使用引用作为参数传递的方法相关。此处使用enum更佳,它允许将来扩展执行结果列表。

以结构体形式返回函数执行结果,包含枚举值(enum)和结果值

enum ENUM_SOME_STR_FUNCTION_RESULT
  {
   SOME_STR_FUNCTION_SUCCES,
   SOME_STR_FUNCTION_FAIL_CODE_1,
   SOME_STR_FUNCTION_FAIL_CODE_2
  };

struct SomeStrFunctionResult
  {
   ENUM_SOME_STR_FUNCTION_RESULT code;
   char              value[255];
  };

SomeStrFunctionResult SomeStrFunction()
  {
   SomeStrFunctionResult result;

   string resultValue="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      result.code=SOME_STR_FUNCTION_FAIL_CODE_1;
      return result;
     }

   bool resultOfSomeOperation2=SomeOperation2();

   if(!resultOfSomeOperation2)
     {
      result.code=SOME_STR_FUNCTION_FAIL_CODE_2;
      return result;
     }

   result.code=SOME_STR_FUNCTION_SUCCES;
   StringToCharArray(resultValue,result.value);
   return result;
  }

void OnTick()
  {
   SomeStrFunctionResult result=SomeStrFunction();

   if(result.code!=SOME_STR_FUNCTION_SUCCES)
     {
      Print("fail, error = "+EnumToString(result.code));
      return;
     }
   string someStr=CharArrayToString(result.value);
  }


尝试执行一项操作多次。在确认执行失败前,有必要对一项操作进行多次尝试。例如,如果一个文件因为被另一个进程使用中而不能被读取,那么应增加请求时间间隔多次请求。很有可能届时另一个进程将释放此文件,则我们的方法或函数将能够读取该文件。

多次尝试打开文件的例子

string fileName="test.txt";
int fileHandle=INVALID_HANDLE;

for(int iTry=0; iTry<=10; iTry++)
  {
   fileHandle=FileOpen(fileName,FILE_TXT|FILE_READ|FILE_WRITE);

   if(fileHandle!=INVALID_HANDLE)
     {
      break;
     }
   Sleep(iTry*200);
  }

注意:上面的例子仅仅显示了此方法的核心,实际使用时需要对发生的错误进行分析。如果,例如错误5002(无效文件名称)或者5003(文件名过长)发生,那么就没有必要做进一步处理了。另外,必须要考虑到,在不希望有任何执行延迟的系统中这个方法就不能使用。


明确地通知用户。当某错误发生时用户必须清晰地通知到(通过弹窗,图表等)。清晰明确的通知经常以程序挂起或完全停止执行的形式组合使用。例如,如果帐户可用资金不足,或者用户输入了错误的参数,那么应该明确清晰地提醒他。

通知用户输入参数错误的例子

input uint MAFastPeriod = 10;
input uint MASlowPeriod = 200;

int OnInit()
  {
//---
   if(MAFastPeriod>=MASlowPeriod)
     {
      Alert("A period of fast moving average has to be less than a period of slow moving average!");
      return INIT_PARAMETERS_INCORRECT;
     }
//---
   return(INIT_SUCCEEDED);
  }

当然也有其他错误处理的方法,场面阐述的仅仅是最常用的。


错误处理的一般建议

选择恰当的错误处理等级。不用的程序使用完全不同的错误处理方式。在仅使用几次来检查一些小idea且不和第三方分享的小脚本中,忽略错误处理是可以的。相反,如果一个程序有成千上万的现在用户,那么应该合理的处理所有可能的错误。在每一种特定的情况下,对要试着对错误处理等级有一个深刻的理解。

选择一个适当的用户交互水平。明确的用户交互仅在特定的错误时需要:程序可以自己处理执行而不需要任何用户通知。找到一个折中的办法很重要:用户不应被错误告警轰炸,或者相反,在关键报错时没有提示。下面提供了一个较好的解决办法 — 在任何关键错误或需要介入的场景时用户可以被清晰的告知,并且其他情况下保留报错日志。

效验函数和方法的所有返回值。如果任何函数或方法有返回值,其中含有报错信息,那么最好对其进行检查。优化程序质量的机会不能放过。

如果可能的话,在执行某些操作前检查条件。例如,在尝试开仓前检查一下内容:

  1. 终端是否允许使用程序自动交易:TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)。
  2. 帐户是否允许自动交易:AccountInfoInteger(ACCOUNT_TRADE_EXPERT)。
  3. 和交易服务器的连接是否正常:TerminalInfoInteger(TERMINAL_CONNECTED)。
  4. 执行交易的参数是否正确:OrderCheck()。

紧随其后的是各种程序部分恰当的执行操作。一个常见的代码实例是没有考虑对交易服务器的频繁请求的追踪止损。这个函数通常每一个价格变动调用一次。如果价格有一个持续的单边运动,或者当尝试修改时出现错误,那么此函数几乎在每一个tick都发送交易修订请求(或者多个持仓的多个请求)。

当报价变动不频繁时,这种报错不会引起任何问题。否则,可能会带来严重的问题 — 异常频繁的交易修订请求可能导致特定账户同经纪商的自动交易连接断开,并带来不愉快的客户体验。最简单的方法是限制交易修订请求的频率:记住上一次请求的时间,并且20秒后再次请求。

每30秒执行追踪止损函数的例子

const int TRAILING_STOP_LOSS_SECONDS_INTERVAL=30;

void TrailingStopLoss()
  {
   static datetime prevModificationTime=0;

   if((int)TimeCurrent() -(int)prevModificationTime<=TRAILING_STOP_LOSS_SECONDS_INTERVAL)
     {
      return;
     }

//--- 修改止损
     {
      ...
      ...
      ...
      prevModificationTime=TimeCurrent();
     }
  }

当你尝试在短时间内放置许多挂单的时候,一样的问题可能发生,作者已经遇到过了。


为了程序的稳定和正确性。在写程序时需要兼顾稳定性和准确性。稳定性 就是说程序能够在错误发生时继续运行,即使它会导致略微不准确的结果。准确性 是指不允许返回错误结果或者执行错误操作。它们必须准确或者彻底缺失,也就是说最好停止程序而不是返回一个不准确的结果或者做一些错误的操作。

例如,如果一个指标不能执行计算,最好不要出信号而不是完全关闭。相反,对于自动交易机器人最好停止工作,而非过量交易。另外,自动交易程序可以在停止工作前通过推送消息通知用户,使得用户能够知道问题所在并及时处理之。


显示关于报错的有用信息。尝试使报错信息可读。当程序反馈报错 — “无法开仓” — 而没有进一步信息是不够的。最好提供更为确切的信息,例如“无法开仓:不正确的开仓手数(0.9999)”。程序是在弹窗中还是在日志中显示报错信息这没有关系。如果可能,在任何情况下(尤其是在日志文件分析时)用户和编程者都应该了解报错的原因并修复它。然而,用户侧不应被加载过多的信息:没有必要在弹窗中显示错误代码,因为用户对它也做不了什么。


用MQL5工具包做日志记录

日志文件通常由程序创建以便于开发者查找失败/报错的原因,以及在某个特定的时刻评估系统条件等。除此之外,日志记录可用于软件分析。


日志等级

通常日志文件中接收到的信息类型各不相同,且需要不同的关注等级。日志等级用于将含有各种类型的信息区分开来,并且能够对显示信息的类别自定义。通常实现有几种日志等级:

  • Debug: 调试信息。这个等级的日志包含在开发、调试和试运行阶段。
  • Info: 信息性消息。它们携带各种系统活动的信息(例如,操作开始/结束,开/平仓等)。这个级别的消息通常不需要任何反馈,但能够明显有助于研究导致报错的事件链。
  • Warning: 告警信息。这个级别的信息可能包含导致报错的场景描述,无需用户介入。例如,如果计算后的交易量小于最低允许交易量,程序自动修正它,然后以《告警》级别在日志中记录下来。
  • Error: 需要干预的报错信息。这个日志记录级别通常用于与保存一个特定的文件,打开和修改订单等发生错误时。换句话说,此类错误程序无法自己修复,因此需要用户或开发者的干预。
  • 致命的: 严重的错误信息,程序无法继续运行。这个级别的报错信息需要立即处理,通过email,SMS等方式通知用户或者程序开发者。下面我们将向您展示在MQL5中如何使用PUSH通知。


维护日志文件

MQL5中维护日志文件最简单的方式就是通过工具包中的Print 或者 PrintFormat函数。因此,所有的信息就被传到EA、指标和终端脚本日志中。

在EA中使用Print()函数显示记录日志的例子

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;
  }

这个方法有几个缺点:

  1. 从多个程序中得到的一串消息可能会混淆起来,使得日志分析复杂化。
  2. 由于日志文件容易生成和获得,有可能无意或有意的被用户删除。
  3. 实现和识别日志等级困难。
  4. 无法将日志信息重新定位到其他源中(外部文件,数据库,邮件等)。
  5. 无法对日志文件实施强制转换(使用数据和时间或者达到特定大小后替换文件)。

这个方式的优点:

  1. 使用同一个函数就够了,而无需引入任何其他东西。
  2. 很多情况下日志文件能直接在终端中查看,且无需单独对它进行搜索。

我们来实现一个自定义的日志机制,来克服使用Print()和PrintFormat()的所有缺点。然而,如果需要重用代码,给新的项目复用的话就需要一个日志记录机制(或者在代码中拒绝使用日志)。

下面的程序可以作为MQL5中日志记录机制的一个例子。

MQL5中实现自定义日志机制的例子

//+------------------------------------------------------------------+
//|                                                       logger.mqh |
//|                                   Copyright 2015, Sergey Eryomin |
//|                                             http://www.ensed.org |
//+------------------------------------------------------------------+
#property copyright "Sergey Eryomin"
#property link      "http://www.ensed.org"

#define LOG(level, message) CLogger::Add(level, message+" ("+__FILE__+"; "+__FUNCSIG__+"; Line: "+(string)__LINE__+")")
//---  执行“每1Mb一个新的日志文件”的最大文件数量
#define MAX_LOG_FILE_COUNTER (100000) 
//--- 1兆字节中的字节数量
#define BYTES_IN_MEGABYTE (1048576)
//--- 日志文件名称的最大长度
#define MAX_LOG_FILE_NAME_LENGTH (255)
//--- 日志级别
enum ENUM_LOG_LEVEL
  {
   LOG_LEVEL_DEBUG,
   LOG_LEVEL_INFO,
   LOG_LEVEL_WARNING,
   LOG_LEVEL_ERROR,
   LOG_LEVEL_FATAL
  };
//--- 日志记录方式
enum ENUM_LOGGING_METHOD
  {
   LOGGING_OUTPUT_METHOD_EXTERN_FILE,// 外部文件
   LOGGING_OUTPUT_METHOD_PRINT // Print 函数
  };
//--- 通知方法
enum ENUM_NOTIFICATION_METHOD
  {
   NOTIFICATION_METHOD_NONE,// 禁用
   NOTIFICATION_METHOD_ALERT,// Alert函数
   NOTIFICATION_METHOD_MAIL, // SendMail 函数
   NOTIFICATION_METHOD_PUSH // SendNotification 函数
  };
//--- 日志文件类型限制
enum ENUM_LOG_FILE_LIMIT_TYPE
  {
   LOG_FILE_LIMIT_TYPE_ONE_DAY,// 每天生成新的日志文件
   LOG_FILE_LIMIT_TYPE_ONE_MEGABYTE // 每1Mb生成新的日志文件
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CLogger
  {
public:
   //--- 向日志中添加一条消息
   //--- 注意:
   //--- 如果启用输出到外部文件模式但无法执行,
   //--- 那么消息输出将通过Print()完成
   static void Add(const ENUM_LOG_LEVEL level,const string message)
     {
      if(level>=m_logLevel)
        {
         Write(level,message);
        }

      if(level>=m_notifyLevel)
        {
         Notify(level,message);
        }
     }
   //--- 设置日志等级
   static void SetLevels(const ENUM_LOG_LEVEL logLevel,const ENUM_LOG_LEVEL notifyLevel)
     {
      m_logLevel=logLevel;
      //--- 通过通知输出的消息不应比写入日志的消息等级低
      m_notifyLevel=fmax(notifyLevel,m_logLevel);
     }
   //--- 设置日志记录方法
   static void SetLoggingMethod(const ENUM_LOGGING_METHOD loggingMethod)
     {
      m_loggingMethod=loggingMethod;
     }
   //--- 设置通知方式
   static void SetNotificationMethod(const ENUM_NOTIFICATION_METHOD notificationMethod)
     {
      m_notificationMethod=notificationMethod;
     }
   //--- 设置日志文件名
   static void SetLogFileName(const string logFileName)
     {
      m_logFileName=logFileName;
     }
   //--- 为一个日志文件设置限制类型
   static void SetLogFileLimitType(const ENUM_LOG_FILE_LIMIT_TYPE logFileLimitType)
     {
      m_logFileLimitType=logFileLimitType;
     }

private:
   //--- 此级别以及更高级别的消息将被保存在一个日志文件中
   static ENUM_LOG_LEVEL m_logLevel;
   //--- 此级别及更高日志等级的消息将以通知形式写入
   static ENUM_LOG_LEVEL m_notifyLevel;
   //--- 日志记录方式
   static ENUM_LOGGING_METHOD m_loggingMethod;
   //--- 通知方式
   static ENUM_NOTIFICATION_METHOD m_notificationMethod;
   //--- 日志文件名称
   static string     m_logFileName;
   //--- 日志文件的类型限制
   static ENUM_LOG_FILE_LIMIT_TYPE m_logFileLimitType;
   //--- 日志的文件名的获取结果           
   struct GettingFileLogNameResult
     {
                        GettingFileLogNameResult(void)
        {
         succes=false;
         ArrayInitialize(value,0);
        }
      bool              succes;
      char              value[MAX_LOG_FILE_NAME_LENGTH];
     };
   //--- 已经存在的日志文件大小的检查结果
   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
     };
   //--- 写入一个日志文件
   static void Write(const ENUM_LOG_LEVEL level,const string message)
     {
      switch(m_loggingMethod)
        {
         case LOGGING_OUTPUT_METHOD_EXTERN_FILE:
           {
            GettingFileLogNameResult getLogFileNameResult=GetLogFileName();
            //---
            if(getLogFileNameResult.succes)
              {
               string fileName=CharArrayToString(getLogFileNameResult.value);
               //---
               if(WriteToFile(fileName,GetDebugLevelStr(level)+": "+message))
                 {
                  break;
                 }
              }
           }
         case LOGGING_OUTPUT_METHOD_PRINT:
            default:
              {
               Print(GetDebugLevelStr(level)+": "+message);
               break;
              }
        }
     }
   //--- 执行通知
   static void Notify(const ENUM_LOG_LEVEL level,const string message)
     {
      if(m_notificationMethod==NOTIFICATION_METHOD_NONE)
        {
         return;
        }
      string fullMessage=TimeToString(TimeLocal(),TIME_DATE|TIME_SECONDS)+", "+Symbol()+" ("+GetPeriodStr()+"), "+message;
      //---
      switch(m_notificationMethod)
        {
         case NOTIFICATION_METHOD_MAIL:
           {
            if(TerminalInfoInteger(TERMINAL_EMAIL_ENABLED))
              {
               if(SendMail("Logger",fullMessage))
                 {
                  return;
                 }
              }
           }
         case NOTIFICATION_METHOD_PUSH:
           {
            if(TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED))
              {
               if(SendNotification(fullMessage))
                 {
                  return;
                 }
              }
           }
        }
      //---
      Alert(GetDebugLevelStr(level)+": "+message);
     }
   //--- 获取日志文件的名称
   static GettingFileLogNameResult GetLogFileName()
     {
      if(m_logFileName=="")
        {
         InitializeDefaultLogFileName();
        }
      //---
      switch(m_logFileLimitType)
        {
         case LOG_FILE_LIMIT_TYPE_ONE_DAY:
           {
            return GetLogFileNameOnOneDayLimit();
           }
         case LOG_FILE_LIMIT_TYPE_ONE_MEGABYTE:
           {
            return GetLogFileNameOnOneMegabyteLimit();
           }
         default:
           {
            GettingFileLogNameResult failResult;
            failResult.succes=false;
            return failResult;
           }
        }
     }
   //--- 如果有“每天创建新的日志文件”限制的情况下获取日志文件的名称
   static GettingFileLogNameResult GetLogFileNameOnOneDayLimit()
     {
      GettingFileLogNameResult result;
      string fileName=m_logFileName+"_"+Symbol()+"_"+GetPeriodStr()+"_"+TimeToString(TimeLocal(),TIME_DATE);
      StringReplace(fileName,".","_");
      fileName=fileName+".log";
      result.succes=(StringToCharArray(fileName,result.value)==StringLen(fileName)+1);
      return result;
     }
   //--- 如果有“每新增1Mb创建新的日志文件”限制的情况下获取日志文件的名称
   static GettingFileLogNameResult GetLogFileNameOnOneMegabyteLimit()
     {
      GettingFileLogNameResult result;
      //---
      for(int i=0; i<MAX_LOG_FILE_COUNTER; i++)
        {
         ResetLastError();
         string fileNameToCheck=m_logFileName+"_"+Symbol()+"_"+GetPeriodStr()+"_"+(string)i;
         StringReplace(fileNameToCheck,".","_");
         fileNameToCheck=fileNameToCheck+".log";
         ResetLastError();
         bool isExists=FileIsExist(fileNameToCheck);
         //---
         if(!isExists)
           {
            if(GetLastError()==5018)
              {
               continue;
              }
           }
         //---
         if(!isExists)
           {
            result.succes=(StringToCharArray(fileNameToCheck,result.value)==StringLen(fileNameToCheck)+1);

            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)+1);

               break;
              }
            else if(checkLogFileSize!=IS_LOG_FILE_NOT_LESS_ONE_MEGABYTE)
              {
               break;
              }
           }
        }
      //---
      return result;
     }
   //---
   static ENUM_LOG_FILE_SIZE_CHECKING_RESULT CheckLogFileSize(const string fileNameToCheck)
     {
      int fileHandle=FileOpen(fileNameToCheck,FILE_TXT|FILE_READ);
      //---
      if(fileHandle==INVALID_HANDLE)
        {
         return LOG_FILE_SIZE_CHECKING_ERROR;
        }
      //---
      ResetLastError();
      ulong fileSize=FileSize(fileHandle);
      FileClose(fileHandle);
      //---
      if(GetLastError()!=0)
        {
         return LOG_FILE_SIZE_CHECKING_ERROR;
        }
      //---
      if(fileSize<BYTES_IN_MEGABYTE)
        {
         return IS_LOG_FILE_LESS_ONE_MEGABYTE;
        }
      else
        {
         return IS_LOG_FILE_NOT_LESS_ONE_MEGABYTE;
        }
     }
   //--- 默认方式初始化日志文件名
   static void InitializeDefaultLogFileName()
     {
      m_logFileName=MQLInfoString(MQL_PROGRAM_NAME);
      //---
#ifdef __MQL4__
      StringReplace(m_logFileName,".ex4","");
#endif

#ifdef __MQL5__
      StringReplace(m_logFileName,".ex5","");
#endif
     }
   //--- 在文件中写入消息
   static bool WriteToFile(const string fileName,
                           const string text)
     {
      ResetLastError();
      string fullText=TimeToString(TimeLocal(),TIME_DATE|TIME_SECONDS)+", "+Symbol()+" ("+GetPeriodStr()+"), "+text;
      int fileHandle=FileOpen(fileName,FILE_TXT|FILE_READ|FILE_WRITE);
      bool result=true;
      //---
      if(fileHandle!=INVALID_HANDLE)
        {
         //--- 在文件末尾放置文件指针           
         if(!FileSeek(fileHandle,0,SEEK_END))
           {
            Print("Logger: FileSeek() is failed, error #",GetLastError(),"; text = \"",fullText,"\"; fileName = \"",fileName,"\"");
            result=false;
           }
         //--- 在文件中写入文本
         if(result)
           {
            if(FileWrite(fileHandle,fullText)==0)
              {
               Print("Logger: FileWrite() is failed, error #",GetLastError(),"; text = \"",fullText,"\"; fileName = \"",fileName,"\"");
               result=false;
              }
           }
         //---
         FileClose(fileHandle);
        }
      else
        {
         Print("Logger: FileOpen() is failed, error #",GetLastError(),"; text = \"",fullText,"\"; fileName = \"",fileName,"\"");
         result=false;
        }
      //---
      return result;
     }
   //--- 获取当前周期
   static string GetPeriodStr()
     {
      ResetLastError();
      string periodStr=EnumToString(Period());
      if(GetLastError()!=0)
        {
         periodStr=(string)Period();
        }
      StringReplace(periodStr,"PERIOD_","");
      //---
      return periodStr;
     }
   //---
   static string GetDebugLevelStr(const ENUM_LOG_LEVEL level)
     {
      ResetLastError();
      string levelStr=EnumToString(level);
      //---
      if(GetLastError()!=0)
        {
         levelStr=(string)level;
        }
      StringReplace(levelStr,"LOG_LEVEL_","");
      //---
      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;
string CLogger::m_logFileName="";
ENUM_LOG_FILE_LIMIT_TYPE CLogger::m_logFileLimitType=LOG_FILE_LIMIT_TYPE_ONE_DAY;
//+------------------------------------------------------------------+

这段代码可以作为独立的包含文件,例如 Logger.mqh,保存在<data_folder>/MQL5/Include (此文件作为本文的附件给出)。CLogger类的运行方式大致如下:

运行自定义日志记录机制的例子

#include <Logger.mqh>

//--- 初始化日志
void InitLogger()
  {
//--- 设置日志等级: 
//--- DEBUG level for writing messages in a log file
//--- ERROR-level for notification
   CLogger::SetLevels(LOG_LEVEL_DEBUG,LOG_LEVEL_ERROR);
//--- 设置通知方式为 PUSH 通知
   CLogger::SetNotificationMethod(NOTIFICATION_METHOD_PUSH);
//--- 将日志记录方式设置为写入外部文件
   CLogger::SetLoggingMethod(LOGGING_OUTPUT_METHOD_EXTERN_FILE);
//--- 为日志文件设置名称
   CLogger::SetLogFileName("my_log");
//--- 日志文件限制设置为“每天生成新的日志文件”
   CLogger::SetLogFileLimitType(LOG_FILE_LIMIT_TYPE_ONE_DAY);
  }

int OnInit()
  {
//---
   InitLogger();
//---
   CLogger::Add(LOG_LEVEL_INFO,"");
   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);
  }

首先,InitLogger()函数初始化所有日志相关的参数,然后消息被写入日志文件。这段代码的结果将被记录在日志文件《my_log_USDCAD_D1_2015_09_23.log»》中,在文件夹<data_folder>/MQL5/Files下:

2015.09.23 09:02:10, USDCAD (D1), INFO: 
2015.09.23 09:02:10, USDCAD (D1), INFO: ---------- OnInit() -----------
2015.09.23 09:02:10, USDCAD (D1), DEBUG: Example of debug message (LoggerTest.mq5; int OnInit(); Line: 36)
2015.09.23 09:02:10, USDCAD (D1), INFO: Example of info message (LoggerTest.mq5; int OnInit(); Line: 38)
2015.09.23 09:02:10, USDCAD (D1), WARNING: Example of warning message (LoggerTest.mq5; int OnInit(); Line: 40)
2015.09.23 09:02:10, USDCAD (D1), ERROR: Example of error message (LoggerTest.mq5; int OnInit(); Line: 42)
2015.09.23 09:02:10, USDCAD (D1), FATAL: Example of fatal message (LoggerTest.mq5; int OnInit(); Line: 44)

另外,ERROR 和 FATAL 级别的消息将通过PUSH-通知发送。

当把写入日志文件的消息等级设置为Warning时(CLogger::SetLevels(LOG_LEVEL_WARNING,LOG_LEVEL_ERROR)),输出如下:

2015.09.23 09:34:00, USDCAD (D1), WARNING: Example of warning message (LoggerTest.mq5; int OnInit(); Line: 40)
2015.09.23 09:34:00, USDCAD (D1), ERROR: Example of error message (LoggerTest.mq5; int OnInit(); Line: 42)
2015.09.23 09:34:00, USDCAD (D1), FATAL: Example of fatal message (LoggerTest.mq5; int OnInit(); Line: 44)

也就是说,消息等级比 WARNING 低的将不被保存。


CLogger类及LOG宏的公共方法

让我们分析下 CLogger 类和 LOG 宏的公共方法。


void SetLevels(const ENUM_LOG_LEVEL logLevel, const ENUM_LOG_LEVEL notifyLevel). 设置日志等级。

const ENUM_LOG_LEVEL logLevel — 此级别以及更高级别的消息将被保存在一个日志文件中。By default = LOG_LEVEL_INFO.

const ENUM_LOG_LEVEL notifyLevel — 此级别及更高日志等级的消息将以通知形式展现。By default = LOG_LEVEL_FATAL.

两者的可能值:

  • LOG_LEVEL_DEBUG,
  • LOG_LEVEL_INFO,
  • LOG_LEVEL_WARNING,
  • LOG_LEVEL_ERROR,
  • LOG_LEVEL_FATAL.


void SetLoggingMethod(const ENUM_LOGGING_METHOD loggingMethod). 设置日志记录方式。

const ENUM_LOGGING_METHOD loggingMethod — 日志方法。By default = LOGGING_OUTPUT_METHOD_EXTERN_FILE.

可能的值:

  • LOGGING_OUTPUT_METHOD_EXTERN_FILE — 外部文件,
  • LOGGING_OUTPUT_METHOD_PRINT — Print 函数。


void SetNotificationMethod(const ENUM_NOTIFICATION_METHOD notificationMethod). 设置通知方式。

const ENUM_NOTIFICATION_METHOD notificationMethod — 通知方法。By default = NOTIFICATION_METHOD_ALERT.

可能的值:

  • NOTIFICATION_METHOD_NONE — 禁用,
  • NOTIFICATION_METHOD_ALERT — Alert函数,
  • NOTIFICATION_METHOD_MAIL — SendMail函数,
  • NOTIFICATION_METHOD_PUSH — SendNotification函数。


void SetLogFileName(const string logFileName). 设置日志文件名。

const string logFileName — 日志文件的名称。默认值将是日志记录器所在的程序名称,(查看InitializeDefaultLogFileName()私有方法)。


void SetLogFileLimitType(const ENUM_LOG_FILE_LIMIT_TYPE logFileLimitType). 设置日志文件限制类型。

const ENUM_LOG_FILE_LIMIT_TYPE logFileLimitType — 日志文件的限制类型。默认值:LOG_FILE_LIMIT_TYPE_ONE_DAY。

可能的值:

  • LOG_FILE_LIMIT_TYPE_ONE_DAY — 每天一个新的日志文件。带有如下文件名的日志文件将被创建:my_log_USDCAD_D1_2015_09_21.log, my_log_USDCAD_D1_2015_09_22.log, my_log_USDCAD_D1_2015_09_23 .log 等。
  • LOG_FILE_LIMIT_TYPE_ONE_MEGABYTE — 每1Mb生成新的日志文件。带有如下文件名的日志文件将被创建: my_log_USDCAD_D1_0.log, my_log_USDCAD_D1_1.log, my_log_USDCAD_D1_2.log 等。当前一个文件达到1Mb后切换到下一个文件。


void Add(const ENUM_LOG_LEVEL level,const string message). 向日志中添加一则消息。

const ENUM_LOG_LEVEL level — 消息等级。可能的值:

  • LOG_LEVEL_DEBUG
  • LOG_LEVEL_INFO
  • LOG_LEVEL_WARNING
  • LOG_LEVEL_ERROR
  • LOG_LEVEL_FATAL

const string message — 一个文本消息。


除了Add方法外,LOG宏也被使用,它向消息中添加文件名,函数签名和行号,写入日志文件中:

#define LOG(level, message) CLogger::Add(level, message+" ("+__FILE__+"; "+__FUNCSIG__+"; Line: "+(string)__LINE__+")")

这个宏在debug时特别有用。

因此,此例子展示的日志记录机制允许:

  1. 定义日志等级(DEBUG..FATAL)。
  2. 当应通知用户时,设置消息等级。
  3. 设置日志应该被写入何处 — 在EA中通过Print()函数或者写入到外部文件。
  4. 对于输出到外部文件 — 确定该文件名并且设置日志文件限制:每天一个新日志文件,后者每1Mb一个新日志文件。
  5. 指定一个通知类型(Alert(), SendMail(), SendNotify())。

本文提出的方法当然只是一个例子,针对特定的场景可能需要做相应的修改(包括去除不必要的功能)。例如,除了写入一个外部文件和常规日志文件,可以通过另一种日志记录方式来实现写入数据库。


总结

本文中我们已经阐述了使用MQL5工具包进行报错处理及日志记录的方法。正确的报错处理和相关的日志记录能够大大提升软件的质量并且极大的简化后期维护。

本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/2041

附加的文件 |
logger.mqh (24.94 KB)
loggertest.mq5 (4.67 KB)

该作者的其他文章

使用HTML和CSS替换的记录(Log)文件 使用HTML和CSS替换的记录(Log)文件
本文中我们将讲述编写一个简单而功能强大的制作html文件的实例, 在过程中我们会学习调整它们的显示, 以及如何在您的EA交易和脚本程序中轻松实现和使用它们.
测试可视化: 人工交易 测试可视化: 人工交易
在历史中测试人工交易策略. 看您的交易算法怎样从简单摆设变成编程之精华!
在Linux桌面系统运行MetaTrader 4客户终端 在Linux桌面系统运行MetaTrader 4客户终端
本文讲述了使用非模拟器wine软件在Linux桌面系统上运行MetaTrader 4客户终端的详细设置步骤.
分形线的构造 分形线的构造
本文讲述了使用趋势线和分形来构造各种类型的分形线.