亲爱的读者,您好!
本文中,我们会研究在“EA 交易”/脚本/指标中查找错误的方式以及记录方法。我还会向您推荐一款查看日志的小程序 - LogMon。
查找错误是编程过程中不可或缺的一部分。编写新的代码块时,有必要检查其是否正确工作、有无逻辑错误。您可以通过三种不同的方式,查找您程序中的错误:
将逻辑步骤写入日志
每种方法都看一看。
1. 评估最终结果我们利用这种方法,对程序或其部分代码的结果进行分析。比如说,取一段简单的代码,为清晰起见,里面只包含一个明显的错误:
void OnStart() { //--- int intArray[10]; for(int i=0;i<9;i++) { intArray[i]=i; } Alert(intArray[9]); }
编译并运行,屏幕上就会显示 "0"。通过结果分析,我们应该得到数字 "9",所以我们推断程序未正常工作。这种查找错误的方法很常用,但不能找到错误位置。不妨试试第二种查找错误的方法,我们会用到调试。
2. 逐步调试此方法允许您找到程序逻辑出错的精确位置。MetaEditor在 'for' 循环内放入一个断点,开始调试并对 i 变量多加注意:
接下来点击 "Resume debugging" (继续调试),直到我们认定程序的整个过程均已正常。我们看到, "i" 变量值为 "8",我们就会退出循环,所以我们推断错误就在此行:
for(int i=0;i<9;i++)
也就是说,在对比 i 值与数字 9 的时候。将该行 "i<9" " 修复为 "i<10" 或者 "i<=9",再检查结果。我们得到了数字 9 - 正是预期的结果。利用调试,我们知道了程序运行时是如何操作的,而且能够修复出现的问题。此方法的弊端:
最后,我们来看看查找错误的第三种方法。
3. 将逻辑步骤写入日志我们利用这种方法记录程序的重大步骤。例如:初始化、达成交易、指标计算等。用一行代码升级我们的脚本。也就是说,我们会在每个迭代上显示 i 变量值:
void OnStart() { //--- int intArray[10]; for(int i=0;i<9;i++) { intArray[i]=i; Alert(i); } Alert(intArray[9]); }
运行并查看日志输出 - 数字 "0 1 2 3 4 5 6 7 8 0"。像之前说的一样,找出其成因并修复脚本。
这种查找错误方法的利弊:
- 程序运行时间延长(主要对优化而言属重要)。
小结:
第一种查找错误的方法不能追踪错误所在实际位置。我们利用的主要是它的速度。第二种方法 - 逐步调试,允许您找到错误的精确位置,但极为耗时。而且,如果您错过了目标代码块,就不得不重新开始。
最后,第三种方法 - 将逻辑步骤记录到日志中,允许您快速分析程序的工作,并保存结果。将您的“EA 交易”/指标/脚本的事件写入到日志的同时,您还可以轻松地找到错误,而且无需寻找错误发生的相应条件,无需花费大量时间来调试您的程序。接下来,我们会详细讲解这些记录信息的方法,并加以对比。而且,我还会为您提供最便利、最快捷的方式。
将信息写入日志的方法多种多样,但只有一些是从始至终都在使用,其它的仅在特别情况下使用。比如说,通过电子邮件或 ICQ 发送日志就不是总有必要的。
下面列出的就是 MQL5 编程中最常用到的方法:
接下来,我会给出带有源代码的每种方法的示例,并描述其各种功能。此类源代码都非常抽象,所以我们不会离题太远。
使用 Comment() 函数
void OnStart() { //--- int intArray[10]; for(int i=0;i<10;i++) { intArray[i]=i; Comment("变量 i: ",i); Sleep(5000); } Alert(intArray[9]); }
这样一来,我们就会在左上角看到 "i" 变量的当前值:
由此,我们即可监控运行程序的当前状态。现在来衡量一下利弊:
Comment() 函数用于显示“EA 交易”的当前状态。比如 "Open 2 deal" 或 "buy GBRUSD lot: 0.7".
使用 Alert() 函数
此函数会在一个独立的窗口中显示信息,且配有声音通知。代码示例:
void OnStart() { //--- Alert("启动脚本"); int intArray[10]; for(int i=0;i<10;i++) { intArray[i]=i; Alert("变量 i:", I); Sleep(1000); } Alert(intArray[9]); Alert("停止脚本"); }代码执行的结果:
现在,我们心中狂喜,结果马上就要明朗,甚至都有声音了。但是现在,先说说利弊吧:
实际交易中第 6 点非常关键,尤其是超短线交易或修改止损价时。缺点非常多,不胜枚举,但我觉得这就够了。
使用 Print() 函数
此函数会将日志消息写入名为 "Experts" 的专门窗口。代码如下:
void OnStart() { //--- Print("启动脚本"); int intArray[10]; for(int i=0;i<10;i++) { intArray[i]=i; Print("变量 i: ",i); } Print(intArray[9]); Print("停止脚本"); }
您也看到了,此函数的调用与 Alert() 函数类似,只是现在是在无通知的情况下,将所有消息写入 "Experts" 选项卡,并写入 "Terminal_dir\MQL5\Logs\data.txt" 文件。再研究一下该方法的利弊:
很可能大多数 MQL5 程序员都是采用此方法,它相当快速,而且非常适合大量的日志记录。
将日志写入文件
再来探讨最后一种记录方法 - 将消息写入文件。与前面的方法相比,它要复杂得多。但是,在准备妥善的情况下,则会确保良好的写入速度,而且日志和通知的查看也方便快捷。下面是将日志写入文件的最简单的代码:
void OnStart() { //--- 打开日志文件 int fileHandle=FileOpen("log.txt",FILE_WRITE|FILE_TXT|FILE_SHARE_READ|FILE_UNICODE); FileWrite(fileHandle,"启动脚本"); int intArray[10]; for(int i=0;i<10;i++) { intArray[i]=i; FileWrite(fileHandle,"变量 i: ",i); // Sleep(1000); } FileWrite(fileHandle,intArray[9]); FileWrite(FileHandle,"停止脚本"); FileClose(fileHandle); // 关闭日志文件 }
运行并浏览至 "Terminal_dir\MQL5\Files" 文件夹,用文本编辑器打开 "log.txt" 文件。内容如下:
如您所见,作为结果的输出无额外消息,只是我们向该文件写入的内容。说一说利弊:
小结:
上述的所有方法都有自身缺点,但是您可以修正改善一些。前三种记录方法不够灵活,我们几乎无法影响其行为。而最后一种方法 - 将日志写入文件则最为灵活,我们可以决定消息记录的方式和时间。如果您想显示单独的一个数字,则显然前三种方法更方便。但如果您拥有一个含有大量代码的复杂程序,没有记录则很难使用。
现在,我来告诉您怎样改善把记录写入文件的方法,再给你一种查看日志的称手工具。这是一款 Windows 应用程序,名为 LogMon,是我用 C++ 编写的。
先开始编写类吧,让它来执行所有的记录,也就是说:
因为 MQL5 是一种面向对象语言,与 C++ 在速度方面没有太大的区别,所以我们要编写一个 MQL5 专用的类。开始吧。
我们会将自己的类放入一个扩展名为 mqh 的独立包含文件中。此为类的一般结构。
下面是带有详尽注释的类的源代码:
//+------------------------------------------------------------------+ //| Clogger.mqh | //| ProF | //| http:// | //+------------------------------------------------------------------+ #property copyright "ProF" #property link "http://" // 高速缓存的最大尺寸 (数量) #define MAX_CACHE_SIZE 10000 // 以MB为单位最大文件尺寸 #define MAX_FILE_SIZEMB 10 //+------------------------------------------------------------------+ //| Logger | //+------------------------------------------------------------------+ class CLogger { private: string project,file; // 项目的名称和日志文件 string logCache[MAX_CACHE_SIZE]; // 最大缓存大小 int sizeCache; // 缓存计数器 int cacheTimeLimit; // 缓存时间 datetime cacheTime; // 最后刷缓存至文件的时间 int handleFile; // 日志文件句柄 string defCategory; // 省缺类别 void writeLog(string log_msg); // 写消息到日志或文件, 并刷缓存 public: void CLogger(void){cacheTimeLimit=0; cacheTime=0; sizeCache=0;}; // 构造器 void ~CLogger(void){}; // 析构器 void SetSetting(string project,string file_name, string default_category="",int cache_time_limit=0); // 设置 void init(); // 初始化, 打开文件写 void deinit(); // 关闭文件 void write(string msg,string category=""); // 生成消息 void write(string msg,string category,color colorOfMsg,string file="",int line=0); // 生成消息 void write(string msg,string category,uchar red,uchar green,uchar blue, string file="",int line=0); // 生成消息 void flush(void); // 刷缓存至文件 }; //+------------------------------------------------------------------+ //| 设置 | //+------------------------------------------------------------------+ void CLogger::SetSetting(string project_name,string file_name, string default_category="",int cache_time_limit=0) { project=project_name; // 项目名 file=file_name; // 文件名 cacheTimeLimit=cache_time_limit; // 缓存时间 if(default_category=="") // 设置省缺类别 { defCategory="注释"; } else {defCategory = default_category;} } //+------------------------------------------------------------------+ //| 初始化 | //+------------------------------------------------------------------+ void CLogger::init(void) { string path; MqlDateTime date; int i=0; TimeToStruct(TimeCurrent(),date); // 得到当前时间 StringConcatenate(path,"log\\log_",project,"\\log_",file,"_", date.year,date.mon,date.day); // 生成文件名和路径 handleFile=FileOpen(path+".txt",FILE_WRITE|FILE_READ| FILE_UNICODE|FILE_TXT|FILE_SHARE_READ); // 打开或创建新文件 while(FileSize(handleFile)>(MAX_FILE_SIZEMB*1000000)) // 检查文件大小 { // 打开或创建新日志文件 i++; FileClose(handleFile); handleFile=FileOpen(path+"_"+(string)i+".txt", FILE_WRITE|FILE_READ|FILE_UNICODE|FILE_TXT|FILE_SHARE_READ); } FileSeek(handleFile,0,SEEK_END); // 设置指针到文件尾 } //+------------------------------------------------------------------+ //| 去初始化 | //+------------------------------------------------------------------+ void CLogger::deinit(void) { FileClose(handleFile); // 关闭文件 } //+------------------------------------------------------------------+ //| 写信息至文件或缓存 | //+------------------------------------------------------------------+ void CLogger::writeLog(string log_msg) { if(cacheTimeLimit!=0) // 检查缓存是否允许 { if((sizeCache<MAX_CACHE_SIZE-1 && TimeCurrent()-cacheTime<cacheTimeLimit) || sizeCache==0) // 检查缓存时间是否超出或到达缓存限制 { // 写文件至缓存 logCache[sizeCache++]=log_msg; } else { // 写文件至缓存并刷缓存至文件 logCache[sizeCache++]=log_msg; flush(); } } else { // 缓存被禁止, 立即写入文件 FileWrite(handleFile,log_msg); } if(FileTell(handleFile)>(MAX_FILE_SIZEMB*1000000)) // 检查当前文件大小 { // 文件大小超出允许限制, 关闭当前文件并打开新的文件 deinit(); init(); } } //+------------------------------------------------------------------+ //| 生成信息和写入日志 | //+------------------------------------------------------------------+ void CLogger::write(string msg,string category="") { string msg_log; if(category=="") // 检查传递的类别是否存在 { category=defCategory; } // 设置省缺类别 // 生成行并调用写消息方法 StringConcatenate(msg_log,category,":|:",TimeToString(TimeCurrent(),TIME_SECONDS)," ",msg); writeLog(msg_log); } //+------------------------------------------------------------------+ //| 生成信息和写入日志 | //+------------------------------------------------------------------+ void CLogger::write(string msg,string category,color colorOfMsg,string file="",int line=0) { string msg_log; int red,green,blue; red=(colorOfMsg &Red); // 从常量选择红色 green=(colorOfMsg &0x00FF00)>>8; // 从常量选择绿色 blue=(colorOfMsg &Blue)>>16; // 从常量选择蓝色 // 检查文件或行是否已传递,生成行并调用写消息方法 if(file!="" && line!=0) { StringConcatenate(msg_log,category,":|:",red,",",green,",",blue, ":|:",TimeToString(TimeCurrent(),TIME_SECONDS)," ", "文件: ",file," 行: ",line," ",msg); } else { StringConcatenate(msg_log,category,":|:",red,",",green,",",blue, ":|:",TimeToString(TimeCurrent(),TIME_SECONDS)," ",msg); } writeLog(msg_log); } //+------------------------------------------------------------------+ //| 生成信息和写入日志 | //+------------------------------------------------------------------+ void CLogger::write(string msg,string category,uchar red,uchar green,uchar blue,string file="",int line=0) { string msg_log; // 检查文件或行是否已传递,生成行并调用写消息方法 if(file!="" && line!=0) { StringConcatenate(msg_log,category,":|:",red,",",green,",",blue, ":|:",TimeToString(TimeCurrent(),TIME_SECONDS)," ", "文件: ",file," 行: ",line," ",msg); } else { StringConcatenate(msg_log,category,":|:",red,",",green,",",blue, ":|:",TimeToString(TimeCurrent(),TIME_SECONDS)," ",msg); } writeLog(msg_log); } //+------------------------------------------------------------------+ //| 刷新缓存至文件 | //+------------------------------------------------------------------+ void CLogger::flush(void) { for(int i=0;i<sizeCache;i++) // 循环写所有消息至文件 { FileWrite(handleFile,logCache[i]); } sizeCache=0; // 重置缓存计数器 cacheTime=TimeCurrent(); // 设置缓存重置时间 } //+------------------------------------------------------------------
在 MetaEditor 中创建包含文件 (.mqh),复制类的源代码,并保存于 "CLogger.mqh" 名下。现在,我们再多谈谈每一种方法,说说如何应用这个类。
使用 CLogger 类
要开始利用该类将消息录入日志,我们需要把类文件纳入到“EA 交易”/指标/脚本:
#include <CLogger.mqh>
接下来,您必须要创建一个该类的对象:
CLogger logger;
我们会利用 "logger" 对象执行所有操作。现在,我们需要通过调用 "SetSetting()" 法调整设置。我们需要将项目名称和文件名称传递到该方法内。还有两个可选参数 - 缺省分类的名称和缓存时间(以秒计,指缓存被写入文件之前的存储期)。如果指定为零,则所有消息会被写入一次。
SetSetting(string project, // 项目名 string file_name, // 日志文件名 string default_category="", // 省缺类别 int cache_time_limit=0 // 缓存生命时间秒数 );
调用示例:
logger.SetSetting("我的项目","我的日志","注释",60);
结果是,消息会被写入 "Client_Terminal_dir\MQL5\Files\log\log_MyProject\log_myLog_date.txt" 文件,缺省分类为 "Comment",缓存时间为 60 秒。之后,您需要调用 init() 方法以打开/创建日志文件。调用示例很简单,因为您无需传递参数:
logger.init();
此方法会生成日志文件的路径和名称,打开它并检查其是否超过了大小上限。如果大小超过了之前设置的常量值,则会打开另一份文件,且有 1 连接其名称。然后再次检查尺寸,直到打开的文件大小正确。
之后,指针移至文件末尾位置。现在,对象已做好了写入日志的准备。我们覆盖了写入方法。我们可以靠它设置消息的不同结构、调用写入方法的示例以及文件中的结果:
// 以省缺类别写消息 logger.write("测试消息"); // 以“错误”类别写消息 logger.write("测试消息", "错误"); // 以“错误”类别写消息,将以红色突出显示在日志监视 logger.write("测试消息", "错误",Red); // 以“错误”类别写消息,将以红色突出显示在日志监视 // 同样,消息将包括当前文件名和当前行 logger.write("测试消息", "错误",Red,__FILE__,__LINE__); // 以“错误”类别写消息,将以绿黄色突出显示在日志监视 // 但现在我们指定每种独立颜色为: 红, 绿, 蓝. 0-黑, 255 - 白 logger.write("测试消息", "错误",173,255,47); // 以“错误”类别写消息,将以绿黄色突出显示在日志监视 // 但现在我们指定每种独立颜色为: 红, 绿, 蓝. 0-黑, 255 - 白 // 同样,消息将包括当前文件名和当前行 logger.write("测试消息", "错误",173,255,47,__FILE__,__LINE__);
日志文件将包含下述行:
注释:|:23:13:12 测试消息 错误:|:23:13:12 测试消息 错误:|:255,0,0:|:23:13:12 测试消息 错误:|:255,0,0:|:23:13:12 文件: testLogger.mq5 行: 27 测试消息 错误:|:173,255,47:|:23:13:12 测试消息 错误:|:173,255,47:|:23:13:12 文件: testLogger.mq5 行: 29 测试消息
看到了吧,一切都是那么地简单。无论在任何地方调用带所需参数的 write() 方法,都会将消息写入文件。在程序的结尾,您需要插入两个方法的调用 - flush() 和 deinit()。
logger.flush(); // 强制刷缓存至硬盘 logger.deinit(); // 关闭文件
下面是将循环数字写入日志的一个简单的脚本示例:
//+------------------------------------------------------------------+ //| testLogger.mq5 | //| ProF | //| http:// | //+------------------------------------------------------------------+ #property copyright "ProF" #property link "http://" #property version "1.00" #include <Сlogger.mqh> CLogger logger; //+------------------------------------------------------------------+ //| 脚本程序初始函数 | //+------------------------------------------------------------------+ void OnStart() { //--- logger.SetSetting("proj","lfile"); // 设置 logger.init(); // 初始化 logger.write("启动脚本","系统"); for(int i=0;i<100000;i++) // 写 100000 消息到日志 { logger.write("日志: "+(string)i,"注释",100,222,100,__FILE__,__LINE__); } logger.write("停止脚本","系统"); logger.flush(); // 刷缓存 logger.deinit(); // 卸载 } //+------------------------------------------------------------------
脚本于 3 秒后执行,并创建了 2 个文件:
文件内容:
全部 100000 条消息均是如此。看到了吧,一切运行都是相当快速。您可以修改此类,添加新功能或是进行优化。
既然您编写了一个程序,您就必须会显示几种类型的消息:
还有一种明智之举,那就是在不更改源代码的情况下,调整想要打印的信息。我们会将此目标作为一个简单的函数实现,而且不会用到类和方法。
声明会存储消息输出量的变量参数。变量中的数越大,将显示的消息分类就越多。如果您想完全禁用消息输出,则为其赋值 "-1"。
input int dLvl=2;
下面是函数的源代码,且必须在创建 CLogger 类的对象之后声明。
void debug(string debugMsg, // 消息文本 int lvl ) // 消息级别 { if (lvl<=dLvl) // 以消息输出级别比较消息级别 { if (lvl==0) // 如果消息是紧急 (级别 = 0) {logger.write(debugMsg,"",Red);} // 标记为红色 else {logger.write(debugMsg);} // 否则打印省缺颜色 } }
来看一个示例:为最重要的消息指定量 "0",将任意数字(从零开始按升序排列)指定给用途最小的消息。
debug("EA 错误!",0); // 紧急错误 debug("止损执行",1); // 通知 int i = 99; debug("变量 i:"+(string)i,2); // 调试信息,变量内容
好,现在我们已经拥有包含数千行内容的日志文件了。但是,要在其中查找信息可是相当困难了。它们未被划分为各个类别,彼此又没什么区别。我曾试图解决这一问题,编写一个程序,来查看由 CLogger 类生成的日志。现在我为您简单地介绍一下 LogMon - 一款利用 WinAPI 以 C++ 语言编写的程序。正因如此,它速度快、且体积小。本程序完全免费。
要使用本程序,您需要:
程序主窗口如下所示:
主窗口中包含工具栏和带有树状视图的窗口。要展开某个项目,则用鼠标左键双击。列表中的文件中 - 都是项目,位于l "Client_Terminal_dir\MQL\Files\log\" 文件夹中。您要利用 SetSetting() 方法设定 CLogger 中项目的名称。文件夹列表中的文件 - 是实际上的日志文件。日志文件中的消息,都被划分为您利用 write() 方法指定的分类。括号中的数字 - 是指该分类中的消息数量。
现在,我们从左到右来研究工具栏上的按钮。
删除项目或日志文件以及复位树状视图的按钮
如果按下该按钮,就会出现下述窗口:
如果按下 "Delete and Flush" (删除与清除)按钮,扫描文件/文件夹的所有线程都会被停止,树状视图会被重置,并提示您删除选定文件或项目(只需点击某元素以将其选定 - 无需勾选复选框!)。"Reset" (复位)按钮会停止所有扫描文件/文件夹的线程,并清空树状视图。
查看 "About" (关于)对话框的按钮
显示有关程序及其编程者的简要信息。
程序窗口始终置顶显示的按钮
将程序窗口置于所有其它窗口之上。
日志文件中新消息监控激活的按钮
此按钮会将程序窗口隐藏到系统托盘 并激活日志文件中的新消息监控。要选择待扫描的项目/文件/分类,则勾选必要元素旁边的复选框。
如果您勾选消息分类旁边的复选框,则会根据该项目/文件/分类中的新消息触发通知。如果您勾选文件旁边的复选框,则会根据该文件(任何分类)的新消息触发通知。最后,如果您勾选项目旁边的复选框,则会根据新日志文件及文件中的消息触发通知。
监控
如果您已激活监控且将程序窗口最小化至系统托盘,那么,当选定元素中出现新消息时,主应用程序窗口就会最大化,且伴有声音通知。要禁用通知,则用鼠标左键点击列表中的任何地方。要停止监控,则点击系统托盘中的程序图标 。要将通知声音更换为自己的,则将名为 "alert.wav" 的 .wav 文件放入程序执行文件的相同文件夹。
查看日志分类
要查看具体分类,只需双击它。之后就会看到消息框:
您可以在此窗口中搜索消息,锁定窗口永处最前,并切换自动滚动。每条信息的颜色,都利用 CLogger 类的 write() 方法分别设置。消息的背景会利用选定颜色高亮显示。
双击某消息时,它会打开一个独立的窗口。如果消息太长、与对话框不匹配,用它就会很方便:
现在,您有了一件查看并监控日志文件的称手工具。衷心期望此款程序能在您开发和使用 MQL5 程序的过程中给您帮助。
您程序中的记录事件非常有用,它会帮助您识别隐藏的错误,发现改善您程序的机会。本文中,我们讲述了记录到文件、日志监控与查看的最简便方法和程序。
期待您的评论和建议!
本社区仅针对特定人员开放
查看需注册登录并通过风险意识测评
5秒后跳转登录页面...
移动端课程