首页 > 博客 > 彻底解决 Invalid stops(10016/130)— MT5/MT4的SL设置问题

MT5MQL5错误代码故障排除EA

彻底解决 Invalid stops(10016/130)— MT5/MT4的SL设置问题

发布日: 2026-07-07阅读时间:约 2 分钟
本文为发布之日的信息。EA的业绩数值(PF、DD、年化)会随实盘运行与重新验证而变动,最新数值请在各EA页面确认。 查看最新EA业绩

彻底解决 Invalid stops(10016/130)

运行EA时,如果Expert标签页出现 Invalid stopsOrderSend error 130,很容易让人困惑:"SL的值不对吗?可计算应该没问题啊……"。但实际上,这个错误绝大多数并非SL/TP计算错误,而是违反了「经纪商设定的最小距离规则」。即便数值本身正确,只要过于接近当前价格、方向搞反、或落入修改禁止区间,都会被拒绝。

本文面向在MT5/MT4上使用EA的交易者,以及用MQL5编写EA的开发者,将 Invalid stops本质、6大成因、30秒排查法、代码层面的根治方案整合为一篇终极指南。如需查看错误代码的完整列表,请参考 MQL5 / MT5 错误代码处理指南

本文基于2026年7月时点的MT5(build 4xxx系列)撰写。止损位的具体数值因经纪商和品种而异。


Invalid stops 是什么(10016 与 130 的区别)

表示「无效止损」的返回值,在MT5与MT4(不同世代)中有两种。区分清楚是在哪个平台、哪个阶段出现的,能大幅加快排查原因的速度。

① TRADE_RETCODE_INVALID_STOPS = 10016(MT5 / OrderSend() 的返回代码)

MQL5中 OrderSend() 的结果会写入 MqlTradeResult.retcode。如果请求中的SL/TP(或挂单价格之间的关系)不符合服务器规则,就会被 10016(TRADE_RETCODE_INVALID_STOPS) 拒绝。这是来自交易服务器的拒绝通知

含义: 请求中的止损(SL/TP)无效(Invalid stops in the request)
常量: TRADE_RETCODE_INVALID_STOPS
值  : 10016
// 日志中常见的典型输出示例
2026.07.07 09:15:32.441 EA_NAME XAUUSD,M5: OrderSend error: retcode=10016 (invalid stops)

② ERR_INVALID_STOPS = 130(MT4 / GetLastError()

在MT4(MQL4)世代中,OrderSend() / OrderModify() 失败后,GetLastError() 会返回 130(ERR_INVALID_STOPS)。Expert标签页中的 OrderSend error 130 就是这个情况。如果同时使用MT4版EA,会看到这个提示。

含义: 无效止损(invalid stops)
常量: ERR_INVALID_STOPS
值  : 130

实际使用时的区分方法:

平台获取来源出现场景
MT5MqlTradeResult.retcode10016(TRADE_RETCODE_INVALID_STOPS)OrderSend / PositionModify 被服务器拒绝
MT5CTrade.ResultRetcode()10016通过 CTrade 下单/修改被拒绝
MT4GetLastError()130(ERR_INVALID_STOPS)OrderSend / OrderModify 失败后

编号虽不同,但含义与成因几乎一致,处理方式也是相通的。另外需要注意,MT5中常被混淆的 10015(TRADE_RETCODE_INVALID_PRICE)是「订单价格本身无效」,属于另一个问题。10016始终是「SL/TP(止损)位置」的问题。


先花30秒做初步排查

打开MT5中的「行情 → 右键品种 → 规格」,查看以下两项:

止损位 (Stops level)    : SL/TP 必须与当前价格保持的最小距离(点数)
冻结距离 (Freeze level) : 即将触发的订单禁止修改/取消的距离(点数)

然后,将错误发生瞬间的日志中 试图发送的SL/TP值与当时的Bid/Ask 并列对比。

  • SL或TP与当前价格的距离小于止损位 → 这基本就是原因(原因①)。
  • BUY单的SL却高于Bid/TP却低于Bid(SELL则相反) → 方向搞反了(原因②)。
  • 只有修改已持仓单会失败 → 可能是冻结距离,或SL/TP需后置的规则(原因③、⑤)。
  • SL的值是「50」这类明显不是价格的数字 → 价格与点数混淆(原因④)。

用代码确认的话,一行就够:

Print("StopsLevel=", SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL),
      " FreezeLevel=", SymbolInfoInteger(_Symbol, SYMBOL_TRADE_FREEZE_LEVEL));

成因与对策(6种模式)

① SL/TP过于接近当前价格(小于止损位)

