MQL5中的错误处理和日志记录(基础篇)
MQL5 里先把错误和日志这关过了
在 MQL5 写 EA 或指标,最容易被忽略的不是策略逻辑,而是运行时错误怎么接住、怎么留痕。MT5 内核通过 GetLastError() 返回整型错误码,常见如 4014(数组越界)、4301(未选中品种),不主动读就只能在日志里看到一句含糊的失败。 真实项目里建议每次调用 OrderSend / CopyBuffer 等后立即取错误码并写文件或 Print。日志量不用大,但必须带时间戳和错误码,否则三天后你根本分不清是哪根 K 线触的雷。 下面这段是最朴素的兜底写法,开 MT5 新建脚本粘进去就能跑,验证错误捕获是否生效。
class="type">int err = GetLastError(); if(err != class="num">0) Print("Error: ", err, " at ", TimeCurrent());
「为什么错误处理决定EA稳定性」
在 MT5 上跑 EA 或指标时,错误不是偶发意外,而是程序生命周期里的常态。订单 rejected、报价断流、数组越界,任何一步没接住就会让策略在实盘里静默失效。外汇与贵金属杠杆高、跳空频繁,这类失效带来的回撤风险被显著放大。 错误处理本身没有银弹。MQL5 里既有返回错误码的隐式检查,也有 throw/try-catch 的异常风格,还能配合标准库的日志工具落盘。不同写法各有代价:错误码轻量但易被忽略,异常清晰却要求调用链统一。实际任务里往往要混用,而不是套某个统一标准。 本文后续会拆开讲具体手法与日志配置,但核心先摆清:把错误当一等公民对待,才可能让你的程序在真实行情里活过三个月。
◍ MQL5里兜住错误的几种实战手法
EA跑在MT5上,最怕的不是策略亏,而是某根K线触发了数组越界或除零,整个程序直接被终端中断。除了这类运行时硬中断,开发者自己也得在逻辑层把错误接住,否则账户可能在无人察觉时被错单啃掉。 遇到致命问题,直接调 ExpertRemove() 让EA自我退市比硬扛更稳妥。比如开仓量算出来超出 SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MAX) 的上限,而账户保证金又不够,这时别强行下单,写一行 Print 记录然后退出,外汇和贵金属杠杆高,这种越界单对本金是实打实威胁。 把越界值夹回合法区间是最常用的软处理:用 MathMax 和 MathMin 把交易量钳在最小和最大手数之间。但注意,如果夹完仍大于资金容许,就必须放弃执行而不是默默成交。 返回错误标记也有讲究。字符串函数出错返 NULL,数值函数可返一个预定义失败常量;但光返错误值容易让调用方漏判,尤其接手老代码时。更好的做法是 bool 型返回值搭配引用传出结果,或者 SetUserError() 写全局错误码,调用处用 GetLastError() 比对。错误类型多的时候,直接返回枚举值最干净,不碰全局变量就少一处隐患。 文件被别的进程占着读不了?别立刻放弃,用循环隔几百毫秒重试几次,很可能对方释放了就好用了。但像错误5002(文件名非法)、5003(文件名过长)这种,重试纯属浪费,实时性要求高的系统也别用这招。 最后,参数填错或可用保证金不足,一定用 Alert 或图表对象把问题甩到用户脸上,别让EA在后台哑火。下面这段代码把上面几种路子揉在一起,逐行看一遍就能照抄。
class="type">void OnTick() { class="type">bool resultOfSomeOperation=SomeOperation(); if(!resultOfSomeOperation) { Alert("fail"); ExpertRemove(); class="kw">return; } } class="type">class="kw">double VolumeCalculation() { class="type">class="kw">double result=...; result=MathMax(result,SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN)); result=MathMin(result,SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MAX)); class="kw">return result; } class="macro">#define SOME_STR_FUNCTION_FAIL_RESULT(NULL) class="type">class="kw">string SomeStrFunction() { class="type">class="kw">string result=""; class="type">bool resultOfSomeOperation=SomeOperation(); if(!resultOfSomeOperation) { class="kw">return SOME_STR_FUNCTION_FAIL_RESULT; } class="kw">return result; } class="type">void OnTick() { class="type">class="kw">string someStr=SomeStrFunction(); if(someStr==SOME_STR_FUNCTION_FAIL_RESULT) { Print("fail"); class="kw">return; } } class="macro">#define SOME_STR_FUNCTION_FAIL_CODE(class="num">123) class="type">class="kw">string SomeStrFunction() { class="type">class="kw">string result=""; class="type">bool resultOfSomeOperation=SomeOperation(); if(!resultOfSomeOperation) { SetUserError(SOME_STR_FUNCTION_FAIL_CODE); class="kw">return ""; } class="kw">return result; } class="type">void OnTick() { ResetLastError(); class="type">class="kw">string someStr=SomeStrFunction(); if(GetLastError()==ERR_USER_ERROR_FIRST+SOME_STR_FUNCTION_FAIL_CODE) { Print("fail"); class="kw">return; } } class="type">bool SomeStrFunction(class="type">class="kw">string &value) { class="type">class="kw">string resultValue=""; class="type">bool resultOfSomeOperation=SomeOperation(); if(!resultOfSomeOperation) { value="";
用枚举和结构体替函数报错松绑
MQL5 里函数想向外传递失败原因,最偷懒的办法是 return false 然后在外部猜。但真要定位两步操作哪一步挂了,靠 bool 返回值就只剩瞎蒙。 原文先用 #define 定义了 123、124 两个用户错误码,在函数内调 SetUserError 后返回 false;OnTick 里用 GetLastError()-ERR_USER_ERROR_FIRST 把数字打出来。这样能区分失败类型,但错误码散在宏里,字符串拼接也容易写错。 更干净的做法是直接上枚举。把成功、失败码1、失败码2写进 ENUM_SOME_STR_FUNCTION_RESULT,函数返回枚举而非 bool。OnTick 拿到返回值后用 EnumToString 打印,连错误码含义都自带可读文本,调参排错时一眼能看清。 若还想顺带把结果数据一起带回,可以套个结构体:code 字段放枚举,value[255] 放 char 数组。函数返回结构体实例,调用方一次拿到状态与数据,省掉引用参数来回传 string 的开销。外汇与贵金属 EA 涉及高杠杆高风险,任何错误处理疏忽都可能让实盘在未预期分支里静默止损。
class="macro">#define SOME_STR_FUNCTION_FAIL_CODE_1(class="num">123) class="macro">#define SOME_STR_FUNCTION_FAIL_CODE_2(class="num">124) class="type">bool SomeStrFunction(class="type">class="kw">string &value) { class="type">class="kw">string resultValue=""; class="type">bool resultOfSomeOperation=SomeOperation(); if(!resultOfSomeOperation) { value=""; SetUserError(SOME_STR_FUNCTION_FAIL_CODE_1); class="kw">return false; } class="type">bool resultOfSomeOperation2=SomeOperation2(); if(!resultOfSomeOperation2) { value=""; SetUserError(SOME_STR_FUNCTION_FAIL_CODE_2); class="kw">return false; } value=resultValue; class="kw">return true; } class="type">void OnTick() { class="type">class="kw">string someStr=""; class="type">bool result=SomeStrFunction(someStr); if(!result) { Print("fail, code = "+(class="type">class="kw">string)(GetLastError()-ERR_USER_ERROR_FIRST)); class="kw">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(class="type">class="kw">string &value) { class="type">class="kw">string resultValue=""; class="type">bool resultOfSomeOperation=SomeOperation(); if(!resultOfSomeOperation) { value=""; class="kw">return SOME_STR_FUNCTION_FAIL_CODE_1; } class="type">bool resultOfSomeOperation2=SomeOperation2(); if(!resultOfSomeOperation2) { value=""; class="kw">return SOME_STR_FUNCTION_FAIL_CODE_2; } value=resultValue; class="kw">return SOME_STR_FUNCTION_SUCCES; } class="type">void OnTick() { class="type">class="kw">string someStr=""; ENUM_SOME_STR_FUNCTION_RESULT result=SomeStrFunction(someStr); if(result!=SOME_STR_FUNCTION_SUCCES) { Print("fail, error = "+EnumToString(result)); class="kw">return; } } enum ENUM_SOME_STR_FUNCTION_RESULT { SOME_STR_FUNCTION_SUCCES, SOME_STR_FUNCTION_FAIL_CODE_1, SOME_STR_FUNCTION_FAIL_CODE_2 }; class="kw">struct SomeStrFunctionResult { ENUM_SOME_STR_FUNCTION_RESULT code; class="type">char value[class="num">255]; }; SomeStrFunctionResult SomeStrFunction() { SomeStrFunctionResult result; class="type">class="kw">string resultValue=""; class="type">bool resultOfSomeOperation=SomeOperation(); if(!resultOfSomeOperation) { result.code=SOME_STR_FUNCTION_FAIL_CODE_1;
「把错误码和重试逻辑写进函数返回结构」
上面这段 MT5 代码把『字符串处理函数 + 文件打开重试 + 均线参数校验』三种常见坑位拼在了一起,核心思路是:别让函数只返回 bool,而是返回一个带 code 和 value 的结构体,调用方在 OnTick 里先判 code 再取字符串。 文件打开那段用了 0 到 10 次递增退避重试,第 iTry 次睡 iTry*200 毫秒,也就是最多约 11 秒(0+200+400+…+2000=11000ms)后放弃,避免文件被占用时直接崩。 均线初始化里 FAST=10、SLOW=200 是硬约束,若把 MAFastPeriod 调到 ≥200,OnInit 会弹 Alert 并返回 INIT_PARAMETERS_INCORRECT,EA 根本不会加载——外汇和贵金属波动大、杠杆高,参数错配可能让策略在极端行情里反向暴露,实盘前务必在策略测试器里先跑一遍 OnInit 校验。 打开 MT5 把这段代码贴进 EA,故意把 MAFastPeriod 设成 250,你会发现编译器不报错但图表加载失败,这就是结构返回 + 初始化拦截在帮你排雷。
class="kw">return result; } class="type">bool resultOfSomeOperation2=SomeOperation2(); if(!resultOfSomeOperation2) { result.code=SOME_STR_FUNCTION_FAIL_CODE_2; class="kw">return result; } result.code=SOME_STR_FUNCTION_SUCCES; StringToCharArray(resultValue,result.value); class="kw">return result; } class="type">void OnTick() { SomeStrFunctionResult result=SomeStrFunction(); if(result.code!=SOME_STR_FUNCTION_SUCCES) { Print("fail, error = "+EnumToString(result.code)); class="kw">return; } class="type">class="kw">string someStr=CharArrayToString(result.value); } class="type">class="kw">string fileName="test.txt"; class="type">int fileHandle=INVALID_HANDLE; for(class="type">int iTry=class="num">0; iTry<=class="num">10; iTry++) { fileHandle=FileOpen(fileName,FILE_TXT|FILE_READ|FILE_WRITE); if(fileHandle!=INVALID_HANDLE) { class="kw">break; } Sleep(iTry*class="num">200); } class="kw">input class="type">uint MAFastPeriod = class="num">10; class="kw">input class="type">uint MASlowPeriod = class="num">200; class="type">int OnInit() { class=class="str">"cmt">//--- if(MAFastPeriod>=MASlowPeriod) { Alert("A period of fast moving average has to be less than a period of slow moving average!"); class="kw">return INIT_PARAMETERS_INCORRECT; } class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); }