交易机器人在市场发布前必须经过的检验·综合运用
🛡️

交易机器人在市场发布前必须经过的检验·综合运用

(3/3)·从资金不足到CPU耗尽,覆盖市场审核最易打回的15个技术雷区与修复路径

案例拆解 第 3/3 篇

接上篇,我们把分散在各处的检验点收拢成一张完整排雷图。很多开发者在提交市场前只跑了一次回测,结果卡在冻结层改单或数组越界这类低级错上,白白消耗审核周期。这一篇把十五个高频驳回原因按实战场景串起来,方便你逐条对照。

「止损挂单的距离红线卡在30点」

用 CTrade::OrderSend 下 EURUSD 止损买入(Buy Stop),当挂单触发价 1.11019、SL 设 1.10992 时,Open 到 StopLoss 距离是 28 点,返回「无效止损」;调到 1.11020 距离 29 点依旧被拒。 直到第 5 次把价格放到 1.11021、SL 收到 1.10991,距离正好 30 点,订单才成交,日志里带出 SYMBOL_TRADE_STOPS_LEVEL=30。这说明该品种当时最小止损距离就是 30 点,少 1 点都不让发单。 限价买入(Buy Limit)同理:价格 1.10621、TP 从 1.10647(距 26 点)一路试到 1.10649(距 28 点)全被拒;把挂单价降到 1.10619、TP 设 1.10649 凑满 30 点才过。外汇和贵金属杠杆高,这种硬约束先在策略里读 SYMBOL_TRADE_STOPS_LEVEL 再算挂单,别靠拍脑袋。

MQL5 / C++
CTrade::OrderSend: 止损买入 class="num">1.00 EURUSD 价格 class="num">1.11019 sl: class="num">1.10992 [无效止损]
class="num">3. 止损买入 class="num">1.0 EURUSD 价格 class="num">1.11020 SL=class="num">1.10992 (Open-StopLoss=class="num">28 个点)
CTrade::OrderSend: 止损买入 class="num">1.00 EURUSD 价格 class="num">1.11020 sl: class="num">1.10992 [无效止损]
class="num">4. 止损买入 class="num">1.0 EURUSD 价格 class="num">1.11021 SL=class="num">1.10992 (Open-StopLoss=class="num">29 个点)
CTrade::OrderSend: 止损买入 class="num">1.00 EURUSD 价格 class="num">1.11021 sl: class="num">1.10992 [无效止损]
class="num">5. 止损买入 class="num">1.0 EURUSD 价格 class="num">1.11021 SL=class="num">1.10991 (Open-StopLoss=class="num">30 个点)
止损买入 class="num">1.0 EURUSD 完成 价格 class="num">1.11021 止损=class="num">1.10991 (SYMBOL_TRADE_STOPS_LEVEL=class="num">30)
---------
class="num">1. 限价买入 class="num">1.0 EURUSD 价格 class="num">1.10621 TP=class="num">1.10647 (TakeProfit-Open=class="num">26 个点)
CTrade::OrderSend: 限价买入1.class="num">00 EURUSD 价格 class="num">1.10621 tp: class="num">1.10647 [无效止损]
class="num">2. 限价买入1.class="num">0 EURUSD 价格 class="num">1.10621 TP=class="num">1.10648 (TakeProfit-Open=class="num">27 个点)
CTrade::OrderSend: 限价买入1.class="num">00 EURUSD 价格 class="num">1.10621 tp: class="num">1.10648 [无效止损]
class="num">3. 限价买入1.class="num">0 EURUSD 价格1.class="num">10621 TP=class="num">1.10649 (TakeProfit-Open=class="num">28 个点)
CTrade::OrderSend: 限价买入1.class="num">00 EURUSD 价格1.class="num">10621 tp: class="num">1.10649 [无效止损]
class="num">4. 限价买入1.class="num">0 EURUSD 价格1.class="num">10619 TP=class="num">1.10648 (TakeProfit-Open=29个点)
CTrade::OrderSend: 限价买入1.class="num">00 EURUSD 价格 class="num">1.10619 tp: class="num">1.10648 [无效止损]
class="num">5. 限价买入 class="num">1.0 EURUSD 价格1.class="num">10619 TP=class="num">1.10649 (TakeProfit-Open=30个点)
限价买入 class="num">1.0 EURUSD 完成 价格1.class="num">10619 TakeProfit=class="num">1.10649 (SYMBOL_TRADE_STOPS_LEVEL=class="num">30)