症状: 常见于SL设置很紧的剥头皮类EA、追踪止损幅度很小的EA。手动交易时,若试图将SL设在「价格附近」,下单按钮会无法点击或被拒绝。

原因: 经纪商会针对每个品种设置 SYMBOL_TRADE_STOPS_LEVEL(以点数为单位的最小止损距离),距当前价格不足此距离的SL/TP、挂单价格都会被服务器统一拒绝。判定所用的基准价格:BUY持仓的SL/TP用Bid,SELL则用Ask。点差扩大时Bid与Ask之间的间隔会拉开,因此平时能通过的距离,也可能在行情发布时或清晨突然被拒绝。

对策:

  1. 在规格窗口确认止损位,并将EA的SL/TP、追踪止损幅度设置得比该值更宽
  2. 在EA端下单前先做夹紧处理(见下文代码)
  3. 如果确实需要很紧的SL,可考虑更换止损位较小的经纪商或账户类型

② SL/TP方向搞反(BUY/SELL混淆)

症状: 特定方向(只做多或只做空)必定出现Invalid stops。这是自制EA首次测试中最常见的模式。

原因: 规则很简单,BUY的SL必须低于当前价格(Bid)、TP必须高于;SELL的SL必须高于当前价格(Ask)、TP必须低于。将BUY的计算公式复制到SELL时忘记改符号、把 price - slprice + sl 弄混,是典型的错误,服务器会立即返回10016/130。

对策:

  1. 出错时用 PrintFormat("type=%s sl=%.5f tp=%.5f bid=%.5f ask=%.5f", ...) 输出实际数值,目视确认方向
  2. 将BUY/SELL的SL/TP计算整合为共用函数,把符号分支集中到一处(避免复制粘贴产生分支)

③ 在冻结区间内修改订单/持仓

症状: 新开仓没有问题,但只有在TP即将触发、SL即将触发、挂单即将触发之前的修改/取消被拒绝。

原因: 在设置了 SYMBOL_TRADE_FREEZE_LEVEL 的品种上,当当前价格接近触发价(TP/SL/挂单的触发价格)到一定距离内,该订单的修改/取消会被冻结。这是为防止成交处理与修改请求冲突而设的服务器规则,常表现为EA的追踪止损「在TP即将触发前想再更新一次却被拒绝」。

对策:

  1. 修改前先读取 SYMBOL_TRADE_FREEZE_LEVEL,若距触发价的距离小于等于冻结距离,则跳过本次修改
  2. 加大追踪止损的更新间隔和更新幅度,减少临近触发时的无效修改请求
  3. 即使被拒绝也不致命(临近触发=即将成交),可以设计为吞掉错误只记录日志即可

④ 价格与点数(距离)混淆

症状: SL中直接填入了「本意是距离」的数值,如 500.0050。日志中出现的 sl=50.00000 明显不是价格。

原因: 传入 MqlTradeRequest.sl / .tp 的必须是绝对价格(而不是「距入场价下方50点」这种相对值)。按距离管理的EA,必须先转换为 entry ± distance * _Point 再传入。反之,若按MT4时代某些函数的习惯,把本该传绝对价格的地方传成了距离,价格就不成立,会导致10016/130。

此外,pips与点数的混淆也是常见问题。在5位报价经纪商中(例如USDJPY为3位小数、EURUSD为5位小数显示),1 pip = 10点。「SL 50」到底是按pips算还是按点数算,距离会相差10倍,从而被压缩到止损位以下而被拒绝。

对策:

  1. SL/TP务必以 NormalizeDouble(price ± dist * _Point, _Digits) 的形式组装
  2. 用注释明确输入参数的单位(pips / points),并将 _Point 换算逻辑集中到一处

⑤ 市价执行经纪商无法在开仓时设置SL/TP

症状: 不带SL/TP能成交,但带SL/TP的新开仓单却出现Invalid stops。尤其常见于ECN/市价执行(Market Execution)类账户。

原因: 市价执行模式下,「请求时的价格」与「实际成交价格」会有偏差,因此部分服务器不接受新开仓请求中携带SL/TP,而要求成交后通过修改持仓的方式设置(这是MT4时代ECN账户频发错误130的经典规则,MT5上仍有服务器保留同样的行为)。

对策:

  1. 改为先不带SL/TP下单 → 确认成交后再用 PositionModify()(若用CTrade则为 trade.PositionModify())设置SL/TP的两段式处理
  2. 这种方式会出现「下单成功但SL设置失败」的瞬间,因此必须加入SL设置失败则重试,达到规定次数仍失败就立即平仓的保护机制(放任无SL持仓是最坏的结果)
  3. 可通过 SymbolInfoInteger(_Symbol, SYMBOL_TRADE_EXEMODE) 确认执行方式

