轻松快捷开发 MetaTrader 程序的函数库(第 二十五部分):处理交易服务器返回的错误(基础篇)
◍ 给交易服务器报错留个统一出口
在 MT5 里写 EA 或脚本,最容易被忽略的就是交易服务器返回的错误码处理。一次 OrderSend 或 PositionOpen 失败,如果不把 RETCODE 抓出来,你连是报价过期还是禁止交易都分不清。 很多新手把错误打印直接丢给 Print(),结果日志里一堆数字,复盘时根本对不上上下文。更稳的做法是在函数库里单独封一层错误分发:拿到交易函数的返回值后,先判断 retcode 是否属于可重试类(如 10030 报价过期、10062 请求频繁),再决定重发还是告警。 这一节要落地的只有一件事:开 MT5 新建一个 include 文件,把交易错误码到中文说明的映射先列出来,后面每篇都会往这个库里加东西。外汇和贵金属杠杆高,服务器拒绝订单是常态,先把错误看清再谈策略。
服务器响应回来后到底怎么拆
把订单推到服务器只是前半程,真正麻烦的是读回那串响应。返回的不只是“成”或“不成”,而是一组错误代码,需要按情形分流:无错误代表已排队;若账户被禁EA或端侧发不出去,属于交易环境被掐断;若持仓已平或挂单没了,应直接退出本次方法。 碰到参数类无效值,逻辑和之前校正请求参数一致——改完重发;若服务器行情数据变了但请求值不用动,就刷新数据再试;最阴的是价格蹭到止损附近,FreezeLevel 会把修改锁死,这时要么等价格离开冻结区,要么撤请求,否则重发也是白白吃拒绝。 返回码比参数校验阶段多得多,且不是每个都能靠纠正重试救回来。我们在发单方法里套一层循环,按交易类里设的尝试次数重复提交,直到成功或次数耗尽;全失败就返回 false,并把服务器给的最后一个错误码抛给调用层,由你决定怎么处置。 外汇与贵金属杠杆高、滑点突变频繁,这种重发机制只是降低“假失败”概率,不保证成交,实盘前务必在 MT5 策略测试器里跑通拒绝码分支。
「给交易库接上延后请求与计时器」
要支持延后请求,先得在 Account.mqh 的 CAccount 里补一个返回对冲账户标志的方法,并在 Defines.mqh 用宏定义默认交易尝试次数。 Trading.mqh 的 CTrading 私密段要加延后请求列表和 m_total_try 变量,构造函数里给 m_total_try 设默认值并清列表、置排序标志。 价格校验逻辑也改了:检查 StopLevel 的方法现在接收 StopLimit 限价参数,若限价为零则查止损/限价距离,否则查 stop limit 激活后的挂单价。以前图表基于 Last 价时订单价取 Ask/Last,现在无论图表价源一律用 Ask/Bid,避免错单。 公开接口上,CEngine 给交易类加了点差倍数设置方法(可全局或单品种),构造函数新建计时器计数器,计时器内嵌交易类计时器操控块。完全平仓方法改为传 -1 表示全平,部分平仓传具体量。 下面这段是 CAccount 简化访问属性的代码片段,注意 TradeMode、MarginSOMode 等只是把整型属性强转枚举,Login/Leverage 直接返回长整型——改库时别在这些 getter 里写业务逻辑,否则延后请求调试会很难排。 外汇与贵金属杠杆高、滑点大,延后请求重试次数和点差倍数务必在策略层显式设定,否则可能放大敞口。
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| Methods of a simplified access to the account object properties |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//--- Return the account&class="macro">#x27;s integer properties</span> <span class="macro">ENUM_ACCOUNT_TRADE_MODE</span> TradeMode(<span class="keyword">class="type">void</span>) <span class="keyword">class="kw">const</span> { <span class="keyword">class="kw">return</span> (<span class="macro">ENUM_ACCOUNT_TRADE_MODE</span>)<span class="keyword">this</span>.GetProperty(ACCOUNT_PROP_TRADE_MODE); } <span class="macro">ENUM_ACCOUNT_STOPOUT_MODE</span> MarginSOMode(<span class="keyword">class="type">void</span>) <span class="keyword">class="kw">const</span> { <span class="keyword">class="kw">return</span> (<span class="macro">ENUM_ACCOUNT_STOPOUT_MODE</span>)<span class="keyword">this</span>.GetProperty(ACCOUNT_PROP_MARGIN_SO_MODE); } <span class="macro">ENUM_ACCOUNT_MARGIN_MODE</span> MarginMode(<span class="keyword">class="type">void</span>) <span class="keyword">class="kw">const</span> { <span class="keyword">class="kw">return</span> (<span class="macro">ENUM_ACCOUNT_MARGIN_MODE</span>)<span class="keyword">this</span>.GetProperty(ACCOUNT_PROP_MARGIN_MODE); } <span class="keyword">class="type">long</span> Login(<span class="keyword">class="type">void</span>) <span class="keyword">class="kw">const</span> { <span class="keyword">class="kw">return</span> <span class="keyword">this</span>.GetProperty(ACCOUNT_PROP_LOGIN); } <span class="keyword">class="type">long</span> Leverage(<span class="keyword">class="type">void</span>) <span class="keyword">class="kw">const</span> { <span class="keyword">class="kw">return</span> <span class="keyword">this</span>.GetProperty(ACCOUNT_PROP_LEVERAGE); }
◍ 账户属性读取的几个底层接口
在 MT5 的账户封装类里,一组 const 方法直接透传了底层账户属性,省去每次手敲 AccountInfoInteger 的麻烦。下面这段接口覆盖了挂单上限、交易开关、EA 交易权限、报价小数位、服务器类型与 FIFO 强平标志等字段。 其中 IsHedge 方法用背景高亮标出,它并非直接读属性,而是拿 MarginMode() 的返回值去比对 ACCOUNT_MARGIN_MODE_RETAIL_HEDGING 宏。若账户是零售对冲模式,该方法返回 true,否则 false——这决定了同品种多空单能否并存,外汇与贵金属账户尤需先确认这一点,因不同券商模式差异大、杠杆风险高。 开 MT5 新建脚本,把这段接口原样贴进你的账户类,编译后调用 IsHedge() 打印返回值,就能立刻看清当前账户是净头寸还是对冲账户,不用再去翻券商说明文档。
class="type">long LimitOrders(class="type">void) class="kw">const { class="kw">return this.GetProperty(ACCOUNT_PROP_LIMIT_ORDERS); } class="type">long TradeAllowed(class="type">void) class="kw">const { class="kw">return this.GetProperty(ACCOUNT_PROP_TRADE_ALLOWED); } class="type">long TradeExpert(class="type">void) class="kw">const { class="kw">return this.GetProperty(ACCOUNT_PROP_TRADE_EXPERT); } class="type">long CurrencyDigits(class="type">void) class="kw">const { class="kw">return this.GetProperty(ACCOUNT_PROP_CURRENCY_DIGITS); } class="type">long ServerType(class="type">void) class="kw">const { class="kw">return this.GetProperty(ACCOUNT_PROP_SERVER_TYPE); } class="type">long FIFOClose(class="type">void) class="kw">const { class="kw">return this.GetProperty(ACCOUNT_PROP_FIFO_CLOSE); } class="type">bool IsHedge(class="type">void) class="kw">const { class="kw">return this.MarginMode()==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING; }
库层宏定义里的交易参数与提示音映射
在 MT5 的 EA / 指标库头部,用预处理器宏把常量和提示资源固化,是降低后期维护成本的直接办法。下面这段定义覆盖了报错定位、请求截止时间、定时器频率、默认下单重试次数,以及一套标准提示音文件名。 值得注意的几个硬参数:库定时器最小频率被锁在 16 毫秒(TIMER_FREQUENCY),订单与成交采集定时器的暂停间隔是 250 毫秒(COLLECTION_ORD_PAUSE),而默认交易尝试次数 TOTAL_TRY 设为 5——这意味着若 broker 端返回重试信号,逻辑层最多补单 5 次后放弃。外汇与贵金属杠杆高,这类重试上限能避免在网络抖动时瞬间堆叠过量挂单。 报错宏 DFUN_ERR_LINE 会按终端语言切换『Page』或『Line』前缀,俄文终端显示『, Page N: 』,其他语言显示『, Line N: 』,调试时能直接定位到函数与行号。END_TIME 写成 D'31.12.3000 23:59:59',基本等于把账户历史请求的上限推到远未来,实盘里不用频繁改宏。 开 MT5 新建一个 include 头文件,把下列宏整段贴入编译,就能在自有 EA 里直接调用 SND_OK、SND_TIMEOUT 等提示音,以及用 DFUN_ERR_LINE 输出带行号的错误串。
class="macro">#define DFUN_ERR_LINE(__FUNCTION__+(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian" ? ", Page " : ", Line ")+(class="type">class="kw">string)__LINE__+": ") class="macro">#define DFUN(__FUNCTION__+": ") class=class="str">"cmt">// "Function description" class="macro">#define COUNTRY_LANG("Russian") class=class="str">"cmt">// Country language class="macro">#define END_TIME(D&class="macro">#x27;class="num">31.12.class="num">3000 class="num">23:class="num">59:class="num">59&class="macro">#x27;) class=class="str">"cmt">// End date for account history data requests class="macro">#define TIMER_FREQUENCY(class="num">16) class=class="str">"cmt">// Minimal frequency of the library timer in milliseconds class="macro">#define TOTAL_TRY(class="num">5) class=class="str">"cmt">// Default number of trading attempts class=class="str">"cmt">//--- Standard sounds class="macro">#define SND_ALERT "alert.wav" class="macro">#define SND_ALERT2 "alert2.wav" class="macro">#define SND_CONNECT "connect.wav" class="macro">#define SND_DISCONNECT "disconnect.wav" class="macro">#define SND_EMAIL "email.wav" class="macro">#define SND_EXPERT "expert.wav" class="macro">#define SND_NEWS "news.wav" class="macro">#define SND_OK "ok.wav" class="macro">#define SND_REQUEST "request.wav" class="macro">#define SND_STOPS "stops.wav" class="macro">#define SND_TICK "tick.wav" class="macro">#define SND_TIMEOUT "timeout.wav" class="macro">#define SND_WAIT "wait.wav" class=class="str">"cmt">//--- Parameters of the orders and deals collection timer class="macro">#define COLLECTION_ORD_PAUSE(class="num">250) class=class="str">"cmt">// Orders and deals collection timer pause in milliseconds
「多路采集定时器的节拍划分」
EA 后台跑采集不能只靠一个 OnTimer,把订单、账户、品种、交易类拆成独立节拍更稳。下面这组宏把五路定时器和一个历史列表的 ID 全部固化,方便后续 EventSetTimer 按 ID 分别派发。 订单与成交采集用 1000 毫秒暂停、计数器步长 16、定时器 ID 为 1;账户采集同样 1000 毫秒、步长 16、ID 为 2。两者节奏一致,但 ID 不同,回调里就能直接分流处理。 市场报价扫描分两路:定时器 1 每 100 毫秒扫一次市场观察窗口品种(ID 3),定时器 2 每 300 毫秒处理品种列表事件(ID 4)。交易类定时器也取 300 毫秒、步长 16、ID 5,和历史列表 ID 0x7779 互不冲突。 把步长统一设成 16 不是必须,只是让计数器进位整齐;若你只盯 XAUUSD 和 EURUSD,可以把 SYM_PAUSE1 调到 50 看看 MT5 终端 CPU 占用变化,外汇与贵金属波动剧烈,高频采集下断线风险会明显上升。
class="macro">#define COLLECTION_ORD_COUNTER_STEP(class="num">16) class=class="str">"cmt">// Increment of the orders and deals collection timer counter class="macro">#define COLLECTION_ORD_COUNTER_ID(class="num">1) class=class="str">"cmt">// Orders and deals collection timer counter ID class=class="str">"cmt">//--- Parameters of the account collection timer class="macro">#define COLLECTION_ACC_PAUSE(class="num">1000) class=class="str">"cmt">// Account collection timer pause in milliseconds class="macro">#define COLLECTION_ACC_COUNTER_STEP(class="num">16) class=class="str">"cmt">// Account timer counter increment class="macro">#define COLLECTION_ACC_COUNTER_ID(class="num">2) class=class="str">"cmt">// Account timer counter ID class=class="str">"cmt">//--- Symbol collection timer class="num">1 parameters class="macro">#define COLLECTION_SYM_PAUSE1(class="num">100) class=class="str">"cmt">// Pause of the symbol collection timer class="num">1 in milliseconds(for scanning market watch symbols) class="macro">#define COLLECTION_SYM_COUNTER_STEP1(class="num">16) class=class="str">"cmt">// Increment of the symbol timer class="num">1 counter class="macro">#define COLLECTION_SYM_COUNTER_ID1(class="num">3) class=class="str">"cmt">// Symbol timer class="num">1 counter ID class=class="str">"cmt">//--- Symbol collection timer class="num">2 parameters class="macro">#define COLLECTION_SYM_PAUSE2(class="num">300) class=class="str">"cmt">// Pause of the symbol collection timer class="num">2 in milliseconds(for events of the market watch symbol list) class="macro">#define COLLECTION_SYM_COUNTER_STEP2(class="num">16) class=class="str">"cmt">// Increment of the symbol timer class="num">2 counter class="macro">#define COLLECTION_SYM_COUNTER_ID2(class="num">4) class=class="str">"cmt">// Symbol timer class="num">2 counter ID class=class="str">"cmt">//--- Trading class timer parameters class="macro">#define COLLECTION_REQ_PAUSE(class="num">300) class=class="str">"cmt">// Trading class timer pause in milliseconds class="macro">#define COLLECTION_REQ_COUNTER_STEP(class="num">16) class=class="str">"cmt">// Trading class timer counter increment class="macro">#define COLLECTION_REQ_COUNTER_ID(class="num">5) class=class="str">"cmt">// Trading class timer counter ID class=class="str">"cmt">//--- Collection list IDs class="macro">#define COLLECTION_HISTORY_ID(0x7779) class=class="str">"cmt">// Historical collection list ID