开发一个跨平台网格 EA·综合运用
◍ 给图表装一个一键清仓的入口
EA 好不好用,不只看策略逻辑,还看你能不能在图表上直接看清当前仓位和利润,并随手关掉所有单子。我们在图表右上角放一个 Close All 按钮,点一下就平掉本 EA 在当前品种上的全部头寸与挂单,省去手动逐个关闭的麻烦。 所有图形对象都统一加前缀 grider_,避免和手动画线或其他 EA 的对象重名。MQL5 里用 PositionsTotal() 枚举持仓,MQL4 里用 OrdersTotal() 配合 OrderSelect(),所以这段代码用 #ifdef __MQL5__ 做条件编译,同一套函数名在两套环境里都能跑。 getmeinfo_btn() 负责统计:posPlus 累加买入仓位手数,posMinus 累加卖出仓位手数,profit 把每单的浮盈加隔夜利息(MQL5)或佣金(MQL4 分支)都算进去。只要 positionExist 被置为 true,图表上就会显示按钮和汇总数据;没有持仓时按钮不出现。 点击响应写在 OnChartEvent() 里,捕捉到按钮的 chart event 后调用 CloseAllPos()。这个函数本体在下一篇才实现,本篇只接好触发链路。另外 EA 退出时由 OnDeInit() 删除带 grider_ 前缀的对象,防止图表残留。 外汇和贵金属杠杆高,一键全平可能在大滑点行情下成交价偏离预期,实盘前务必在 MT5 策略测试器用历史数据跑一遍按钮逻辑。
class="type">class="kw">string prefix_graph="grider_"; class="type">void getmeinfo_btn(class="type">class="kw">string symname){ class="type">class="kw">double posPlus=class="num">0; class="type">class="kw">double posMinus=class="num">0; class="type">class="kw">double profit=class="num">0; class="type">class="kw">double positionExist=false; class=class="str">"cmt">// 对开启的买入和卖出仓位计数, class=class="str">"cmt">// 并计算它们的总利润 class="macro">#ifdef __MQL5__ class="type">int cntMyPos=PositionsTotal(); for(class="type">int ti=cntMyPos-class="num">1; ti>=class="num">0; ti--){ if(PositionGetSymbol(ti)!=symname) class="kw">continue; if(EA_Magic>class="num">0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) class="kw">continue; positionExist=true; profit+=PositionGetDouble(POSITION_PROFIT); profit+=PositionGetDouble(POSITION_SWAP); if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY){ posPlus+=PositionGetDouble(POSITION_VOLUME); }else{ posMinus+=PositionGetDouble(POSITION_VOLUME); } } class="macro">#else class="type">int cntMyPos=OrdersTotal(); if(cntMyPos>class="num">0){ for(class="type">int ti=cntMyPos-class="num">1; ti>=class="num">0; ti--){ if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) class="kw">continue; if( OrderType()==OP_BUY || OrderType()==OP_SELL ){}else{ class="kw">continue; } if(OrderSymbol()!=symname) class="kw">continue; if(EA_Magic>class="num">0 && OrderMagicNumber()!=EA_Magic) class="kw">continue; positionExist=true; profit+=OrderCommission();
「一键平仓按钮的盈亏与仓位回显」
这段逻辑干的事很直接:遍历当前图表挂单与持仓,把每笔的浮动盈利和隔夜利息累加到 profit,再按买卖方向分别统计手数到 posPlus(多)与 posMinus(空)。 当检测到有持仓时,脚本在图表右上角偏移 233 像素处生成一个名为 prefix_graph+"delall" 的按钮,标题格式为「关闭全部 (盈亏两位小数) L: 多单手数 S: 空单手数」。例如当前多 0.50 空 0.20、浮盈 12.34,按钮就显示 closeAll (12.34) L: 0.5 S: 0.2。 若无持仓但按钮仍存在,则用 ObjectFind 判断后 ObjectDelete 清掉,避免界面残留。最后 ChartRedraw(0) 强制重绘,让变化立刻可见。 createObject 的入参是按钮名、横向权重值(此处传 233)和标题文本;函数内先算图表宽度减 87 像素作为右边距,再两轮循环扫描带 prefix_graph 前缀的对象,取最大 YDISTANCE 做纵向避让,保证新按钮不压住已有的同类控件。外汇与贵金属杠杆高,这类一键平仓在剧烈滑点行情中可能以偏离预期的价格成交,实盘前务必在策略测试器里跑通。
profit+=OrderProfit(); profit+=OrderSwap(); if(OrderType()==OP_BUY){ posPlus+=OrderLots(); }else{ posMinus+=OrderLots(); } class=class="str">"cmt">// 有开启的仓位 class=class="str">"cmt">// 增加关闭按钮 if(positionExist){ createObject(prefix_graph+"delall", class="num">233, langs.closeAll+" ("+DoubleToString(profit, class="num">2)+") L: "+(class="type">class="kw">string) posPlus+" S: "+(class="type">class="kw">string) posMinus); }else{ class=class="str">"cmt">// otherwise, class="kw">delete the button for closing positions if(ObjectFind(class="num">0, prefix_graph+"delall")>class="num">0){ ObjectDelete(class="num">0, prefix_graph+"delall"); } } class=class="str">"cmt">// 更新当前图表 class=class="str">"cmt">// 显示所实现的变化 ChartRedraw(class="num">0); } class="type">void createObject(class="type">class="kw">string name, class="type">int weight, class="type">class="kw">string title){ class=class="str">"cmt">// 如果图表上没有 &class="macro">#x27;name&class="macro">#x27; 按钮,就创建它 if(ObjectFind(class="num">0, name)<class="num">0){ class=class="str">"cmt">// 定义相对于要显示按钮的图表右上角的偏移 class="type">long offset= ChartGetInteger(class="num">0, CHART_WIDTH_IN_PIXELS)-class="num">87; class="type">long offsetY=class="num">0; for(class="type">int ti=class="num">0; ti<ObjectsTotal((class="type">long) class="num">0); ti++){ class="type">class="kw">string objName= ObjectName(class="num">0, ti); if( StringFind(objName, prefix_graph)<class="num">0 ){ class="kw">continue; } class="type">long tmpOffset=ObjectGetInteger(class="num">0, objName, OBJPROP_YDISTANCE); if( tmpOffset>offsetY){ offsetY=tmpOffset; } } for(class="type">int ti=class="num">0; ti<ObjectsTotal((class="type">long) class="num">0); ti++){ class="type">class="kw">string objName= ObjectName(class="num">0, ti); if( StringFind(objName, prefix_graph)<class="num">0 ){ class="kw">continue; } class="type">long tmpOffset=ObjectGetInteger(class="num">0, objName, OBJPROP_YDISTANCE); if( tmpOffset!=offsetY ){ class="kw">continue; } }
按钮排布与一键平仓的事件绑定
这段逻辑解决的是面板按钮在图表上的自动换行与点击响应。先遍历已有对象取最小水平间距 tmpOffset,若落在 0 到当前 offset 之间就刷新 offset,随后 offset 减去按钮宽度 weight+1 像素;当 offset 算出来小于 0,就回退到图表宽度减 87 像素处,并把 offsetY 下移 25 像素重新排,保证按钮不溢出右边界。 创建按钮时用 ObjectCreate 指定 OBJ_BUTTON,依次写死 XDISTANCE/YDISTANCE 定位、OBJPROP_TEXT 标题、XSIZE=weight、FONTSIZE=8、YSIZE=25,背景色 clrLightGray、文字 clrBlack,最后 ChartRedraw 让 MT5 立即重绘。若对象已存在则只改 OBJPROP_TEXT,避免重复建按钮。 交互层挂在 OnChartEvent 的 CHARTEVENT_OBJECT_CLICK 上:当 sparam 等于 prefix_graph+"delall" 时调用 closeAllPos() 平掉所有持仓。外汇与贵金属杠杆高,这类一键平仓在滑点行情中可能瞬间放大回撤,实盘前务必在策略测试器用历史数据验证点击逻辑。 析构用 OnDeinit 里的 ObjectsDeleteAll(0, prefix_graph) 按前缀清场,切换周期或移除 EA 时不留垃圾对象。
tmpOffset=ObjectGetInteger(class="num">0, objName, OBJPROP_XDISTANCE); if( tmpOffset>class="num">0 && tmpOffset<offset){ offset=tmpOffset; } } offset-=(weight+class="num">1); if(offset<class="num">0){ offset=ChartGetInteger(class="num">0, CHART_WIDTH_IN_PIXELS)-class="num">87; offsetY+=class="num">25; offset-=(weight+class="num">1); } ObjectCreate(class="num">0, name, OBJ_BUTTON, class="num">0, class="num">0, class="num">0); ObjectSetInteger(class="num">0,name,OBJPROP_XDISTANCE,offset); ObjectSetInteger(class="num">0,name,OBJPROP_YDISTANCE,offsetY); ObjectSetString(class="num">0,name,OBJPROP_TEXT, title); ObjectSetInteger(class="num">0,name,OBJPROP_XSIZE,weight); ObjectSetInteger(class="num">0,name,OBJPROP_FONTSIZE, class="num">8); ObjectSetInteger(class="num">0,name,OBJPROP_COLOR, clrBlack); ObjectSetInteger(class="num">0,name,OBJPROP_YSIZE,class="num">25); ObjectSetInteger(class="num">0,name,OBJPROP_BGCOLOR, clrLightGray); ChartRedraw(class="num">0); }else{ ObjectSetString(class="num">0,name,OBJPROP_TEXT, title); } } class="type">void OnChartEvent(const class="type">int id, class=class="str">"cmt">// 事件 ID const class="type">long& lparam, class=class="str">"cmt">// 长整型的事件参数 const class="type">class="kw">double& dparam, class=class="str">"cmt">// 双精度浮点型的事件参数 const class="type">class="kw">string& sparam) class=class="str">"cmt">// 字符串类型的事件参数 { class="type">class="kw">string text=""; class="kw">switch(id){ case CHARTEVENT_OBJECT_CLICK: class=class="str">"cmt">// 如果按钮名称是 prefix_graph+"delall", 则 if (sparam==prefix_graph+"delall"){ closeAllPos(); } class="kw">break; } } class="type">void OnDeinit(const class="type">int reason) { ObjectsDeleteAll(class="num">0, prefix_graph); }
◍ 用反向单对锁平仓省点差
EA 里一键清场不能只会无脑删仓。若同一品种同时持有一手多和一手空,MQL5 下走 TRADE_ACTION_CLOSE_BY 让经纪商以反向仓位抵消,能把开双边时付的两道点差收回来一部分,账户净值倾向比普通市价平仓略多一截。 closeByPos() 先扫一遍持仓,对符号和魔术码(EA_Magic)做过滤,只在手数完全相等的 BUY 与 SELL 之间发起 close by。若成交量不同则跳过,留给后续常规平仓处理。 函数发单成功后置 repeatOpen=true 并自我递归:因为持仓量可能不等,一次抵消只减不掉完,重跑才能把残余对锁清干净。外汇和贵金属杠杆高,这类对冲清仓能否省点差完全看经纪商是否支持该指令,实盘前务必在 MT5 策略测试器用真实点差验证。 下面这段是 MQL5 头文件和 closeByPos 前半段,CTrade 对象已在 __MQL5__ 宏下挂好: #ifdef __MQL5__ // 仅 MQL5 环境编译 #include <Trade\Trade.mqh> // 引入交易类 CTrade Trade; // 创建交易对象 #endif void closeByPos(){ // 定义反向平仓函数 bool repeatOpen=false; // 是否需要重跑标记 #ifdef __MQL5__ int cntMyPos=PositionsTotal(); // 当前持仓总数 for(int ti=cntMyPos-1; ti>=0; ti--){ // 逆序遍历持仓 if(PositionGetSymbol(ti)!=_Symbol) continue; // 非当前品种跳过 if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue; // 魔术码不符跳过 if( PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY ){ // 找到多单 long closefirst=PositionGetInteger(POSITION_TICKET); // 多单 ticket double closeLots=PositionGetDouble(POSITION_VOLUME); // 多单手数 for(int ti2=cntMyPos-1; ti2>=0; ti2--){ // 再扫一遍找空单 if(PositionGetSymbol(ti2)!=_Symbol) continue; // 非本品种跳过 if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue; // 魔术码不符跳过 if( PositionGetInteger(POSITION_TYPE)!=POSITION_TYPE_SELL ) continue; // 不是空单跳过 if( PositionGetDouble(POSITION_VOLUME)!=closeLots ) continue; // 手数不等跳过 MqlTradeRequest request; // 交易请求结构 MqlTradeResult result; // 交易结果结构 ZeroMemory(request); // 清空请求 ZeroMemory(result); // 清空结果 request.action=TRADE_ACTION_CLOSE_BY; // 反向平仓动作 request.position=closefirst; // 被平多单 request.position_by=PositionGetInteger(POSITION_TICKET); // 用来抵消的空单 if(EA_Magic>0) request.magic=EA_Magic; // 带魔术码 if(OrderSend(request,result)){ // 发单成功 repeatOpen=true; // 标记重跑 break; // 跳出空单循环 } }
class="macro">#ifdef __MQL5__ class="macro">#include <Trade\Trade.mqh> CTrade Trade; class="macro">#endif class="type">void closeByPos(){ class="type">bool repeatOpen=false; class="macro">#ifdef __MQL5__ class="type">int cntMyPos=PositionsTotal(); for(class="type">int ti=cntMyPos-class="num">1; ti>=class="num">0; ti--){ if(PositionGetSymbol(ti)!=_Symbol) class="kw">continue; if(EA_Magic>class="num">0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) class="kw">continue; if( PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY ){ class="type">long closefirst=PositionGetInteger(POSITION_TICKET); class="type">class="kw">double closeLots=PositionGetDouble(POSITION_VOLUME); for(class="type">int ti2=cntMyPos-class="num">1; ti2>=class="num">0; ti2--){ if(PositionGetSymbol(ti2)!=_Symbol) class="kw">continue; if(EA_Magic>class="num">0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) class="kw">continue; if( PositionGetInteger(POSITION_TYPE)!=POSITION_TYPE_SELL ) class="kw">continue; if( PositionGetDouble(POSITION_VOLUME)!=closeLots ) class="kw">continue; class="type">MqlTradeRequest request; class="type">MqlTradeResult result; ZeroMemory(request); ZeroMemory(result); request.action=TRADE_ACTION_CLOSE_BY; request.position=closefirst; request.position_by=PositionGetInteger(POSITION_TICKET); if(EA_Magic>class="num">0) request.magic=EA_Magic; if(OrderSend(request,result)){ repeatOpen=true; class="kw">break; } }
「用 OrderCloseBy 对冲反手仓位」
这段逻辑跑在 MQL5 的 #else 分支里,专门针对老式持仓遍历方式做反手平仓。它先取 OrdersTotal() 拿到当前持仓数,再从尾到头扫一遍,只挑本符号且 Magic 匹配的订单,避免误伤其他 EA 的仓位。
核心动作是:找到一个 BUY 单后,再嵌套一轮扫描去找同符号、同 Magic 且手数不小于它的 SELL 单,然后调用 OrderCloseBy(closefirst, OrderTicket()) 用反向单直接抵销。外汇与贵金属杠杆高,这种抵销若手数匹配不当,可能留下残仓继续暴露风险。
只要内层抵销成功,repeatOpen 置真并 break,外层回到 closeByPos() 递归再跑一遍,直到没有可配对的反向仓。你可以在 MT5 策略测试器里把 EA_Magic 设成固定值,挂一多一空看 repeatOpen 的触发次数来验证配对效率。
class="macro">#else class="type">int cntMyPos=OrdersTotal(); if(cntMyPos>class="num">0){ for(class="type">int ti=cntMyPos-class="num">1; ti>=class="num">0; ti--){ if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) class="kw">continue; if( OrderSymbol()!=_Symbol ) class="kw">continue; if(EA_Magic>class="num">0 && OrderMagicNumber()!=EA_Magic) class="kw">continue; if( OrderType()==OP_BUY ){ class="type">int closefirst=OrderTicket(); class="type">class="kw">double closeLots=OrderLots(); for(class="type">int ti2=cntMyPos-class="num">1; ti2>=class="num">0; ti2--){ if(OrderSelect(ti2,SELECT_BY_POS,MODE_TRADES)==false) class="kw">continue; if( OrderSymbol()!=_Symbol ) class="kw">continue; if(EA_Magic>class="num">0 && OrderMagicNumber()!=EA_Magic) class="kw">continue; if( OrderType()!=OP_SELL ) class="kw">continue; if( OrderLots()<closeLots ) class="kw">continue; if( OrderCloseBy(closefirst, OrderTicket()) ){ repeatOpen=true; class="kw">break; } } if(repeatOpen){ class="kw">break; } } } } class="macro">#endif class=class="str">"cmt">// 如果我们根据反向仓位关闭了仓位 class=class="str">"cmt">// 再次运行 closeByPos 函数 if(repeatOpen){ closeByPos(); } } class="type">void closeAllPos(){ closeByPos(); class="macro">#ifdef __MQL5__
MT4与MT5双版本的平仓撤单写法
EA在出场环节必须兼容两套终端接口:MT5用PositionsTotal/OrdersTotal遍历,MT4则靠OrdersTotal加OrderSelect按位选取。两者都靠EA_Magic过滤自身订单,避免误动手动单或其他EA的单子。 MT5分支先清持仓再删挂单:从后往前循环,PositionGetSymbol不匹配_Symbol就continue, magic不对也跳过,命中后Trade.PositionClose按ticket平掉;挂单侧用OrderGetTicket取ticket,同样过滤符号与magic,再Trade.OrderDelete撤单。 MT4分支没有持仓/挂单分离概念,OrdersTotal>0时逆序OrderSelect,OrderSymbol和OrderMagicNumber双重过滤。买单用SymbolInfoTick取bid、OrderClose按原手数平(滑点给100点);卖单逻辑对称,先取tick再Close。外汇与贵金属杠杆高,这类全平代码若magic设错可能一次清掉无关仓位,上MT5前先用策略测试器跑一遍。
class="type">int cntMyPos=PositionsTotal(); for(class="type">int ti=cntMyPos-class="num">1; ti>=class="num">0; ti--){ if(PositionGetSymbol(ti)!=_Symbol) class="kw">continue; if(EA_Magic>class="num">0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) class="kw">continue; Trade.PositionClose(PositionGetInteger(POSITION_TICKET)); } class="type">int cntMyPosO=OrdersTotal(); for(class="type">int ti=cntMyPosO-class="num">1; ti>=class="num">0; ti--){ class="type">ulong orderTicket=OrderGetTicket(ti); if(OrderGetString(ORDER_SYMBOL)!=_Symbol) class="kw">continue; if(EA_Magic>class="num">0 && OrderGetInteger(ORDER_MAGIC)!=EA_Magic) class="kw">continue; Trade.OrderDelete(orderTicket); } class="macro">#else class="type">int cntMyPos=OrdersTotal(); if(cntMyPos>class="num">0){ for(class="type">int ti=cntMyPos-class="num">1; ti>=class="num">0; ti--){ if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) class="kw">continue; if( OrderSymbol()!=_Symbol ) class="kw">continue; if(EA_Magic>class="num">0 && OrderMagicNumber()!=EA_Magic) class="kw">continue; if( OrderType()==OP_BUY ){ class="type">MqlTick latest_price; if(!SymbolInfoTick(OrderSymbol(),latest_price)){ Alert(GetLastError()); class="kw">return; } if(!OrderClose(OrderTicket(), OrderLots(),latest_price.bid,class="num">100)){ } }else if(OrderType()==OP_SELL){ class="type">MqlTick latest_price; if(!SymbolInfoTick(OrderSymbol(),latest_price)){ Alert(GetLastError()); class="kw">return; }
◍ 平仓与挂单的清理逻辑
这段片段处理的是持仓和平仓后的界面清理:对市价单调用 OrderClose 按当前 ask 平仓,滑点给到 100 点;若单子是挂单则走 OrderDelete 直接删掉,失败时不额外报错只留空块。 OrderClose 的第三个参数用了 latest_price.ask,意味着只处理了多单平仓路径,空单若复用同一段逻辑会以买价平仓,实盘里可能吃掉额外点差。外汇与贵金属杠杆高,这种硬编码价格方向在剧烈行情中可能触发非预期成交。 结尾的 ObjectFind 检查名为 delall 的图形对象,存在就 ObjectDelete 清掉——这是把‘一键清仓’按钮从图表移除的动作,通常放在循环退出后执行,避免每帧重复查找。 把滑点参数 100 改成经纪商实际允许值再去 MT5 回测,能看出成交率差异;若做空较多,需补一个 bid 分支而非直接套 ask。
if(!OrderClose(OrderTicket(), OrderLots(),latest_price.ask,class="num">100)){ } }else{ if(!OrderDelete(OrderTicket())){ } } } } class="macro">#endif class=class="str">"cmt">// 删除关闭仓位的按钮 if(ObjectFind(class="num">0, prefix_graph+"delall")>class="num">0){ ObjectDelete(class="num">0, prefix_graph+"delall"); }
「把下单逻辑塞进 OnTick 的骨架」
EA 的主循环落在 OnTick(),但每个 tick 都跑网格检查纯属浪费。实际做法是在函数头部加一根「新柱判断」栅栏:不是 M5 新柱的首 tick 就直接 return,相当于把检查频率压到每 5 分钟一次,EA 也必须挂在 M5 周期上跑。 新柱判定靠 pdxIsNewBar():用静态变量存上根柱时间,CopyTime 取当前柱时间,不一致就刷新并返回 true。这段逻辑本身不依赖任何外部指标,复制进 MT5 能立刻编译。 进网格之前有两道前置:先 checkTakeProfit() 把同品种、同 magic 的持仓盈亏加总(含 swap/commission),超过 takeProfit 输入值就调 closeAllPos() 全平;再补一句 Close All 按钮的显示调用,否则前面写的面板控件永远是隐身。 最后才轮到网格本体——existLimits() 返回 true 就不动,返回 false 才 initLimits() 铺新单。这样 OnTick 的分工就清晰了:拦 tick、查利润、亮按钮、补网格,四步顺序不能乱。外汇与贵金属杠杆高,网格马丁类逻辑回撤可能超出本金,上模拟盘验证前别挂实盘。
if( !pdxIsNewBar() ){ class="kw">return; } class="type">bool pdxIsNewBar(){ class="kw">static class="type">class="kw">datetime Old_Time; class="type">class="kw">datetime New_Time[class="num">1]; if(CopyTime(_Symbol,_Period,class="num">0,class="num">1,New_Time)>class="num">0){ if(Old_Time!=New_Time[class="num">0]){ Old_Time=New_Time[class="num">0]; class="kw">return true; } } class="kw">return false; } if(checkTakeProfit()){ closeAllPos(); } class="type">bool checkTakeProfit(){ if( takeProfit<=class="num">0 ) class="kw">return false; class="type">class="kw">double curProfit=class="num">0; class="type">class="kw">double profit=class="num">0; class="macro">#ifdef __MQL5__ class="type">int cntMyPos=PositionsTotal(); for(class="type">int ti=cntMyPos-class="num">1; ti>=class="num">0; ti--){ if(PositionGetSymbol(ti)!=_Symbol) class="kw">continue; if(EA_Magic>class="num">0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) class="kw">continue; profit+=PositionGetDouble(POSITION_PROFIT); profit+=PositionGetDouble(POSITION_SWAP); } class="macro">#else class="type">int cntMyPos=OrdersTotal(); if(cntMyPos>class="num">0){ for(class="type">int ti=cntMyPos-class="num">1; ti>=class="num">0; ti--){ if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) class="kw">continue; if( OrderType()==OP_BUY || OrderType()==OP_SELL ){}else{ class="kw">continue; } if(OrderSymbol()!=_Symbol) class="kw">continue; if(EA_Magic>class="num">0 && OrderMagicNumber()!=EA_Magic) class="kw">continue; profit+=OrderCommission(); profit+=OrderProfit(); profit+=OrderSwap(); } } class="macro">#endif if(profit>takeProfit){ class="kw">return true; }
仓位存在性判定与网格初始化
网格 EA 启动后第一件事不是直接挂单,而是先确认当前品种是否已有本魔术码(EA_Magic)的未结仓位或挂单。existLimits() 函数就是干这个的:MQL5 环境下分别遍历 PositionsTotal() 与 OrdersTotal(),用 PositionGetSymbol / OrderGetString 比对 _Symbol,再用 PositionGetInteger(POSITION_MAGIC) / OrderGetInteger(ORDER_MAGIC) 过滤魔术码,命中即返回 true。 如果 existLimits() 返回 false,才进入 initLimits() 铺设网格。这里有个容易被忽略的护栏:先用 SymbolInfoTick 取 lastme.bid,若 bid 为 0 直接 return,避免在没有有效报价时瞎挂单。 minStop 的计算很关键——它等于 SYMBOL_POINT 乘以 SYMBOL_TRADE_STOPS_LEVEL,也就是经纪商要求的止损/挂单最小距离。循环中 curPrice 从 bid 起每次加 ST(步长),若 curPrice - ask 小于 minStop 就 continue 跳过,防止订单因距离不足被服务器拒掉。外汇与贵金属杠杆高,这类距离限制随品种波动,实盘前务必在 MT5 用具体品种核对 SYMBOL_TRADE_STOPS_LEVEL 数值。
class="type">bool existLimits(){ class="macro">#ifdef __MQL5__ class="type">int cntMyPos=PositionsTotal(); for(class="type">int ti=cntMyPos-class="num">1; ti>=class="num">0; ti--){ if(PositionGetSymbol(ti)!=_Symbol) class="kw">continue; if(EA_Magic>class="num">0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) class="kw">continue; class="kw">return true; } class="type">int cntMyPosO=OrdersTotal(); for(class="type">int ti=cntMyPosO-class="num">1; ti>=class="num">0; ti--){ class="type">ulong orderTicket=OrderGetTicket(ti); if(OrderGetString(ORDER_SYMBOL)!=_Symbol) class="kw">continue; if(EA_Magic>class="num">0 && OrderGetInteger(ORDER_MAGIC)!=EA_Magic) class="kw">continue; class="kw">return true; } class="macro">#else class="type">int cntMyPos=OrdersTotal(); if(cntMyPos>class="num">0){ for(class="type">int ti=cntMyPos-class="num">1; ti>=class="num">0; ti--){ if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) class="kw">continue; if(OrderSymbol()!=_Symbol) class="kw">continue; if(EA_Magic>class="num">0 && OrderMagicNumber()!=EA_Magic) class="kw">continue; class="kw">return true; } } class="macro">#endif class="kw">return false; } class="type">void initLimits(){ class="type">class="kw">double curPrice; class="type">MqlTick lastme; SymbolInfoTick(_Symbol, lastme); if( lastme.bid==class="num">0 ){ class="kw">return; } class="type">class="kw">double minStop=SymbolInfoDouble(_Symbol, SYMBOL_POINT)*SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL); curPrice=lastme.bid; for(class="type">uint i=class="num">0; i<maxLimits; i++){ curPrice+=ST; if( curPrice-lastme.ask < minStop ) class="kw">continue;
◍ 挂单网格的距离与止损校验
这段逻辑在循环里批量铺设卖出止损单,核心是先取当前 ask 价,再按固定步长 ST 逐格下移挂价。每下一格都先减 ST,避免所有卖单挤在同一价位。 下单价与实时 bid 的落差若小于 minStop,直接 continue 跳过,防止触发经纪商最小止损距离限制导致报错。外汇与贵金属杠杆高,这种距离校验在剧烈波动时可能救你于大量 reject 订单之外。 pdxSendOrder 封装了 MY_SELLSTOP 类型、手数 Lot 与当前品种 _Symbol,返回失败也不在此处处理,调用方需自行补日志或重试。开 MT5 把 ST 和 minStop 打印出来比对点差,能立刻看出网格在哪些价位会失效。
class=class="str">"cmt">// 设置卖出订单 curPrice=lastme.ask; for(class="type">uint i=class="num">0; i<maxLimits; i++){ curPrice-=ST; if( lastme.bid-curPrice < minStop ) class="kw">continue; if(!pdxSendOrder(MY_SELLSTOP, curPrice, class="num">0, class="num">0, Lot, class="num">0, "", _Symbol)){ } } }
「用 MT5 回测网格 EA 的真实表现」
EA 写完后先不做优化,直接在 MT5 跑一轮基础测试。选 EURUSD、M5 周期、2018-08-01 到 2019-01-01,测试模式用 1 Minute OHLC,参数全保持默认:0.01 手、步长 10 点、每网格 7 单、获利 $1。 前几个月曲线平稳,累计盈利接近 $100,回撤控制在 $30 左右。转折出现在 2018-09-13 16:15 之后:价格先触发 1 个买单,接着激活 2 个卖单和 2 个买单,最后还留了 5 个卖单挂著。到 9 月 20 日价格拉回高位,剩余 4 个买单全部成交,账户变成 7 买 7 卖,多空完全锁死,获利功能失效。 后续价格再涨约 80 点——如果网格容量是 13 单,大概率能解套反赚;若放到 30 单,价格后来跌 200 点也理论上可盈利,但回撤会极其恐怖,且可能耗数月。把网格单数提到 13 和 20 验证:20 单时回撤约 $300,总利润仅略超 $100,策略没废但急需改。 顺手做次优化:网格单数 4–21、步长 10–20 点、获利不变 $1。结果步长 13 点、16 单最优;在「基于真实分时的每一分时」模式下 5 个月赚 $119、回撤 $221。外汇与贵金属网格策略属高风险,这种回撤利润比说明实盘前必须重构出场逻辑。
用亏损阈值强制换网格
双向挂单都被扫、浮亏持续放大时,死扛可能让回撤拖上数月。除了人工干预和叠加新网格,更硬的手段是给当前网格设一个美元亏损上限,触线就清掉全部持仓与挂单、重开一套网格——相当于认输一仗但保住本金。 具体落地只需在 EA 输入参数里加一个 takeLoss:填负数表示允许的最大亏损金额(例如 -500 即浮亏达到 500 美元就清盘)。原 checkTakeProfit() 只返回 true/false,现在改成返回所有同魔法码、同品种持仓的净盈亏合计(含 swap/库存费),MQL5 下用 PositionsTotal 遍历,MQL4 下退化为 OrdersTotal 兼容。 ontick() 里除了检查获利,再加一段:当 checkTakeProfit() 小于 takeLoss 时调用 closeAllPos() 平掉整网格。外汇与贵金属杠杆高,这种止损换网格能截断极端行情下的深套,但也可能在震荡市频繁重置、吃掉手续费,实盘前务必在 MT5 策略测试器用历史数据跑一遍确认触发频率。 下面这段是 MQL5 侧核心改动,黄底为变更点,直接复制到你的 EA 源码对照即可:
<span class="keyword">input</span> <span class="keyword">class="type">class="kw">double</span> takeLoss=<span class="number">class="num">0</span>; <span class="comment">class=class="str">"cmt">//如有亏损则平仓, $</span> <span class="keyword" style="background-class="type">class="kw">color:rgb(class="num">255, class="num">221, class="num">0);">class="type">class="kw">double</span> checkTakeProfit(){ <span class="keyword">class="type">class="kw">double</span> curProfit=<span class="number">class="num">0</span>; <span class="keyword">class="type">class="kw">double</span> profit=<span class="number">class="num">0</span>; <span class="preprocessor">class="macro">#ifdef </span><span class="macro">__MQL5__</span> <span class="keyword">class="type">int</span> cntMyPos=<span class="functions">PositionsTotal</span>(); <span class="keyword">for</span>(<span class="keyword">class="type">int</span> ti=cntMyPos-<span class="number">class="num">1</span>; ti>=<span class="number">class="num">0</span>; ti--){ <span class="keyword">if</span>(<span class="functions">PositionGetSymbol</span>(ti)!=<span class="predefines">_Symbol</span>) <span class="keyword">class="kw">continue</span>; <span class="keyword">if</span>(EA_Magic><span class="number">class="num">0</span> && <span class="functions">PositionGetInteger</span>(<span class="macro">POSITION_MAGIC</span>)!=EA_Magic) <span class="keyword">class="kw">continue</span>; profit+=<span class="functions">PositionGetDouble</span>(<span class="macro">POSITION_PROFIT</span>); profit+=<span class="functions">PositionGetDouble</span>(<span class="macro">POSITION_SWAP</span>); } <span class="preprocessor">class="macro">#else </span><span class="keyword">class="type">int</span> cntMyPos=<span class="functions">OrdersTotal</span>(); <span class="keyword">if</span>(cntMyPos><span class="number">class="num">0</span>){ <span class="keyword">for</span>(<span class="keyword">class="type">int</span> ti=cntMyPos-<span class="number">class="num">1</span>; ti>=<span class="number">class="num">0</span>; ti--){ <span class="keyword">if</span>(<span class="functions">OrderSelect</span>(ti,<span class="macro">SELECT_BY_POS</span>,<span class="macro">MODE_TRADES</span>)==<span class="macro">false</span>) <span class="keyword">class="kw">continue</span>; <span class="keyword">if</span>( <span class="functions">OrderType</span>()==<span class="macro">OP_BUY</span> || <span class="functions">OrderType</span>()==<span class="macro">OP_SELL</span> ){}<span class="keyword">else</span>{ <span class="keyword">class="kw">continue</span>; } <span class="keyword">if</span>(<span class="functions">OrderSymbol</span>()!=<span class="predefines">_Symbol</span>) <span class="keyword">class="kw">continue</span>; <span class="keyword">if</span>(EA_Magic><span class="number">class="num">0</span> && <span class="functions">OrderMagicNumber</span>()!=EA_Magic) <span class="keyword">class="kw">continue</span>; profit+=<span class="functions">OrderCommission</span>(); profit+=<span class="functions">OrderProfit</span>(); profit+=<span class="functions">OrderSwap</span>(); } } <span class="preprocessor">class="macro">#endif </span><span class="keyword">class="kw">return</span> <span style="background-class="type">class="kw">color:rgb(class="num">255, class="num">221, class="num">0);">profit</span>; } <span class="keyword">if</span>(takeProfit><span class="number">class="num">0</span> && checkTakeProfit()>takeProfit){ closeAllPos(); }<span class="keyword">else</span> <span class="keyword">if</span>(takeLoss<<span class="number">class="num">0</span> && checkTakeProfit()<takeLoss){ closeAllPos(); }
◍ 只调止损区间的实盘回测
把优化范围收窄到 -5 至 -100 美元的止损档位,其余参数沿用上一轮测试的结果:网格步长 13 点、单网格挂 16 张单。这样跑出来的目的很直接——验证前面那套改进到底有没有用。 回测里止损设在 -56 美元时利润最高,5 个月净赚 156 美元,最大回撤 83 美元。翻成交明细能看到,这 5 个月里止损只被触发过一次,所以利润回撤比看起来才这么漂亮。 但把时间拉长到过去五年再跑同一组参数,结果就难看了。可能额外的优化能救回来一些,但至少说明:拿网格策略做长期自动交易,指望‘浮亏仓位迟早会被后续单扛回来’这个念头本身是错的。外汇与贵金属杠杆高,这类马丁类网格在极端行情下爆仓风险极大,实盘前务必在 MT5 用历史数据自测。
「给网格单挂止损反而更脆」
把单独交易加上止损后,拿五年历史跑优化,曲线比裸网格好看一点:最优参数是止损140点、获利50点,并且若30天内当前网格没开出任何新仓,就强平旧网格重开。 最终净利351美元、最大回撤226美元,表面优于无止损版本。但拆开看,最后一次成交后未满30天就强制关网格的那些回合,全部是亏损;连撑过30天的批次,多数也以亏收场。 外汇与贵金属自动网格本身高风险,这组数字更像是样本巧合,不是可复用的规律。真要验证,自己在MT5里把止损参数从0拉到140点,对比 equity 曲线拐点。
画得少,看得清
跨 MT4 与 MT5 的网格 EA 已经跑通,代码层面双平台兼容这一目标算达成了。但几个月历史回测远远不够,除非你愿意每周手动重调参数,否则简单网格在实盘里大概率扛不住单边。 文中附带 griderEA.mq5(77.8 KB)与 ex5(112.85 KB)可供直接下载验证;评论区有用户提到震荡区间上下限挂网比裸网格更稳,回撤也可能更小。外汇与贵金属网格天然带高杠杆风险,盲用易爆。 复杂网格信号如 EURUSD/AUDUSD/AUDCAD 网格据称月利可达 100%,但那套逻辑不在本文范围内。下一篇才拆 ENUM_ORDER_TYPE 与马丁变体,这一节到此为止,剩下交给你自己开 MT5 拖一遍。