冻结水平内改单会直接撞墙

交易品种规格里的 SYMBOL_TRADE_FREEZE_LEVEL 定义了挂单激活价与实时报价之间的最小安全距离。若资产路由到外部系统,限价单离卖价过近时,改单请求可能触发成交而非修改,仓位和挂单在冻结距离内均不可移动。 发送修改前必须按类型做距离校验:买入限价看 Ask-OpenPrice 是否 ≥ 冻结值,卖出限价看 OpenPrice-Bid,BuyStop 看 OpenPrice-Ask,SellStop 看 Bid-OpenPrice;已开仓位还要同时校验止盈止损与 Bid/Ask 的双向距离。 下面这段从检查函数里摘出的分支,演示了两类限价单的判定逻辑。注意所有比较都乘了 _Point,因为冻结水平是以点数为单位的整数。 [CODE] //--- 检查订单类型 switch(type) { //--- 限价买入挂单 case ORDER_TYPE_BUY_LIMIT: { //--- 检查当前价格与激活价格的距离 check=((Ask-price)>freeze_level*_Point); if(!check) PrintFormat("订单 %s #%d 不能被修改: Ask-Open=%d 个点 < SYMBOL_TRADE_FREEZE_LEVEL=%d 个点", EnumToString(type),ticket,(int)((Ask-price)/_Point),freeze_level); return(check); } //--- 限价买入挂单 case ORDER_TYPE_SELL_LIMIT: { //--- 检查当前价格与激活价格的距离 check=((price-Bid)>freeze_level*_Point); if(!check) PrintFormat("订单 %s #%d 不能被修改: Open-Bid=%d 个点 < SYMBOL_TRADE_FREEZE_LEVEL=%d 个点", EnumToString(type),ticket,(int)((price-Bid)/_Point),freeze_level); return(check); } break; //--- BuyStop 挂单 case ORDER_TYPE_BUY_STOP: { //--- 检查当前价格与激活价格的距离 check=((price-Ask)>freeze_level*_Point); if(!check) PrintFormat("订单 %s #%d 不能被修改: Ask-Open=%d 个点 < SYMBOL_TRADE_FREEZE_LEVEL=%d 个点", EnumToString(type),ticket,(int)((price-Ask)/_Point),freeze_level); return(check); } //--- 止损卖出挂单 case ORDER_TYPE_SELL_STOP: { //--- 检查当前价格与激活价格的距离 check=((Bid-price)>freeze_level*_Point); if(!check) PrintFormat("订单 %s #%d 不能被修改: Bid-Open=%d points < SYMBOL_TRADE_FREEZE_LEVEL=%d 个点", [/CODE] 想实锤这个限制,开一个模拟账户,挑 SYMBOL_TRADE_FREEZE_LEVEL 不为 0 的品种,挂任意单后加载测试 EA,它会把单子推到离市价最近处并循环发起非法改单,用 PlaySound() 报警。外汇与贵金属杠杆高,冻结规则因品种和经纪商而异,实盘前务必在模拟环境跑通检查逻辑。

