彻底解决 Invalid stops(10016/130)— MT5/MT4的SL设置问题
目录
- Invalid stops 是什么(10016 与 130 的区别)
- ① TRADE_RETCODE_INVALID_STOPS = 10016(MT5 / `OrderSend()` 的返回代码)
- ② ERR_INVALID_STOPS = 130(MT4 / `GetLastError()`)
- 先花30秒做初步排查
- 成因与对策(6种模式)
- ① SL/TP过于接近当前价格(小于止损位)
- ② SL/TP方向搞反(BUY/SELL混淆)
- ③ 在冻结区间内修改订单/持仓
- ④ 价格与点数(距离)混淆
- ⑤ 市价执行经纪商无法在开仓时设置SL/TP
- ⑥ 品种特有的特性(黄金、指数的止损位较大)
- 经纪商之间的差异(注意事项)
- 用MQL5代码防止此错误(面向EA开发者)
- 下单前验证并夹紧SL/TP
- 单独处理 retcode 10016
- 修改追踪止损前先检查冻结距离
- 优先级检查清单
- 总结
- 常见问题
- Q: 明明SL/TP的计算应该没问题,却出现了Invalid stops,这是为什么?
- Q: 10016 与 130 有什么区别?
- Q: 平时不会出现,但只在行情发布时会出现Invalid stops。
- Q: 不带SL/TP能通过,带SL/TP就被拒绝。
- Q: 在EURUSD上运行正常的EA,用在黄金上却频繁出现Invalid stops。
彻底解决 Invalid stops(10016/130)
运行EA时,如果Expert标签页出现 Invalid stops 或 OrderSend 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
实际使用时的区分方法:
| 平台 | 获取来源 | 值 | 出现场景 |
|---|---|---|---|
| MT5 | MqlTradeResult.retcode | 10016(TRADE_RETCODE_INVALID_STOPS) | OrderSend / PositionModify 被服务器拒绝 |
| MT5 | CTrade.ResultRetcode() | 10016 | 通过 CTrade 下单/修改被拒绝 |
| MT4 | GetLastError() | 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之间的间隔会拉开,因此平时能通过的距离,也可能在行情发布时或清晨突然被拒绝。
对策:
- 在规格窗口确认止损位,并将EA的SL/TP、追踪止损幅度设置得比该值更宽
- 在EA端下单前先做夹紧处理(见下文代码)
- 如果确实需要很紧的SL,可考虑更换止损位较小的经纪商或账户类型
② SL/TP方向搞反(BUY/SELL混淆)
症状: 特定方向(只做多或只做空)必定出现Invalid stops。这是自制EA首次测试中最常见的模式。
原因: 规则很简单,BUY的SL必须低于当前价格(Bid)、TP必须高于;SELL的SL必须高于当前价格(Ask)、TP必须低于。将BUY的计算公式复制到SELL时忘记改符号、把 price - sl 和 price + sl 弄混,是典型的错误,服务器会立即返回10016/130。
对策:
- 出错时用
PrintFormat("type=%s sl=%.5f tp=%.5f bid=%.5f ask=%.5f", ...)输出实际数值,目视确认方向 - 将BUY/SELL的SL/TP计算整合为共用函数,把符号分支集中到一处(避免复制粘贴产生分支)
③ 在冻结区间内修改订单/持仓
症状: 新开仓没有问题,但只有在TP即将触发、SL即将触发、挂单即将触发之前的修改/取消被拒绝。
原因: 在设置了 SYMBOL_TRADE_FREEZE_LEVEL 的品种上,当当前价格接近触发价(TP/SL/挂单的触发价格)到一定距离内,该订单的修改/取消会被冻结。这是为防止成交处理与修改请求冲突而设的服务器规则,常表现为EA的追踪止损「在TP即将触发前想再更新一次却被拒绝」。
对策:
- 修改前先读取
SYMBOL_TRADE_FREEZE_LEVEL,若距触发价的距离小于等于冻结距离,则跳过本次修改 - 加大追踪止损的更新间隔和更新幅度,减少临近触发时的无效修改请求
- 即使被拒绝也不致命(临近触发=即将成交),可以设计为吞掉错误只记录日志即可
④ 价格与点数(距离)混淆
症状: SL中直接填入了「本意是距离」的数值,如 50 或 0.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倍,从而被压缩到止损位以下而被拒绝。
对策:
- SL/TP务必以
NormalizeDouble(price ± dist * _Point, _Digits)的形式组装 - 用注释明确输入参数的单位(pips / points),并将
_Point换算逻辑集中到一处
⑤ 市价执行经纪商无法在开仓时设置SL/TP
症状: 不带SL/TP能成交,但带SL/TP的新开仓单却出现Invalid stops。尤其常见于ECN/市价执行(Market Execution)类账户。
原因: 市价执行模式下,「请求时的价格」与「实际成交价格」会有偏差,因此部分服务器不接受新开仓请求中携带SL/TP,而要求成交后通过修改持仓的方式设置(这是MT4时代ECN账户频发错误130的经典规则,MT5上仍有服务器保留同样的行为)。
对策:
- 改为先不带SL/TP下单 → 确认成交后再用
PositionModify()(若用CTrade则为trade.PositionModify())设置SL/TP的两段式处理 - 这种方式会出现「下单成功但SL设置失败」的瞬间,因此必须加入SL设置失败则重试,达到规定次数仍失败就立即平仓的保护机制(放任无SL持仓是最坏的结果)
- 可通过
SymbolInfoInteger(_Symbol, SYMBOL_TRADE_EXEMODE)确认执行方式
⑥ 品种特有的特性(黄金、指数的止损位较大)
症状: 同一个EA在EURUSD上正常运行,一旦用于XAUUSD(黄金)或股指CFD,就频繁出现Invalid stops。
原因: 止损位是按品种单独设置的,黄金、指数、小众货币对通常比主要货币对设置得更大。如果直接套用为主要外汇品种调校的紧凑SL/追踪止损幅度,就会达不到该品种的最小距离而被拒绝。小数位数也因品种而异(黄金通常为2〜3位),因此硬编码 _Digits 的代码同样会出问题。
对策:
- 更换品种后,务必在规格窗口确认止损位与小数位数
- 将SL/TP幅度改为基于ATR等波动率指标而非固定点数,可以更好地跨品种适应
- 代码中始终动态获取
_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;
}
要点有三个。
- 每次都动态获取
SYMBOL_TRADE_STOPS_LEVEL与SYMBOL_POINT(做到品种、经纪商无关) - 加上点差的余量(即使止损位为0的动态判定型经纪商也更容易通过)
- 最后务必用
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 stops在 MT5中为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目前均为首发价,并将随销量阶梯式上调。在每次涨价前收到通知,另有每日一封邮件:自动交易本质、正确解读回测、券商选择技巧。
※ 严格保护隐私。可随时取消订阅。