轻松快捷开发 MetaTrader 程序的函数库 (第十九部分) : 函数库消息类(基础篇)
「给函数库加一层消息类封装」
在 MT5 里做自己的 EA / 指标函数库时,最容易被忽略的是运行状态回传。很多自制库只返回 bool 或 int,调用方很难知道失败到底是参数错、句柄失效还是行情未就绪。 这一节落地的做法,是把“消息”单独做成一个类,而不是散落在各函数里用 Print() 硬写。类内部维护一个消息代码与文本映射,外部只需取代码就能在日志或面板里统一呈现。 实际跑过的一版参考实现,在 2019 年 12 月公开示例里被下载 3907 次,说明社区对“可维护的错误反馈”需求很实。外汇与贵金属杠杆高,策略逻辑一旦因静默错误失效,亏损可能快速放大,因此消息类至少应覆盖初始化、订单发送、指标句柄三类返回。 你可以现在打开 MT5 的 MetaEditor,新建一个 CMessage 类,先只写 SetCode() 和 GetText(),把现有库里所有 Print 替换掉,验证日志是否更易读。
把消息文本从类里抽出来
很多人觉得程序里的文本提示只是辅助,无关紧要。但在 MT5 函数库开发中,消息是用户和程序沟通的必要环节,不能随便写死。 目前库里用英文或俄文(跟随终端语言)输出提示的代码,直接散落在各个类的实现位置。这种做法在小规模时够用,但一旦要加第三种语言,或者把俄文翻译成英语之外的语种,就得跨多个文件搜替换,极其麻烦。 另一个被忽略的代价是体积。创建多个同类型对象时,每个对象都内嵌一份相同文本,编译后 EX5 程序会无谓膨胀。重复文本只保留一个副本,由各对象引用,才是合理结构。 现在函数库已有不少文本消息,是时候重排存储方式、支持俄/英之外语言的切换,并留一个能快速加新语言的接口。外汇与贵金属 EA 多语言部署时,这种改动会直接降低维护风险和编译体积。
◍ 用二维数组管好多语言消息
在 MT5 函数库里折腾文本提示,最省事的办法是把消息塞进一个二维数组:第一维是消息编号,第二维放不同语言的文案。实际约定是第二维 0 号位存终端默认语(当前库版本是俄语),1 号位存英语,再往后随便加用户自定义语种。 所有库内预置提示的编号收进一个枚举,调取时直接拿枚举当第一维下标。另一组数组专门接终端和交易服务器抛回的错误码——GetLastError() 的返回值就当作下标去查对应文案,定位很直接。 这类消息处理类至少要能覆盖这些动作:按预置编号以选定语言弹消息;按服务器返回码或 GetLastError 错误码翻成对应语言显示;原样转发外部传入的串;发邮件、推手机通知、走 FTP 传文件、播声音。 扩展语种不用改结构,往二维数组第二维追加一列翻译即可;切换语言也只是改一下第二维的索引值,用户侧成本极低。外汇和贵金属行情波动剧烈,这类提示仅作运行态辅助,不预示任何方向。
「文本消息数据库」
鉴于我们已决定将所有预定义的消息存储在数组当中,且有了指示所需消息在数组中位置的索引列表,因此在 \MQL5\Include\DoEasy\ Datas.mqh 函数库文件中为其创建所需的枚举( 第一个消息索引应等于用户错误列表开始的索引 — 这将避免函数库消息索引与终端的标准消息代码重叠): 如我们所见,枚举含有所有已有的函数库消息,这些消息按创建来源的文本,以及不同来源函数库类的消息,划分到不同的组。 如同我们创建其他函数库类一样,我们会将描述函数库消息的新常量添加到枚举列表中。 要在日记中显示的消息,现在会按相应的常量名称来设置,其文本则应从数组取得。 现在,我们需要创建一个二维数组,以便来自上述枚举中的每条消息都与其在数组中的文本位置(第一维)相对应,而消息本身将位于第二维中 — 零索引包含以用户所选国家语言显示的消息(在当前版本中为俄语),而第一个索引包含英语的消息。 我们编写所有函数库消息的数组,并 添加用来指定使用语言数量的宏替换,此举更加便利 : 在查找函数库消息的枚举和数组时,我们可以看到每条消息与枚举中声明的常量准确对应。 位于数组中的所有消息都应严格对应于位于枚举中的常量,或与 GetLastError() 函数返回的错误消息代码准确对应 。 若要添加另一种翻译语言,只需在每条消息中添加该语言的文本(在英文文本之后)。 如果要将两种预定义语言中的任何一种替换为另一种,则只需编辑现有消息。 例如( 对于上面突出显示的代码 ),为了添加德语版的文本“未知帐户类型”, 要在相应的英文消息 之后 添加翻译文本 : 添加另一种语言时, 应在所有消息数组中添加对应的消息文本 ,否则消息索引将被破坏,从而在尝试显示消息时会导致不可预测的结果。 我们为交易服务器的返回代码编写消息数组: 交易服务器返回的所有代码自 10004 开始。 一些代码被跳过,且没有说明,例如 10005 和 10037。 但它们仍然应该出现在数组中,因为所有代码都严格按升序安置在数组中。 所以,也为那些未用到的代码添加了“未知的交易服务器返回代码”消息。 更重要的是,交易服务器返回码从 10004 开始,而在数组中,它们从索引 0 开始。 这就是为什么在访问数组时,我们要将获得的代码减去交易服务器第一个返回代码的数值。 在此情况下,我们才能准确地按所需代码位于数组中的描述来获取其索引。 例如,对于 10004,我们获得索引值 0(10004 - 10004 = 0),而对于 10007,我们获得索引值 3(10007 - 10004 = 3)。 执行时间错误消息数组: 执行时间错误消息的数组( 图表错误部分): 执行时间错误消息数组( 图形对象错误部分): 执行时间错误消息数组( MarketInfo 错误部分): 执行时间错误消息数组( 访问历史记录错误部分): 执行时间错误消息数组( 全局变量错误部分): 执行时间错误消息的数组( 自定义指标缓冲区和属性错误部分): 执行时间错误消息的数组( “帐户错误”部分): 执行时间错误消息数组( “指标错误”部分): 执行时间错误消息数组( “市场深度错误”部分): 执行时间错误消息数组( 文件操作错误部分): 执行时间错误消息数组( 字符串转换错误部分): 执行时间错误消息数组( “处理数组错误”部分): 执行时间错误消息的数组( “OpenCL 操作错误”部分): 执行时间错误消息数组( WebRequest() 操作错误部分): 执行时间错误消息的数组( “网络(套接字)从左错误”部分): 执行时间错误消息数组(
标准库抛错清单里藏着哪些系统断点
在 MT5 自建 EA 或指标调用系统标准库时,一组 MSG_LIB_SYS_* 常量直接暴露了底层最容易崩的环节。把这些报错码摊开看,能少走很多弯路。 从价格与保证金读取开始,MSG_LIB_SYS_NOT_GET_PRICE 代表实时报价没拿回来,MSG_LIB_SYS_NOT_GET_MARGIN_RATES 是保证金比率获取失败,MSG_LIB_SYS_NOT_GET_DATAS 则更泛地指向数据缺失。若此时硬跑策略,外汇与贵金属的高杠杆环境下可能直接触发异常平仓。 文件与账户对象创建阶段也密集出错:MSG_LIB_SYS_FAILED_CREATE_STORAGE_FOLDER 说明存储目录建不成,MSG_LIB_SYS_FAILED_OPEN_FILE_FOR_WRITE 是写文件句柄没打开,MSG_LIB_SYS_FAILED_CREATE_CURR_ACC_OBJ 则意味着当前账户对象构造失败。这类错误往往不是行情问题,而是终端权限或路径配置不对。 事件与集合列表管理同样有一串特定码:MSG_LIB_SYS_EVENT_ALREADY_IN_LIST 提示事件重复注册,MSG_LIB_SYS_ERROR_NOT_MARKET_LIST 和 MSG_LIB_SYS_ERROR_NOT_HISTORY_LIST 区分了集合类型用错,MSG_LIB_SYS_FAILED_ADD_ORDER_TO_LIST / _DEAL_TO_LIST 则是订单成交记录塞不进容器。 最后一类偏向序列化:MSG_LIB_SYS_FAILED_WRITE_UARRAY_TO_FILE 与 MSG_LIB_SYS_FAILED_LOAD_UARRAY_FROM_FILE 对应 uchar 数组落盘与回读,MSG_LIB_SYS_FAILED_CREATE_OBJ_STRUCT_FROM_UARRAY 表示从字节数组重建对象结构失败。遇到 MSG_LIB_SYS_NO_TICKS_YET 时,说明Tick还没来,不宜在 OnInit 里急着算逻辑。 开 MT5 把这几行加进调试输出,哪条先弹出来,就先修哪条依赖。
MSG_LIB_SYS_NOT_GET_PRICE, class=class="str">"cmt">// Failed to get current prices. Error: MSG_LIB_SYS_NOT_GET_MARGIN_RATES, class=class="str">"cmt">// Failed to get margin ratios. Error: MSG_LIB_SYS_NOT_GET_DATAS, class=class="str">"cmt">// Failed to get data MSG_LIB_SYS_FAILED_CREATE_STORAGE_FOLDER, class=class="str">"cmt">// Failed to create folder for storing files. Error: MSG_LIB_SYS_FAILED_ADD_ACC_OBJ_TO_LIST, class=class="str">"cmt">// Error. Failed to add current account object to collection list MSG_LIB_SYS_FAILED_CREATE_CURR_ACC_OBJ, class=class="str">"cmt">// Error. Failed to create account object with current account data MSG_LIB_SYS_FAILED_OPEN_FILE_FOR_WRITE, class=class="str">"cmt">// Could not open file for writing MSG_LIB_SYS_INPUT_ERROR_NO_SYMBOL, class=class="str">"cmt">// Input error: no symbol MSG_LIB_SYS_FAILED_CREATE_SYM_OBJ, class=class="str">"cmt">// Failed to create symbol object MSG_LIB_SYS_FAILED_ADD_SYM_OBJ, class=class="str">"cmt">// Failed to add symbol MSG_LIB_SYS_NOT_GET_CURR_PRICES, class=class="str">"cmt">// Failed to get current prices by event symbol MSG_LIB_SYS_EVENT_ALREADY_IN_LIST, class=class="str">"cmt">// This event is already in the list MSG_LIB_SYS_ERROR_ALREADY_CREATED_COUNTER, class=class="str">"cmt">// Error. Counter with ID already created MSG_LIB_SYS_FAILED_CREATE_COUNTER, class=class="str">"cmt">// Failed to create timer counter MSG_LIB_SYS_FAILED_CREATE_TEMP_LIST, class=class="str">"cmt">// Error creating temporary list MSG_LIB_SYS_ERROR_NOT_MARKET_LIST, class=class="str">"cmt">// Error. This is not a market collection list MSG_LIB_SYS_ERROR_NOT_HISTORY_LIST, class=class="str">"cmt">// Error. This is not a history collection list MSG_LIB_SYS_FAILED_ADD_ORDER_TO_LIST, class=class="str">"cmt">// Could not add order to the list MSG_LIB_SYS_FAILED_ADD_DEAL_TO_LIST, class=class="str">"cmt">// Could not add deal to the list MSG_LIB_SYS_FAILED_ADD_CTRL_ORDER_TO_LIST, class=class="str">"cmt">// Failed to add control order MSG_LIB_SYS_FAILED_ADD_CTRL_POSITION_TO_LIST, class=class="str">"cmt">// Failed to add control position MSG_LIB_SYS_FAILED_ADD_MODIFIED_ORD_TO_LIST, class=class="str">"cmt">// Could not add modified order to the list of modified orders MSG_LIB_SYS_NO_TICKS_YET, class=class="str">"cmt">// No ticks yet MSG_LIB_SYS_FAILED_CREATE_OBJ_STRUCT, class=class="str">"cmt">// Could not create object structure MSG_LIB_SYS_FAILED_WRITE_UARRAY_TO_FILE, class=class="str">"cmt">// Could not write class="type">uchar array to file MSG_LIB_SYS_FAILED_LOAD_UARRAY_FROM_FILE, class=class="str">"cmt">// Could not load class="type">uchar array from file MSG_LIB_SYS_FAILED_CREATE_OBJ_STRUCT_FROM_UARRAY, class=class="str">"cmt">// Could not create object structure from class="type">uchar array MSG_LIB_SYS_FAILED_SAVE_OBJ_STRUCT_TO_UARRAY, class=class="str">"cmt">// Failed to save object structure to class="type">uchar array, error MSG_LIB_SYS_ERROR_INDEX class=class="str">"cmt">// Error. "index" value should be within class="num">0 - class="num">3
◍ 交易库报错枚举的落地写法
在自建 MQL5 交易封装库时,把系统级异常做成枚举常量是最省事的做法。下面这段定义覆盖了从符号数组为空到改单失败的全链路错误,直接贴进头文件就能用。 这些常量名本身即文档:例如 MSG_LIB_SYS_ERROR_FAILED_GET_PRICE_ASK 对应取不到卖价,MSG_LIB_SYS_ERROR_POSITION_ALREADY_CLOSED 说明仓位已被平掉。实盘里若日志频繁刷出 MSG_LIB_SYS_ERROR_FAILED_OPEN_BUY,先查交易环境而非策略逻辑——经纪商拒单或点差突变都可能触发。 外汇与贵金属属高杠杆品种,这类底层报错若被吞掉,会让 EA 在连亏时静默失控。建库时建议每个常量都接一个 Print 或 SendNotification,开 MT5 跑一遍空字符串符号就能验证首条报错是否生效。
MSG_LIB_SYS_ERROR_EMPTY_STRING, class=class="str">"cmt">// Error. Predefined symbols class="type">class="kw">string empty, to be used MSG_LIB_SYS_FAILED_PREPARING_SYMBOLS_ARRAY, class=class="str">"cmt">// Failed to prepare array of used symbols. Error MSG_LIB_SYS_INVALID_ORDER_TYPE, class=class="str">"cmt">// Invalid order type: MSG_LIB_SYS_ERROR_FAILED_GET_PRICE_ASK, class=class="str">"cmt">// Failed to get Ask price. Error MSG_LIB_SYS_ERROR_FAILED_GET_PRICE_BID, class=class="str">"cmt">// Failed to get Bid price. Error MSG_LIB_SYS_ERROR_FAILED_OPEN_BUY, class=class="str">"cmt">// Failed to open Buy position. Error MSG_LIB_SYS_ERROR_FAILED_PLACE_BUYLIMIT, class=class="str">"cmt">// Failed to set BuyLimit order. Error MSG_LIB_SYS_ERROR_FAILED_PLACE_BUYSTOP, class=class="str">"cmt">// Failed to set BuyStop order. Error MSG_LIB_SYS_ERROR_FAILED_PLACE_BUYSTOPLIMIT, class=class="str">"cmt">// Failed to set BuyStopLimit order. Error MSG_LIB_SYS_ERROR_FAILED_OPEN_SELL, class=class="str">"cmt">// Failed to open Sell position. Error MSG_LIB_SYS_ERROR_FAILED_PLACE_SELLLIMIT, class=class="str">"cmt">// Failed to set SellLimit order. Error MSG_LIB_SYS_ERROR_FAILED_PLACE_SELLSTOP, class=class="str">"cmt">// Failed to set SellStop order. Error MSG_LIB_SYS_ERROR_FAILED_PLACE_SELLSTOPLIMIT, class=class="str">"cmt">// Failed to set SellStopLimit order. Error MSG_LIB_SYS_ERROR_FAILED_SELECT_POS, class=class="str">"cmt">// Failed to select position. Error MSG_LIB_SYS_ERROR_POSITION_ALREADY_CLOSED, class=class="str">"cmt">// Position already closed MSG_LIB_SYS_ERROR_NOT_POSITION, class=class="str">"cmt">// Error. Not a position: MSG_LIB_SYS_ERROR_FAILED_CLOSE_POS, class=class="str">"cmt">// Failed to closed position. Error MSG_LIB_SYS_ERROR_FAILED_SELECT_POS_BY, class=class="str">"cmt">// Failed to select opposite position. Error MSG_LIB_SYS_ERROR_POSITION_BY_ALREADY_CLOSED, class=class="str">"cmt">// Opposite position already closed MSG_LIB_SYS_ERROR_NOT_POSITION_BY, class=class="str">"cmt">// Error. Opposite position is not a position: MSG_LIB_SYS_ERROR_FAILED_CLOSE_POS_BY, class=class="str">"cmt">// Failed to close position by opposite one. Error MSG_LIB_SYS_ERROR_FAILED_SELECT_ORD, class=class="str">"cmt">// Failed to select order. Error MSG_LIB_SYS_ERROR_ORDER_ALREADY_DELETED, class=class="str">"cmt">// Order already deleted MSG_LIB_SYS_ERROR_NOT_ORDER, class=class="str">"cmt">// Error. Not an order: MSG_LIB_SYS_ERROR_FAILED_DELETE_ORD, class=class="str">"cmt">// Failed to class="kw">delete order. Error MSG_LIB_SYS_ERROR_SELECT_CLOSED_POS_TO_MODIFY, class=class="str">"cmt">// Error. Closed position selected for modification: MSG_LIB_SYS_ERROR_FAILED_MODIFY_POS, class=class="str">"cmt">// Failed to modify position. Error MSG_LIB_SYS_ERROR_SELECT_DELETED_ORD_TO_MODIFY, class=class="str">"cmt">// Error. Removed order selected for modification: MSG_LIB_SYS_ERROR_FAILED_MODIFY_ORD, class=class="str">"cmt">// Failed to modify order. Error MSG_LIB_SYS_ERROR_CODE_OUT_OF_RANGE, class=class="str">"cmt">// Return code out of range of error codes