MQL5 / C++
class=class="str">"cmt">//--- 检查订单类型
  class="kw">switch(type)
    {
    class=class="str">"cmt">//--- 限价买入挂单
    case  ORDER_TYPE_BUY_LIMIT:
      {
      class=class="str">"cmt">//--- 检查当前价格与激活价格的距离
      check=((Ask-price)>freeze_level*_Point);
      if(!check)
        PrintFormat("订单 %s #%d 不能被修改: Ask-Open=%d 个点 < SYMBOL_TRADE_FREEZE_LEVEL=%d 个点",
                    EnumToString(type),ticket,(class="type">int)((Ask-price)/_Point),freeze_level);
      class="kw">return(check);
      }
    class=class="str">"cmt">//--- 限价买入挂单
    case  ORDER_TYPE_SELL_LIMIT:
      {
      class=class="str">"cmt">//--- 检查当前价格与激活价格的距离
      check=((price-Bid)>freeze_level*_Point);
      if(!check)
        PrintFormat("订单 %s #%d 不能被修改: Open-Bid=%d 个点 < SYMBOL_TRADE_FREEZE_LEVEL=%d 个点",
                    EnumToString(type),ticket,(class="type">int)((price-Bid)/_Point),freeze_level);
      class="kw">return(check);
      }
    class="kw">break;
    class=class="str">"cmt">//--- BuyStop 挂单
    case  ORDER_TYPE_BUY_STOP:
      {
      class=class="str">"cmt">//--- 检查当前价格与激活价格的距离
      check=((price-Ask)>freeze_level*_Point);
      if(!check)
        PrintFormat("订单 %s #%d 不能被修改: Ask-Open=%d 个点 < SYMBOL_TRADE_FREEZE_LEVEL=%d 个点",
                    EnumToString(type),ticket,(class="type">int)((price-Ask)/_Point),freeze_level);
      class="kw">return(check);
      }
    class=class="str">"cmt">//--- 止损卖出挂单
    case  ORDER_TYPE_SELL_STOP:
      {
      class=class="str">"cmt">//--- 检查当前价格与激活价格的距离
      check=((Bid-price)>freeze_level*_Point);
      if(!check)
        PrintFormat("订单 %s #%d 不能被修改: Bid-Open=%d points < SYMBOL_TRADE_FREEZE_LEVEL=%d 个点",

◍ 挂单冻结区的距离判定逻辑

上面这段代码片段承担了一个关键动作:把挂单类型、订单票号,以及当前买价与挂单价格之间的点差距离,连同经纪商设定的冻结层级一并送出校验。 其中 (int)((Bid-price)/_Point) 直接计算出现价到挂单价的间隔点数,再与 freeze_level 比较,可判断该挂单是否落入禁止修改/删除的冻结区。MT5 中 _Point 为当前品种最小报价单位,黄金 XAUUSD 通常 _Point=0.01,欧美则多为 0.00001。 你在 EA 里调挂单前,最好先打印 freeze_level 和实际点差,避免订单因处于冻结区被 silently rejected。外汇与贵金属杠杆高,这类底层拒绝不会弹窗提示,回测不暴露,实盘才暴雷。

MQL5 / C++
EnumToString(type),ticket,(class="type">int)((Bid-price)/_Point),freeze_level);
 class="kw">return(check);
 }
 class="kw">break;
 }

「历史不足时 EA 与指标的两种死法」

在 MT5 图表上跑 EA 或指标时,如果本地历史报价不够长,程序只有两条路:要么自己先数柱数,发现不够就向终端请求补齐,并在下一 tick 到来前暂停计算;要么什么都不查,直接当数据已就绪开干。前者能避开数组越界、除零这类低级崩溃,后者迟早翻车。 想亲手复现第二种惨状很简单:把测试用的指标或 EA 挂在图表上,关掉终端,去历史目录删掉对应品种文件,再重开终端。若日志干净,就现场切品种或周期——很多指标在 W1、MN1 上会直接报错,因为这两周期默认柱数极少。 再狠一点,运行时把图表从 EURUSD 瞬间切成 CADJPY,指标或 EA 大概率因来不及补算历史而抛错。外汇与贵金属杠杆高、品种切换频繁,这类隐性缺陷在实盘可能触发异常下单,务必在策略测试器里先压一遍边界。

动态数组越界的两个隐蔽触发点

MQL5 里访问数组元素靠非负索引,且索引必须严格小于 ArraySize() 的返回值。这条规则在静态数组上很直观,但遇到动态数组就容易踩坑。 CopyTicks() 是个典型陷阱:它试图把请求的订单量写进你传进去的动态数组,但如果实际复制到的tick少于请求数,数组真实大小会比你预想的小。此时若按原请求数量去索引,就会触发超出范围。外汇与贵金属行情在流动性断裂时 tick 复制可能不完整,这类风险需警惕。 另一个隐蔽点是指标缓冲区。它本质也是动态数组,大小由终端在图表初始化后确定。若在 OnInit() 里直接读缓冲区数据,缓冲区尚未分配好长度,几乎必然报“数组超出范围”。正确做法是将访问逻辑放到 OnCalculate() 中。 附件 Test_Out_of_range.mq5 给出了可复现的报错实例,开 MT5 加载该文件即可观察报错时机。