⑥ 品种特有的特性(黄金、指数的止损位较大)

症状: 同一个EA在EURUSD上正常运行,一旦用于XAUUSD(黄金)或股指CFD,就频繁出现Invalid stops。

原因: 止损位是按品种单独设置的,黄金、指数、小众货币对通常比主要货币对设置得更大。如果直接套用为主要外汇品种调校的紧凑SL/追踪止损幅度,就会达不到该品种的最小距离而被拒绝。小数位数也因品种而异(黄金通常为2〜3位),因此硬编码 _Digits 的代码同样会出问题。

对策:

  1. 更换品种后,务必在规格窗口确认止损位与小数位数
  2. 将SL/TP幅度改为基于ATR等波动率指标而非固定点数,可以更好地跨品种适应
  3. 代码中始终动态获取 _Point / _Digits / SYMBOL_TRADE_STOPS_LEVEL(禁止硬编码)

经纪商之间的差异(注意事项)

止损位、冻结距离因经纪商与品种的组合而完全不同。同一个EA、同样的设置,在经纪商A上从未出现过的错误,在经纪商B上却每天都出现,这种情况很常见。

更需要注意的是,止损位显示为「0」的经纪商。0往往不代表「无限制」,而是意味着「动态判定」,平时无论SL设置得多近都能通过,却会在行情发布或清晨等点差扩大的瞬间被拒绝。「偶尔才出现的Invalid stops」的真相多半就是这个原因。

请务必在实盘账户上进行确认。

long stops  = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);   // 点数
long freeze = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_FREEZE_LEVEL);  // 点数

具体数值因各经纪商的账户类型和品种规格而异,本文不做罗列。在自己的账户上运行上述代码得到的值才是唯一正确答案


用MQL5代码防止此错误(面向EA开发者)

正确的设计思路不是「出错后再修」,而是在下单前就将SL/TP夹紧到经纪商要求的最小距离以上,从根源上避免触发10016

下单前验证并夹紧SL/TP

// 将 SL/TP 夹紧到止损位以上的距离后再下单
// 返回值 false = 方向搞反(设计错误),不予下单
bool ClampStops(ENUM_ORDER_TYPE type, double &sl, double &tp)
{
   double point   = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   int    digits  = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
   long   stopsPt = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   double spread  = SymbolInfoDouble(_Symbol, SYMBOL_ASK)
                  - SymbolInfoDouble(_Symbol, SYMBOL_BID);
   // 止损位 + 点差的余量(应对 stops=0 动态判定型经纪商)
   double minDist = stopsPt * point + spread;

   double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);

   if(type == ORDER_TYPE_BUY)
   {
      // BUY 的 SL/TP 以 Bid 为基准判定
      if(sl > 0 && sl >= bid) return false;              // 方向搞反
      if(tp > 0 && tp <= bid) return false;
      if(sl > 0 && (bid - sl) < minDist) sl = bid - minDist;
      if(tp > 0 && (tp - bid) < minDist) tp = bid + minDist;
   }
   else if(type == ORDER_TYPE_SELL)
   {
      // SELL 的 SL/TP 以 Ask 为基准判定
      if(sl > 0 && sl <= ask) return false;              // 方向搞反
      if(tp > 0 && tp >= ask) return false;
      if(sl > 0 && (sl - ask) < minDist) sl = ask + minDist;
      if(tp > 0 && (ask - tp) < minDist) tp = ask - minDist;
   }
   sl = NormalizeDouble(sl, digits);
   tp = NormalizeDouble(tp, digits);
   return true;
}

要点有三个。

  1. 每次都动态获取 SYMBOL_TRADE_STOPS_LEVELSYMBOL_POINT(做到品种、经纪商无关)
  2. 加上点差的余量(即使止损位为0的动态判定型经纪商也更容易通过)
  3. 最后务必用 NormalizeDouble(价格, _Digits) 统一小数位数(多余的小数位也会导致被拒绝)

单独处理 retcode 10016

被拒绝时,将「发送了什么、当时的距离是多少」记录到日志中,就能一眼看出属于原因①〜⑥中的哪一种。

MqlTradeRequest req; MqlTradeResult res;
ZeroMemory(req); ZeroMemory(res);
// ... 组装 req(sl/tp 已经过 ClampStops 处理) ...
if(!OrderSend(req, res) || res.retcode != TRADE_RETCODE_DONE)
{
   if(res.retcode == TRADE_RETCODE_INVALID_STOPS)   // 10016
      PrintFormat("Invalid stops: sl=%s tp=%s bid=%s stopsLevel=%d",
                  DoubleToString(req.sl, _Digits),
                  DoubleToString(req.tp, _Digits),
                  DoubleToString(SymbolInfoDouble(_Symbol, SYMBOL_BID), _Digits),
                  (int)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL));
   else
      PrintFormat("OrderSend failed: retcode=%d, lastError=%d", res.retcode, GetLastError());
}

