MQL5 交易工具包(第 1 部分):开发仓位管理 EX5 库·进阶篇
「拆开一个 .mq5 库文件的骨架」
MQL5 的库文件(.mq5)编译后生成 .ex5,但结构比 EA 简单得多,只由两块构成。顶部必须写 #property library,这条预处理指令告诉编译器“当前文件是库”,编译后相关信息会写进 ex5 的库头说明里,少了它就无法被其他程序以库形式识别。 另一块是导出函数,这是库真正干活的部件。它和普通 MQL5 函数写法基本一致,唯一区别是在函数声明末尾加 export 修饰符。编译器看到 export,会把这个函数塞进 ex5 的导出函数表;没有 export 的函数,外部程序用 #import 也看不到、调不到。 库文件不需要响应行情事件,所以里面没有 OnInit、OnDeinit、OnTick 这些标准函数,也不像脚本那样被直接运行。你新建一个库时若误写了 OnTick,编译器不会把它当事件入口,只是个普通空函数。 下面这段是最精简的导出库函数示例,只做整数相加,但结构已经完整:
class="macro">#class="kw">property library class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Example of an exported function with the class="kw">export postmodifier | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int ExportedFunction(class="type">int a, class="type">int b) class="kw">export { class="kw">return(a + b); } class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 把仓位管理收进一个可复用库
写 EA 最忌讳每套系统都把开仓、改仓、平仓逻辑重写一遍。在 MT5 里正经做法是建一个 PositionsManager.mq5,把仓位相关操作全塞进去,EA 主程序只管策略信号,交易动作统一调库。这样代码库干净,后期改风控逻辑也不用挨个 EA 翻。 MQL5 的交易请求和结果分别由 MqlTradeRequest 与 MqlTradeResult 两个预定义结构承载。因为绝大多数函数都要用,直接把它们声明成全局变量,整个库里随取随用。 错误处理是库的地基。OrderSend 之后服务器可能回 10004 重新报价、10019 资金不足、10027 客户端禁 EA,运行时还可能抛 4753 仓位找不到。下面这张表是实盘里最高频的一批返回码与应对: 代码10004 REQUOTE→Sleep几毫秒重发;10008 PLACED与10009 DONE→成功不必重发;10017 TRADE_DISABLED与10027 CLIENT_DISABLES_AT→终止发单;10012 TIMEOUT与10031 CONNECTION→暂停后重发;运行时0 ERR_SUCCESS→停发,4752 TRADE_DISABLED→停发。 ErrorAdvisor() 用 export 修饰,返回 bool:可恢复错误回 true 让调用方重发,严重或已成功回 false。内部用嵌套 switch,外层扫 tradeServerErrorCode,default 里再扫 GetLastError() 的 runtimeErrorCode,比挨个 if 判断省行数。 开仓前先调 TradingIsAllowed() 确认终端、服务器、经纪商三方都放行;被禁就直接返 false 不发单。发单前对全局 tradeRequest / tradeResult 调 ZeroMemory() 清残留,再填 type=ORDER_TYPE_BUY、action=TRADE_ACTION_DEAL、magic、symbol,sl/tp 先置 0 留待后手。外汇与贵金属杠杆高,回测通过的库在真实点差滑点下仍可能触发 10004/10019,上线前务必在策略测试器跑多品种验证。
挂单请求里的偏差与重试循环怎么写
tradeRequest.deviation 建议设为交易品种当前点差的两倍以内,平台得以在匹配价附近灵活成交,同时压住重新报价的范围。订单量 lotSize 必须按 symbol 的允许区间做规范化,否则 OrderSend 直接拒单。 打开市价单前先调 ResetLastError() 清掉旧错误码,保证后面 ErrorAdvisor() 拿到的码是干净的。随后进一个 for 循环,最多迭代 101 次(loop <= 100),首单因瞬时波动失败时能自动补试。 每次迭代都用 SymbolInfoDouble(symbol, SYMBOL_ASK) 刷新 tradeRequest.price,止盈止损按新入场价重算并规范化后写回 tradeRequest.tp / tradeRequest.sl。OrderSend() 返回 true 且 tradeResult.retcode 正常就 PrintOrderDetails("Sent OK") 并 break;返回 false 则打印错误码并从函数 return false。 修改已有仓位 SL/TP 用 SetSlTpByTicket(),入参是 positionTicket、sl 点数、tp 点数。先用 TradingIsAllowed() 把关,PositionSelectByTicket 选中后取开仓价与品种属性,买入仓和卖出仓分别做止损止盈边界校验,越界就保留原值并打印原因。 校验过的值通过 TRADE_ACTION_SLTP 请求发出,同样套 101 次重试循环,retcode 为 10008 或 10009 才认定修改成功并返回 true。平仓函数 ClosePositionByTicket() 结构一致:选仓、取属性、ZeroMemory 清结构、按买卖方向定收盘价与订单类型,弱网下靠循环把单子递出去。 编译 PositionsManager.mq5 后会在 libraries\Toolkit\ 下生成 PositionsManager.ex5,外部 EA 加 export 修饰符即可直接调。外汇与贵金属杠杆高,实盘前务必在 MT5 策略测试器用极小手数跑通这套重试逻辑。
class=class="str">"cmt">//---Global variables class=class="str">"cmt">//------------------------------- class=class="str">"cmt">//-- Trade operations request and result data structures class="type">MqlTradeRequest tradeRequest; class="type">MqlTradeResult tradeResult; class=class="str">"cmt">//------------------------------- class=class="str">"cmt">//------------------------------------------------------------------+ class=class="str">"cmt">// ErrorAdvisor(): Error analysis and processing function. | class=class="str">"cmt">// Returns true if order opening failed and order can be re-sent | class=class="str">"cmt">// Returns false if the error is critical and can not be executed | class=class="str">"cmt">//------------------------------------------------------------------+ class="type">bool ErrorAdvisor(class="type">class="kw">string callingFunc, class="type">class="kw">string symbol, class="type">int tradeServerErrorCode) class="kw">export { class=class="str">"cmt">//-- place the function body here } class=class="str">"cmt">//-- save the current runtime error code
「成交回码里藏着重试逻辑」
EA 发单后不能只看返回值就完事,交易服务器回码和运行时错误码要分开判断。下面这段 ErrorAdvisor 把两类错误拆成两层 switch,外部调用方只关心它返回 true 还是 false:true 代表可以重试,false 代表停止尝试。 服务器回码 10004 是 Requote,也就是报价已变、原价无法成交。函数里 Sleep(10) 后返回 true,让上层重新挂单;10008 和 10009 都是成功类回码(Order placed / Request completed),直接返回 false 退出。 若落到 default 分支,再查 runtimeErrorCode。码值 0 表示运行时层面没问题,ResetLastError() 清缓存后返回 false;其余未知组合则打印双码并停止尝试。外汇与贵金属杠杆高,重连重试若不加报价时效判断,可能以不利价格成交,实盘前务必在 MT5 策略测试器跑通这套分支。
class="type">bool ErrorAdvisor(class="type">class="kw">string callingFunc, class="type">class="kw">string symbol, class="type">int tradeServerErrorCode) class="kw">export { class=class="str">"cmt">//-- save the current runtime error code class="type">int runtimeErrorCode = GetLastError(); class="kw">switch(tradeServerErrorCode)class=class="str">"cmt">//-- check for trade server errors { case class="num">10004: Print(symbol, " - ", callingFunc, " ->(TradeServer_Code: ", tradeServerErrorCode, ") Requote!"); Sleep(class="num">10); class="kw">return(true); class=class="str">"cmt">//--- Exit the function and retry opening the order again case class="num">10008: Print(symbol, " - ", callingFunc, " ->(TradeServer_Code: ", tradeServerErrorCode, ") Order placed!"); class="kw">return(false); class=class="str">"cmt">//--- success - order placed ok. exit function case class="num">10009: Print(symbol, " - ", callingFunc, " ->(TradeServer_Code: ", tradeServerErrorCode, ") Request completed!"); class="kw">return(false); class=class="str">"cmt">//--- success - order placed ok. exit function case class="num">10011: Print(symbol, " - ", callingFunc, " ->(TradeServer_Code: ", tradeServerErrorCode, ") Request processing error!");
◍ 交易服务器报错码的分级休眠重试
EA 向 MT5 交易服务器发单后,不同服务器返回码对应不同的故障性质,不能用同一种节奏硬刚。下面这段处理把 10012、10015、10020、10021、10024、10031 等典型码做了分类,休眠时长从 10 毫秒到 1000 毫秒不等。
- 是请求超时取消,打印 symbol 与调用函数名后睡 100 毫秒再退出函数重试;10015 是请求价格非法,睡 10 毫秒;10020 是价格已变,同样睡 10 毫秒。10021 是无报价处理请求,网络连接类问题,睡 100 毫秒。
最该拉开间隔的是 10024——请求过于频繁,直接 Sleep(1000) 把节奏降下来,否则可能被 broker 限流。10031 无服务器连接也睡 100 毫秒后重试。 default 分支再切到运行时错误码判断:runtimeErrorCode 为 0 代表操作成功,ResetLastError 清缓存并返回 false 终止重试。外汇与贵金属杠杆高,这类自动重试逻辑若休眠设太短,可能在波动期制造大量废单,增加滑点风险。
Sleep(class="num">10); class="kw">return(true); class=class="str">"cmt">//--- Exit the function and retry opening the order again case class="num">10012: Print(symbol, " - ", callingFunc, " ->(TradeServer_Code: ", tradeServerErrorCode, ") Request canceled by timeout!"); Sleep(class="num">100); class="kw">return(true); class=class="str">"cmt">//--- Exit the function and retry opening the order again case class="num">10015: Print(symbol, " - ", callingFunc, " ->(TradeServer_Code: ", tradeServerErrorCode, ") Invalid price in the request!"); Sleep(class="num">10); class="kw">return(true); class=class="str">"cmt">//--- Exit the function and retry opening the order again case class="num">10020: Print(symbol, " - ", callingFunc, " ->(TradeServer_Code: ", tradeServerErrorCode, ") Prices changed!"); Sleep(class="num">10); class="kw">return(true); class=class="str">"cmt">//--- Exit the function and retry opening the order again case class="num">10021: Print(symbol, " - ", callingFunc, " ->(TradeServer_Code: ", tradeServerErrorCode, ") There are no quotes to process the request!"); Sleep(class="num">100); class="kw">return(true); class=class="str">"cmt">//--- Exit the function and retry opening the order again case class="num">10024: Print(symbol, " - ", callingFunc, " ->(TradeServer_Code: ", tradeServerErrorCode, ") Too frequent requests!"); Sleep(class="num">1000); class="kw">return(true); class=class="str">"cmt">//--- Exit the function and retry opening the order again case class="num">10031: Print(symbol, " - ", callingFunc, " ->(TradeServer_Code: ", tradeServerErrorCode, ") No connection with the trade server!"); Sleep(class="num">100); class="kw">return(true); class=class="str">"cmt">//--- Exit the function and retry opening the order again class="kw">default: class="kw">switch(runtimeErrorCode)class=class="str">"cmt">//-- check for runtime errors case class="num">0: Print(symbol, " - ", callingFunc, " ->(Runtime_Code: ", runtimeErrorCode, ") The operation completed successfully!"); ResetLastError(); class=class="str">"cmt">//--- reset error cache class="kw">return(false); class=class="str">"cmt">//--- Exit the function and stop trying to open order
EA 报错分支与自动交易闸门
上面这段 switch 把交易函数跑完后的运行时错误码做了细分拦截:4752 代表券商禁用了 EA 交易,4753 是查不到持仓,4754 找不到挂单,4755 查无成交记录,剩下的一律进 default 打印交易服务器返回码和运行时码再退出。每个分支都先 ResetLastError() 清掉错误缓存,然后 return(false) 终止后续开仓尝试,避免同一个错误被反复触发。 default 分支里用 \r\n 换行把「Trade Server RetCode」和「Runtime Error Code」分开打印,方便在专家日志里直接对照券商回码与本地运行时码定位问题。注意 4752 这种禁交易错误往往发生在服务器维护或账户权限变更时,外汇与贵金属杠杆品种在高波动时段更容易被临时限制 EA 下单,概率上不算低。 紧随其后的 TradingIsAllowed() 是另一道闸门:它同时检查 IsStopped() 取反、MQL_TRADE_ALLOWED 与 TERMINAL_TRADE_ALLOWED 三个条件,只有全部为真才允许交易。把这个函数放在每笔下单前调用,能挡掉大部分「EA 其实已被终端禁交易却还在硬发订单」的无效报错。开 MT5 把这两段塞进你的订单管理模块,跑一晚模拟盘就能看到 4752~4755 各自在哪种断点冒出来。
case class="num">4752: Print(symbol, " - ", callingFunc, " ->(Runtime_Code: ", runtimeErrorCode, ") Trading by Expert Advisors prohibited!"); ResetLastError(); class=class="str">"cmt">//--- reset error cache class="kw">return(false); class=class="str">"cmt">//--- Exit the function and stop trying to open order case class="num">4753: Print(symbol, " - ", callingFunc, " ->(Runtime_Code: ", runtimeErrorCode, ") Position not found!"); ResetLastError(); class=class="str">"cmt">//--- reset error cache class="kw">return(false); class=class="str">"cmt">//--- Exit the function and stop trying to open order case class="num">4754: Print(symbol, " - ", callingFunc, " ->(Runtime_Code: ", runtimeErrorCode, ") Order not found!"); ResetLastError(); class=class="str">"cmt">//--- reset error cache class="kw">return(false); class=class="str">"cmt">//--- Exit the function and stop trying to open order case class="num">4755: Print(symbol, " - ", callingFunc, " ->(Runtime_Code: ", runtimeErrorCode, ") Deal not found!"); ResetLastError(); class=class="str">"cmt">//--- reset error cache class="kw">return(false); class=class="str">"cmt">//--- Exit the function and stop trying to open order class="kw">default: class=class="str">"cmt">//--- All other error codes Print(symbol, " - ", callingFunc, " *OTHER* Error occurred \r\nTrade Server RetCode: ", tradeServerErrorCode, ", Runtime Error Code = ", runtimeErrorCode); ResetLastError(); class=class="str">"cmt">//--- reset error cache class="kw">return(false); class=class="str">"cmt">//--- Exit the function and stop trying to open order break; } } } class=class="str">"cmt">//+-----------------------------------------------------------------------+ class=class="str">"cmt">//| TradingIsAllowed() verifies whether auto-trading is currently allowed | | class=class="str">"cmt">//+-----------------------------------------------------------------------+ class="type">bool TradingIsAllowed() class="kw">export { if( !IsStopped() && MQLInfoInteger(MQL_TRADE_ALLOWED) && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) &&