◍ 别让零值掐断EA

除以零是 MQL5 里最直白的致命错误之一:只要运算里出现分母为零,程序立刻中止,策略测试器会在日志抛出函数名加行号。这类事故通常不是语法写错,而是开发者没料到某些数据会“坏掉”——比如读取指标属性拿到空值,或表达式里混入了未初始化的变量。 用 TestZeroDivide.mq5 这个 EA 可以稳定复现:它故意拿零当除数,跑起来测试器马上报错并定位到具体行。顺着日志回去改判断逻辑,比盲猜高效得多。外汇和贵金属行情缺口多,历史数据调试时更要警惕这种零值陷阱,概率上它常在异常 tick 后出现。 另一个容易同场暴雷的是不正确的对象指针,调试时建议直接挂历史数据逐步跑,能较快锁定是哪一根 K 线或哪次调用触发了崩溃。

「改单前先确认参数真动了」

EA 在发修改挂单或持仓的请求前,必须先核实价格、止损、获利里至少有一个数值相对原值发生了变动。若请求发出却没有任何实际改动,交易服务器会直接回拒:MQL5 拿到 TRADE_RETCODE_NO_CHANGES = 10025,MQL4 拿到 ERR_NO_RESULT = 1。这种无效发单既浪费服务器往返,也会在日志里堆出无意义报错。 下面这段 MQL5 检查函数,就是避免空改单的核心逻辑。它先用订单/持仓对象选中目标,再按品种 point 精度做差值的绝对值比较,只要开仓价、SL、TP 任一偏离原值超过一个点,才返回 true 允许修改。 别把正态当圣经 实际写机器人时,很多人直接 trade.PositionModify() 不判返回值,结果在震荡行情里反复发相同 SL,触发 10025 把 EA 卡在重试循环。把检查函数接在修改动作之前,能显著减少无效通信。外汇与贵金属杠杆高,无效改单虽不直接亏钱,但会拖慢对实时风险的响应。