修改追踪止损前先检查冻结距离

// 修改持仓前,确认触发价是否已进入冻结区间
bool CanModify(double triggerPrice)
{
   long freeze = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_FREEZE_LEVEL);
   if(freeze <= 0) return true;
   double dist = MathAbs(SymbolInfoDouble(_Symbol, SYMBOL_BID) - triggerPrice);
   return (dist > freeze * SymbolInfoDouble(_Symbol, SYMBOL_POINT));
}

若是MT4版EA,可用 MarketInfo(Symbol(), MODE_STOPLEVEL) / MODE_FREEZELEVEL 获取相同的值,思路完全一致。

FXEA365发布的EA均标准实现了这套下单前SL/TP验证、止损位夹紧、品种无关的动态获取机制,因此即使更换经纪商或品种,也不会因Invalid stops而卡住。


优先级检查清单

优先级检查项对策
🚨 首先SL/TP与当前价格的距离是否 < 止损位加大SL/TP幅度/实现夹紧逻辑
🚨 首先BUY/SELL的SL/TP方向是否搞反输出实际数值到日志并目视确认
⚠️ 其次只有修改失败 → 是否处于冻结距离内跳过临近触发时的修改
⚠️ 其次sl/tp中是否误填了距离(点数)转换为绝对价格 price ± dist*_Point
✅ 确认是否为需要后置设置SL/TP的市价执行账户下单→PositionModify 的两段式处理
🛠 开发是否动态获取品种规格实现上述 ClampStops

总结

  • Invalid stopsMT5中为10016(TRADE_RETCODE_INVALID_STOPS),在MT4中为130(ERR_INVALID_STOPS)。编号不同,但含义与对策相通。
  • 成因大多不是计算错误,而是止损位不足、方向搞反、冻结距离、价格与点数混淆、SL/TP需后置的规则、品种特有特性这6种。
  • 开发者可通过「下单前读取 SYMBOL_TRADE_STOPS_LEVEL 进行夹紧 + NormalizeDouble」实现根治。止损位为0的经纪商,也可用点差余量来吸收。

错误代码的完整列表请参考 MQL5 / MT5 错误代码处理指南。同样常见的下单被拒错误「资金不足」,可参考 ERR_NO_MONEY(134/10019)解析文章


常见问题

Q: 明明SL/TP的计算应该没问题,却出现了Invalid stops,这是为什么?

很可能不是数值本身的问题,而是被距当前价格的距离拒绝了。低于经纪商最小止损距离(止损位)的SL/TP,即使数值准确也会被一律拒绝。请在规格窗口或通过 SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL) 确认最小距离。

Q: 10016 与 130 有什么区别?

10016(TRADE_RETCODE_INVALID_STOPS)是MT5中 OrderSend() / PositionModify() 的结果代码,130(ERR_INVALID_STOPS)是MT4中 GetLastError() 返回的值。只是平台不同,含义(SL/TP位置无效)与对策是一致的。

Q: 平时不会出现,但只在行情发布时会出现Invalid stops。

原因是点差扩大。BUY的SL/TP以Bid为基准判定、SELL以Ask为基准判定,因此点差扩大的瞬间距离会不足。即使是止损位为0的经纪商,也可能在急变时动态拒绝。请在SL/TP距离上加上点差的余量(参见正文代码)。

Q: 不带SL/TP能通过,带SL/TP就被拒绝。

可能是市价执行(Market Execution)类账户的服务器不接受新开仓请求中携带SL/TP。请改为先不带SL/TP下单,成交后再通过 PositionModify() 设置。同时务必加入SL设置失败时的重试机制,以及持续失败时的立即平仓保护。

Q: 在EURUSD上运行正常的EA,用在黄金上却频繁出现Invalid stops。

黄金、股指的止损位通常比主要货币对设置得更大,小数位数也不同。请根据品种规格加大SL/追踪止损幅度,或改为基于ATR的波动率联动幅度。

📧 涨价预告 + 免费5天邮件课程

所有EA目前均为首发价,并将随销量阶梯式上调。在每次涨价前收到通知,另有每日一封邮件:自动交易本质、正确解读回测、券商选择技巧。

※ 严格保护隐私。可随时取消订阅。

评论与提问