开发一个跨平台网格 EA·进阶篇
(2/3)· 换经纪商不必重写策略,一套代码让网格在双平台跑起来的关键拆解
不少交易者刚在 MT5 写完网格 EA,就因为经纪商只给 MT4 账户而卡住,只能重头改代码。双平台语法差异藏在开仓函数和订单循环里,硬搬往往编译报错。本篇接着基础篇,把跨平台网格的落地细节一层层拆开。
「跨版本下单的统一封装」
EA 最核心的动作就是发单,但 MQL4 与 MQL5 的下单接口差异极大:MQL4 用 int 标记订单类型,MQL5 改用 ENUM_ORDER_TYPE 枚举,老版本里根本没有这套枚举。直接写死版本相关代码,后面迁移就是坑。 做法是在自己的工程里先定义一套私有枚举 TypeOfPos,把市价买、市价卖、四种挂单都收进去。这样上层逻辑只认 MY_BUY / MY_SELLSTOP 这类符号,底层再按编译版本翻译,EA 主体就不必关心跑在 MT4 还是 MT5。 pdxSendOrder() 把发单要用的信息一次性收进来:类型、价格、止损、止盈、手数、待改仓位的 ticket、注释和交易品种。sl/tp 不设为 0 即可,不传 symbol 就默认用 _Symbol 在当前图下发。 函数开头先规范化价格精度——取 SYMBOL_DIGITS 后用 NormalizeDouble 把 price/sl/tp 都压到经纪商小数位,否则 MT5 可能因价格尾差拒单。 #ifdef __MQL5__ 里才会出现 ENUM_TRADE_REQUEST_ACTIONS 和 MqlTradeRequest 结构,挂单与市价单靠 action 字段区分(TRADE_ACTION_PENDING 还是 TRADE_ACTION_DEAL)。MQL5 独有的成交模式参数也用条件编译隔离,MT4 编译时根本看不到这些代码。 幻数 EA_Magic 决定仓位归属:不填就接管同品种所有仓位,填了只认带该 magic 的仓。下单失败时不静默,把错误码丢给 msgErr() 弹本地化提示,下一节再拆这块。外汇与贵金属杠杆高,自动发单前务必在策略测试器用极小手数验证下单路径。
enum TypeOfPos{ MY_BUY, MY_SELL, MY_BUYSTOP, MY_BUYLIMIT, MY_SELLSTOP, MY_SELLLIMIT, }; class=class="str">"cmt">// 发送订单的函数 class="type">bool pdxSendOrder(TypeOfPos mytype, class="type">class="kw">double price, class="type">class="kw">double sl, class="type">class="kw">double tp, class="type">class="kw">double volume, class="type">ulong position=class="num">0, class="type">class="kw">string comment="", class="type">class="kw">string sym=""){ class=class="str">"cmt">// 检查传入的数值 if( !StringLen(sym) ){ sym=_Symbol; } class="type">int curDigits=(class="type">int) SymbolInfoInteger(sym, SYMBOL_DIGITS); if(sl>class="num">0){ sl=NormalizeDouble(sl,curDigits); } if(tp>class="num">0){ tp=NormalizeDouble(tp,curDigits); } if(price>class="num">0){ price=NormalizeDouble(price,curDigits); } class="macro">#ifdef __MQL5__ ENUM_TRADE_REQUEST_ACTIONS action=TRADE_ACTION_DEAL; ENUM_ORDER_TYPE type=ORDER_TYPE_BUY; class="kw">switch(mytype){ case MY_BUY: action=TRADE_ACTION_DEAL; type=ORDER_TYPE_BUY; class="kw">break; case MY_BUYSTOP: action=TRADE_ACTION_PENDING; type=ORDER_TYPE_BUY_STOP; class="kw">break; case MY_BUYLIMIT: action=TRADE_ACTION_PENDING; type=ORDER_TYPE_BUY_LIMIT; class="kw">break; case MY_SELL: action=TRADE_ACTION_DEAL; type=ORDER_TYPE_SELL; class="kw">break; case MY_SELLSTOP: action=TRADE_ACTION_PENDING; type=ORDER_TYPE_SELL_STOP; class="kw">break; case MY_SELLLIMIT: action=TRADE_ACTION_PENDING; type=ORDER_TYPE_SELL_LIMIT; class="kw">break; } class="type">MqlTradeRequest mrequest;
挂单与市价单的发送结构差异
这段 MT5 交易请求组装逻辑,把新交易框架(MqlTradeRequest)和旧 MQL4 兼容分支拆得很清楚。核心是先 ZeroMemory 清空结构体,再按 action 类型填 symbol、sl、tp、volume、price 等字段;当 action 不等于 TRADE_ACTION_SLTP 时,才允许写入挂单/市价单的价格与手数,并把 magic 固定为 EA_Magic。 成交策略由 useORDER_FILLING_RETURN 控制,FOK、RETURN、IOC 三种分别对应 ORDER_FILLING_FOK / RETURN / IOC,偏离值写死为 100 点。这个 deviation=100 在黄金跳空时段可能直接吃单失败,实盘前建议在策略测试器里改小到 10~30 验证滑点容忍度。 发送后只看 retcode:10009(已完成)或 10008(已受理)才返回 true,其余一律走 msgErr 报错。下面代码里 #else 之后是老终端的 OP_BUY / OP_BUYSTOP 等类型映射,纯兼容层,跑 MT5 时不会进这条分支。外汇与贵金属杠杆品种波动剧烈,回测通过不代表实盘安全。
class="type">MqlTradeResult mresult; ZeroMemory(mrequest); mrequest.action = action; mrequest.sl = sl; mrequest.tp = tp; mrequest.symbol = sym; if(position>class="num">0){ mrequest.position = position; } if(StringLen(comment)){ mrequest.comment=comment; } if(action!=TRADE_ACTION_SLTP){ if(price>class="num">0){ mrequest.price = price; } if(volume>class="num">0){ mrequest.volume = volume; } mrequest.type = type; mrequest.magic = EA_Magic; class="kw">switch(useORDER_FILLING_RETURN){ case FOK: mrequest.type_filling = ORDER_FILLING_FOK; class="kw">break; case RETURN: mrequest.type_filling = ORDER_FILLING_RETURN; class="kw">break; case IOC: mrequest.type_filling = ORDER_FILLING_IOC; class="kw">break; } mrequest.deviation=class="num">100; } if(OrderSend(mrequest,mresult)){ if(mresult.retcode==class="num">10009 || mresult.retcode==class="num">10008){ class="kw">return true; }else{ msgErr(GetLastError(), mresult.retcode); } } class="macro">#else class="type">int type=OP_BUY; class="kw">switch(mytype){ case MY_BUY: type=OP_BUY; class="kw">break; case MY_BUYSTOP: type=OP_BUYSTOP; class="kw">break; case MY_BUYLIMIT: type=OP_BUYLIMIT; class="kw">break; case MY_SELL: type=OP_SELL; class="kw">break; case MY_SELLSTOP: type=OP_SELLSTOP; class="kw">break; case MY_SELLLIMIT: type=OP_SELLLIMIT;
◍ 下单失败别只盯错误码
这段逻辑把 MQL5 的成交流程和报错出口收在了一处。OrderSend 返回小于 0 时直接走 msgErr(GetLastError()),正常成交则返回 true,否则函数末尾兜底返回 false。 MQL5 用枚举 TypeOfFilling 约束成交方式:FOK 表示全部或撤单,RETURN 是部分成交余量撤销,IOC 允许不完全成交。实盘里若经纪商不支持你选的填充策略,OrderSend 会直接报错,外汇与贵金属杠杆交易高风险,参数错配可能瞬时吃掉预期滑点。 msgErr 函数把整数 err 和可选 retcode 翻译成提示串:err=1 映射 langs.err1,其余走 default 拼出 'err0: 具体码';retcode>0 时再追加如 10004 的服务器回执说明,最后弹 Alert。 把这段直接贴进 MT5 的 EA 源码,改 input useORDER_FILLING_RETURN 从 FOK 切到 RETURN,能在流动性薄的黄金时段直观看到成交行为的差异。
class="kw">break; } if(OrderSend(sym, type, volume, price, class="num">100, sl, tp, comment, EA_Magic, class="num">0)<class="num">0){ msgErr(GetLastError()); }else{ class="kw">return true; } class="macro">#endif class="kw">return false; } class="macro">#ifdef __MQL5__ enum TypeOfFilling class=class="str">"cmt">//执行模式 { FOK,class=class="str">"cmt">//ORDER_FILLING_FOK RETURN,class=class="str">"cmt">// ORDER_FILLING_RETURN IOC,class=class="str">"cmt">//ORDER_FILLING_IOC }; input TypeOfFilling useORDER_FILLING_RETURN=FOK; class=class="str">"cmt">//执行模式 class="macro">#endif class="type">void msgErr(class="type">int err, class="type">int retcode=class="num">0){ class="type">class="kw">string curErr=""; class="kw">switch(err){ case class="num">1: curErr=langs.err1; class="kw">break; class=class="str">"cmt">// case N: class=class="str">"cmt">// curErr=langs.errN; class=class="str">"cmt">// class="kw">break; class="kw">default: curErr=langs.err0+": "+(class="type">class="kw">string) err; } if(retcode>class="num">0){ curErr+=" "; class="kw">switch(retcode){ case class="num">10004: curErr+=langs.retcode10004; class="kw">break; class=class="str">"cmt">// case N: class=class="str">"cmt">// curErr+=langs.retcodeN; class=class="str">"cmt">// class="kw">break; } } Alert(curErr); }
「给EA塞进双语切换的骨架」
做跨市场喊单或跟单EA时,俄语用户和英语用户同在服务器上跑是常态。把提示语写死成一种语言,后期维护就是灾难。先在输入参数层放一个语言枚举,让用户在下单前就选好界面语种,比运行时动态判断更省资源。 核心做法是建一个 struct 把所有EA内用到的字符串收拢,再用一个 init_lang() 按枚举分支填值。下面这段就是最小可编译骨架,注意 LANG 默认给了 MY_RUS,意味着不改动参数时俄文优先。 调用时机别乱放,只在 OnInit() 里调一次 init_lang() 就够了——EA每次加载只触发一次,字符串填充完驻留内存,避免每笔 tick 重复赋值拖慢执行。 实盘外汇与贵金属EA多语言化后,仍需警惕不同语种字符长度导致面板排版错位,这属于高杠杆环境下的细节风险,可能引发行情紧急时读错提示。
enum TypeOfLang{ MY_ENG, class=class="str">"cmt">// 英语 MY_RUS, class=class="str">"cmt">// 俄语 }; input TypeOfLang LANG=MY_RUS; class=class="str">"cmt">// 语言 class="kw">struct translate{ class="type">class="kw">string err1; class="type">class="kw">string err2; class=class="str">"cmt">// ... 其它字符串 }; translate langs; class="type">void init_lang(){ class="kw">switch(LANG){ case MY_ENG: langs.err1="No error, but unknown result. (class="num">1)"; langs.err2="General error(class="num">2)"; langs.err3="Incorrect parameters(class="num">3)"; class=class="str">"cmt">// ... other strings class="kw">break; case MY_RUS: langs.err0="Во время выполнения запроса произошла ошибка"; langs.err1="Нет ошибки, но результат неизвестен (class="num">1)"; langs.err2="Общая ошибка (class="num">2)"; langs.err3="Неправильные параметры (class="num">3)"; class=class="str">"cmt">// ... other strings class="kw">break; } }
网格EA的核心输入参数怎么设
给网格型EA接参数时,除了识别码和语言项,真正决定仓位结构的是四件套:单笔手数、单向挂单上限、网格步长、以及触发平仓的利润额。 以一段典型声明为例,Lot=0.01 代表每单固定微型手;maxLimits=7 表示同一方向最多铺 7 张限价单,反向也对称铺 7 张,合计最多 14 张挂单在跑。 Step=10 是网格间距,首单距现价 10 点,次单再距首单 10 点,依此类推呈等差排布。takeProfit=1 意为浮盈凑满 1 美元就清场——所有未平头寸与挂单全撤,网格重置。 这套逻辑只认盈利出场,文本里明确不处理亏损情形,因而外汇与贵金属市场里遇单边拉伸时回撤可能失控,属典型高风险结构,参数上线前建议在 MT5 策略测试器用历史数据跑一遍验证。
input class="type">class="kw">double Lot=class="num">0.01; class=class="str">"cmt">//手数大小 input class="type">uint maxLimits=class="num">7; class=class="str">"cmt">//在一个方向上的限价订单数量 input class="type">int Step=class="num">10; class=class="str">"cmt">//网格步长点数 input class="type">class="kw">double takeProfit=class="num">1; class=class="str">"cmt">//当达到指定利润时关闭交易, $
◍ EA 初始化里先把小数位和交易权限卡死
OnInit() 在 EA 首次加载时只跑一次,框架里已经先调了 init_lang() 做语言绑定,剩下要干的就两件硬活:按经纪商小数位修正网格步长,以及确认终端是否放行交易。 外汇和贵金属品种常见 5 位或 3 位小数(对应标准账户的 4 位、2 位加一个冗余位)。若 _Digits 为 5 或 3,就把用户填的 Step 乘 10,否则网格间距会缩水一个数量级。 修正后的 ST 不直接用点数,而是再乘 SymbolInfoDouble(_Symbol, SYMBOL_POINT),转成真实价格距离。这样后面挂网格时用的是价差而非点值,跨品种不用手改。 如果 MQLInfoInteger(MQL_TRADE_ALLOWED) 返回 false,说明终端禁了自动交易,立刻 Alert 用选定语言提示并 ExpertRemove() 卸载 EA,避免静默空跑。外汇贵金属杠杆高,终端权限没开就硬跑策略只会无意义消耗。 下面这段是可直接拷进 MT5 的 OnInit 终版,注意 ST 是 double 型全局变量,必须在调用前声明:
ST=Step; if(_Digits==class="num">5 || _Digits==class="num">3){ ST*=class="num">10; } ST*=SymbolInfoDouble(_Symbol, SYMBOL_POINT); if(!MQLInfoInteger(MQL_TRADE_ALLOWED)){ Alert(langs.noBuy+" ("+(class="type">class="kw">string) EA_Magic+")"); ExpertRemove(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA 交易初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { init_lang(); if(!MQLInfoInteger(MQL_TRADE_ALLOWED)){ Alert(langs.noBuy+" ("+(class="type">class="kw">string) EA_Magic+")"); ExpertRemove(); } ST=Step; if(_Digits==class="num">5 || _Digits==class="num">3){ ST*=class="num">10; } ST*=SymbolInfoDouble(_Symbol, SYMBOL_POINT); class="kw">return(INIT_SUCCEEDED); }