MQL5 / C++
class=class="str">"cmt">//--- 用于进行交易操作的类
class="macro">#include <Trade\Trade.mqh>
CTrade trade;
class="macro">#include <Trade\Trade.mqh>
class=class="str">"cmt">//--- 用于操作订单的类
class="macro">#include <Trade\OrderInfo.mqh>
COrderInfo orderinfo;
class=class="str">"cmt">//--- 用于操作仓位的类
class="macro">#include <Trade\PositionInfo.mqh>
CPositionInfo positioninfo;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 在修改订单之前检查水平新的数值                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool OrderModifyCheck(class="type">class="kw">ulong ticket,class="type">class="kw">double price,class="type">class="kw">double sl,class="type">class="kw">double tp)
  {
class=class="str">"cmt">//--- 根据编号选择订单
   if(orderinfo.Select(ticket))
     {
      class=class="str">"cmt">//--- 用于设置挂单的交易品种的点位大小和名称
      class="type">class="kw">string symbol=orderinfo.Symbol();
      class="type">class="kw">double point=SymbolInfoDouble(symbol,SYMBOL_POINT);
      class="type">int digits=(class="type">int)SymbolInfoInteger(symbol,SYMBOL_DIGITS);
      class=class="str">"cmt">//--- 检查建仓价格有没有改变
      class="type">bool PriceOpenChanged=(MathAbs(orderinfo.PriceOpen()-price)>point);
      class=class="str">"cmt">//--- 检查止损水平有没有改变
      class="type">bool StopLossChanged=(MathAbs(orderinfo.StopLoss()-sl)>point);
      class=class="str">"cmt">//--- 检查获利水平有没有改变
      class="type">bool TakeProfitChanged=(MathAbs(orderinfo.TakeProfit()-tp)>point);
      class=class="str">"cmt">//--- 如果水平有任何变化
      if(PriceOpenChanged || StopLossChanged || TakeProfitChanged)
        class="kw">return(true);  class=class="str">"cmt">// 订单可以修改      
      class=class="str">"cmt">//--- 开盘价,止损和获利水平没有变化
      else
      class=class="str">"cmt">//--- 通知错误
        PrintFormat("订单 #%d 水平已经是 Open=%.5f SL=%.5f TP=%.5f",
                    ticket,orderinfo.PriceOpen(),orderinfo.StopLoss(),orderinfo.TakeProfit());
     }
class=class="str">"cmt">//--- 结束,订单没有改变
   class="kw">return(class="kw">false);      class=class="str">"cmt">// 不需要做修改 
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 在修改订单之前检查水平新的数值                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool PositionModifyCheck(class="type">class="kw">ulong ticket,class="type">class="kw">double sl,class="type">class="kw">double tp)
  {
class=class="str">"cmt">//--- 根据编号选择订单
   if(positioninfo.SelectByTicket(ticket))
     {
      class=class="str">"cmt">//--- 用于设置挂单的交易品种的点位大小和名称
      class="type">class="kw">string symbol=positioninfo.Symbol();
      class="type">class="kw">double point=SymbolInfoDouble(symbol,SYMBOL_POINT);
      class=class="str">"cmt">//--- 检查止损水平有没有改变
      class="type">bool StopLossChanged=(MathAbs(positioninfo.StopLoss()-sl)>point);
      class=class="str">"cmt">//--- 检查获利水平有没有改变
      class="type">bool TakeProfitChanged=(MathAbs(OrderTakeProfit()-tp)>point);
      class=class="str">"cmt">//--- 如果水平有任何变化
      if(StopLossChanged || TakeProfitChanged)
        class="kw">return(true);  class=class="str">"cmt">// 仓位可以修改      

改单前先比点位差

在 MT5 里直接调挂单或仓位,最怕反复发无效修改请求——服务器拒单不说,还白白消耗执行资源。这段逻辑的核心是先拿当前订单的 Open/SL/TP 和传入新值做差,用品种 point 值当门槛,差不超过一个点位就判定「没变化」。 具体看 OrderModifyCheck:对非市价单(OP_BUY/OP_SELL 之外)才比较开盘价,市价持仓本身没有挂单价可改;SL、TP 则一律用 MathAbs 算绝对差。三者任一差大于 point,返回 true 才进 trade.OrderModify,否则 PrintFormat 打出「订单 #%d 水平已经是 Open=%.5f SL=%.5f TP=%.5f」并 return false。 外汇和贵金属点值小、跳动点频繁,这种「先比对再发单」的写法能明显降低无效改单概率,属于高频调参脚本里该有的防护。开 MT5 把这段塞进你的 EA 前置函数,跑一晚日志就能看出拒单率变化。

MQL5 / C++
else
class=class="str">"cmt">//--- 通知错误
  PrintFormat("订单 #%d 水平已经是 Open=%.5f SL=%.5f TP=%.5f",
                ticket,orderinfo.PriceOpen(),orderinfo.StopLoss(),orderinfo.TakeProfit());
   }
class=class="str">"cmt">//--- 结束,订单没有改变
   class="kw">return(class="kw">false);      class=class="str">"cmt">// 不需要做修改 
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 脚本程序起始函数                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
class=class="str">"cmt">//--- 订单和仓位的价格水平
   class="type">class="kw">double priceopen,stoploss,takeprofit;
class=class="str">"cmt">//--- 当前订单和仓位的编号
   class="type">class="kw">ulong orderticket,positionticket;
class=class="str">"cmt">/*
   ... 取得订单编号和新的止损/获利/建仓价格水平
*/
class=class="str">"cmt">//--- 在修改挂单之前检查水平   
   if(OrderModifyCheck(orderticket,priceopen,stoploss,takeprofit))
     {
      class=class="str">"cmt">//--- 检验成功
      trade.OrderModify(orderticket,priceopen,stoploss,takeprofit,
                         orderinfo.TypeTime(),orderinfo.TimeExpiration());
     }
class=class="str">"cmt">/*
   ... 取得仓位的编号和新的止损/获利水平
*/
class=class="str">"cmt">//--- 在修改仓位之前做检查
   if(PositionModifyCheck(positionticket,stoploss,takeprofit))
     {
      class=class="str">"cmt">//--- 检验成功
      trade.PositionModify(positionticket,stoploss,takeprofit);
     }
class=class="str">"cmt">//---
  }
class="macro">#class="kw">property strict
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 在修改订单之前检查水平新的数值                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool OrderModifyCheck(class="type">int ticket,class="type">class="kw">double price,class="type">class="kw">double sl,class="type">class="kw">double tp)
  {
class=class="str">"cmt">//--- 根据编号选择订单
   if(OrderSelect(ticket,SELECT_BY_TICKET))
     {
      class=class="str">"cmt">//--- 用于设置挂单的交易品种的点位大小和名称
      class="type">class="kw">string symbol=OrderSymbol();
      class="type">class="kw">double point=SymbolInfoDouble(symbol,SYMBOL_POINT);
      class=class="str">"cmt">//--- 检查建仓价格有没有改变
      class="type">bool PriceOpenChanged=true;
      class="type">int type=OrderType();
      if(!(type==OP_BUY || type==OP_SELL))
        {
         PriceOpenChanged=(MathAbs(OrderOpenPrice()-price)>point);
        }
      class=class="str">"cmt">//--- 检查止损水平有没有改变
      class="type">bool StopLossChanged=(MathAbs(OrderStopLoss()-sl)>point);
      class=class="str">"cmt">//--- 检查获利水平有没有改变
      class="type">bool TakeProfitChanged=(MathAbs(OrderTakeProfit()-tp)>point);
      class=class="str">"cmt">//--- 如果水平有任何变化
      if(PriceOpenChanged || StopLossChanged || TakeProfitChanged)
         class="kw">return(true);   class=class="str">"cmt">// 订单可以修改      
      class=class="str">"cmt">//--- 开盘价,止损和获利水平没有变化
      else
      class=class="str">"cmt">//--- 通知错误
        PrintFormat("订单 #%d 水平已经是 Open=%.5f SL=%.5f TP=%.5f",

◍ 改单前的校验与落地调用

这段脚本把订单修改拆成了两步:先校验、再执行。核心思路是写一个 OrderModifyCheck 函数,在真正调用 OrderModify 之前把 ticket、开仓价、止损、获利都过一遍,避免无效改单占用交易上下文。 OnStart 里只做三件事:取订单编号与新的价格水平、跑校验、校验通过才改单。注意 OrderModify 的第四个参数直接用了 OrderExpiration(),意味着过期时间沿用原订单,不会因改 SL/TP 而被清零。 外汇与贵金属市场滑点和报价跳空频繁,未通过检查的订单直接 return(false),不触发任何修改。你在 MT5 里接这段逻辑时,可把 OrderModifyCheck 内部补上点差与止损最小距离判断,能显著降低 130(无效价格)报错概率。

MQL5 / C++
   ticket,OrderOpenPrice(),OrderStopLoss(),OrderTakeProfit());
   }
class=class="str">"cmt">//--- 结束,订单没有改变
   class="kw">return(class="kw">false);   class=class="str">"cmt">// 不需要做修改 
  }
class=class="str">"cmt">//+----------------------------------------+
class=class="str">"cmt">//| 脚本程序起始函数                       |
class=class="str">"cmt">//+----------------------------------------+
class="type">void OnStart()
  {
class=class="str">"cmt">//--- 订单和仓位的价格水平
   class="type">class="kw">double priceopen,stoploss,takeprofit;
class=class="str">"cmt">//--- 当前订单的编号 
   class="type">int orderticket;
class=class="str">"cmt">/*
   ... 取得订单编号和新的止损/获利/建仓价格水平
*/
class=class="str">"cmt">//--- 在修改订单之前做检查  
   if(OrderModifyCheck(orderticket,priceopen,stoploss,takeprofit))
     {
      class=class="str">"cmt">//--- 检验成功
      OrderModify(orderticket,priceopen,stoploss,takeprofit,OrderExpiration());
     }
  }

「市场发布为何封死DLL与编译件调用」

在MQL5社区市场发布的产品,强制要求对用户环境安全可控。凡是直接调用DLL,或引用已编译好的EX4/EX5文件里的函数,审核时会直接判为错误,这类产品根本过不了上架关。 如果你确实需要标准发布包之外的自定义指标逻辑,正规做法不是外挂编译件,而是把指标以资源(Resources)方式嵌入到EA或脚本里,随主程序一起编译分发。 这条限制意味着:想走市场分发,就得放弃运行时动态加载第三方二进制;本地测试可以随便玩,上架前必须改成自包含的资源内嵌结构,否则提交即被打回。

把自定义指标塞进资源再调 iCustom

想在 EA 或脚本里拿自定义指标的数值,第一步不是直接读外部文件,而是把那个指标先编译进资源(Resources)。市场里的产品必须能在裸机环境跑起来,所以 EX5 文件自身得携带全部依赖,否则换台没预装指标的终端就直接废了。 用 iCustom() 拿数据的时候,句柄指向的是已经作为资源嵌入的指标,而不是客户端目录里碰巧存在的同名文件。这样分发才稳,不会因为用户少放一个指标就报「找不到自定义指标」。 MQL5 里资源指标的引用写法是 #resource "Indicators\\MyFilter.ex5",编译后它就跟你的主程序绑死了。外汇和贵金属策略调用这类自定义过滤条件时波动放大,回测和实盘都属高风险,参数误用可能直接改变信号频率。

MQL5 / C++
class="macro">#resource "Indicators\\MyFilter.ex5"
class="type">int handle = iCustom(_Symbol, _Period, "::Indicators\\MyFilter.ex5", class="num">14, class="num">0);

◍ 运行时参数错位的几个代号

向 MQL5 函数传入无效参数触发的运行时错误,实际占比不高,但代号体系很清晰,排错时先对照数值比看描述快得多。 下表是常见的一类:4003 代表系统函数拿到错误参数;4516 多见于把空串或 NULL 塞进 SendNotification();4602 是指标缓冲区索引越界;4808–4811 集中在自定义指标创建阶段的参数数量或类型异常;5030/5031 是时间字符串解析失败;5038/5039 则是 Format 类函数里占位符和实参数量不匹配。 这些代号不会覆盖程序运行和测试中所有意外,比如内部函数误调用返回的 4002 就常被忽略。遇到非预期崩溃,先打印 GetLastError() 的数值,再反查上表,比盲改参数省时间。外汇与贵金属行情波动剧烈,EA 在实盘跑这类校验失败可能瞬间漏单,建议先在策略测试器里用极端输入复现。

「撞上访问冲突先别急着怪平台」

MQL5 程序跑着跑着弹出访问冲突,本质是 EA 或指标试图读写一个被系统拒掉的内存地址,这类崩溃通常不在你策略逻辑里,而在底层运行时环境。 碰到这种情况,自己硬调参数没用,正确动作是走服务台或联系人页面找开发方报障。 报障时把重现步骤写细、把相关源代码一并附上,能显著缩短对方定位时间,也顺带帮编译器后续版本排雷。外汇与贵金属品种波动剧烈,这类底层异常若出现在实盘加载阶段,应先在模拟盘隔离验证再上真仓。

一个线程拖垮整个品种图表

MT5 终端给「市场报价」里的每个交易品种只分配一条通用线程,挂在该品种上的所有图表与指标全挤在这条线程里算。比如同时开 5 个 EURUSD 不同周期图、共跑 15 个指标,这 5 图 15 指标的运算和绘图都由同一线程完成;其中一个指标写得很费资源,就可能把同品种其他图表的价格刷新也拖死。外汇和贵金属杠杆高、跳空频繁,图表卡死时你看到的报价可能早已滞后,实盘风险陡增。 想揪出耗时大户,直接在代码里插 GetMicrosecondCount() 抓前后微秒数,除以 1000 就得到毫秒。经验阈值:把「图表最大柱数」设成 Unlimited、在 M1 周期加载十年以上历史的品种,若指标首次 OnCalculate 超过 100 毫秒,基本就要重构。下方 ROC 指标样例里高亮两行就是掐表逻辑。 内存占用用 MQLInfoInteger(MQL_MEMORY_USED) 量的,配合 MT5 自带的 Profiler 能直接定位最吃算力的函数。EA 虽跑独立线程,这套测法同样适用——不论 EA、指标还是脚本,写出省资源的循环都是底线。

MQL5 / C++
class=class="str">"cmt">//+-----------------------------------------------+
class=class="str">"cmt">//| 变化速率(Rate of Change, ROC)                  |
class=class="str">"cmt">//+-----------------------------------------------+
class="type">int OnCalculate(class="kw">const class="type">int rates_total,class="kw">const class="type">int prev_calculated,class="kw">const class="type">int begin,class="kw">const class="type">class="kw">double &price[])
  {
class=class="str">"cmt">//--- 检查数据数量
   if(rates_total<ExtRocPeriod)
      class="kw">return(class="num">0);
class=class="str">"cmt">//--- 计算开始时间
   class="type">class="kw">ulong start=GetMicrosecondCount();
class=class="str">"cmt">//--- 预先计算
   class="type">int pos=prev_calculated-class="num">1; class=class="str">"cmt">// 设置计算位置
   if(pos<ExtRocPeriod)
      pos=ExtRocPeriod;
class=class="str">"cmt">//--- 计算的主循环
   for(class="type">int i=pos;i<rates_total && !IsStopped();i++)
     {
      if(price[i]==class="num">0.0)
         ExtRocBuffer[i]=class="num">0.0;
      else
         ExtRocBuffer[i]=(price[i]-price[i-ExtRocPeriod])/price[i]*class="num">100;
     }
class=class="str">"cmt">//--- 计算结束时间
   class="type">class="kw">ulong finish=GetMicrosecondCount();
   PrintFormat("Function %s in %s took %.1f ms",__FUNCTION__,__FILE__,(finish-start)/class="num">1000.);
class=class="str">"cmt">//--- OnCalculate 完成返回新的 prev_calculated.
   class="kw">return(rates_total);
  }

◍ 把工具请下神坛

前面聊的检验指标与 EA 的技巧,不只用在准备上架的程序,你自己写的私用机器人同样该照做。文章没覆盖真实账户里所有坑,比如断线、重新报价、拒单这些会打断交易系统规则的状况,每个开发者都有自己的处理路数。 新手把错误处理那几篇啃完,去论坛评论区抛问题,老手大概率会接话;这些积累能让你少花时间做出更稳的机器。 评论区有个现成例子:有交易者在 M5 专门写 XAUUSD 的 EA,验证却自动跑 EURUSD 导致失败,日志里刷出 failed market sell 0.2 EURUSD [Invalid stops] 这类报错。结论很直白——MT5 验证有意跨品种跨周期跑,EA 必须能在任意符号和时间框架下无故障交易,不盈利可以,崩不行。外汇和贵金属杠杆高,这类隐性故障真上了真实账户可能直接放大亏损。 所以别把回测绿线和验证通过当成圣旨。工具是帮你排错用的,不是替你担保收益,把 EA 丢进多品种乱跑的测试里挨一遍打,比信任何‘已通过’标签都实在。

把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到异常订单与资源占用的提示,你只需专注策略逻辑本身。

常见问题

冻结层是券商设定的不可操作区间,强行改单会触发运行时拒绝,检验日志会记为无效请求,必须绕开该范围或先撤单再挂。
路径依赖和编译不一致可能导致其他用户端加载失败,检验会要求改为资源内嵌或标准指标调用,降低环境耦合。
可以,小布在品种页持续采样脚本资源曲线,若发现回测外实时消耗突增会给出提示,便于发布前定位死循环类问题。
它依赖特定行情下的分母取值,只在极端价差或空数据段触发,需多品种多时段交叉测试才能暴露。
指开发者堆砌无意义的安全判断拖慢执行,审核倾向精简的必要防护,去掉冗余分支即可通过。