Home > Blog > Invalid Stops in MT5: Fix Retcode 10016 & MT4 Error 130

MT5MQL5ErrorTroubleshootingEA

Invalid Stops in MT5: Fix Retcode 10016 & MT4 Error 130

Published: 2026-07-07Read time: about 7 min
This article reflects information as of its publish date. EA performance figures (PF, DD, annual return) change with live trading and re-validation — check the latest on the EA pages. See the latest EA results

Invalid Stops in MT5 (10016 / 130)

When your EA logs Invalid stops or OrderSend error 130, the instinct is to double-check your SL/TP math. Don't start there — in most cases the numbers are fine, and the order is being rejected for breaking the broker's minimum-distance rules. The stop is too close to the current price, on the wrong side of it, or you're trying to modify an order inside a frozen zone.

This is a single, complete reference for Invalid stops, aimed at both traders running EAs on MT5/MT4 and MQL5 developers writing them — what the error really means, the 6 root causes, a 30-second triage, and the code that prevents it permanently. For the full list of error codes, see the complete MT5 error-code guide.

This article targets MT5 (build 4xxx) as of July 2026. Actual stop-level values vary by broker and symbol.


What "Invalid stops" means (10016 vs 130)

There are two different codes for an invalid stop, depending on the platform generation. Knowing which one you're looking at speeds up the diagnosis considerably.

① TRADE_RETCODE_INVALID_STOPS = 10016 (MT5, OrderSend() return code)

In MQL5, the result of OrderSend() lands in MqlTradeResult.retcode. When the SL/TP in the request (or a pending order's price relative to them) violates the server's rules, the trade server rejects it with 10016 (TRADE_RETCODE_INVALID_STOPS).

Meaning : Invalid stops in the request
Constant: TRADE_RETCODE_INVALID_STOPS
Value   : 10016
// Typical log output
2026.07.07 09:15:32.441 EA_NAME XAUUSD,M5: OrderSend error: retcode=10016 (invalid stops)

② ERR_INVALID_STOPS = 130 (MT4, GetLastError())

In the MT4/MQL4 world, a failed OrderSend() / OrderModify() followed by GetLastError() returns 130 (ERR_INVALID_STOPS). The classic OrderSend error 130 in the Experts tab is exactly this. If you run MT4 builds of your EAs, this is the code you'll see.

Meaning : invalid stops
Constant: ERR_INVALID_STOPS
Value   : 130

How to tell them apart:

PlatformSourceValueWhen it appears
MT5MqlTradeResult.retcode10016 (TRADE_RETCODE_INVALID_STOPS)OrderSend / PositionModify rejected by server
MT5CTrade.ResultRetcode()10016rejection via CTrade
MT4GetLastError()130 (ERR_INVALID_STOPS)after a failed OrderSend / OrderModify

Different numbers, same meaning, same fixes. One code often confused with it: 10015 (TRADE_RETCODE_INVALID_PRICE) means the order price itself is invalid — a separate problem. 10016 is strictly about where your stops sit.


30-second triage

Open Market Watch → right-click the symbol → Specification and read two fields:

Stops level  : minimum distance (in points) SL/TP must keep from the current price
Freeze level : distance (in points) inside which pending/near-trigger orders can't be modified

Then put the SL/TP you tried to send next to the current Bid/Ask in the log:

  • SL or TP closer to the current price than the stops level → this is the cause in most cases (cause ①).
  • A buy with SL above Bid, or TP below it (mirrored for sells) → direction mix-up (cause ②).
  • Only modifications of an open position fail → freeze level, or a set-stops-after-entry server (causes ③ / ⑤).
  • SL logged as something like 50.00000 that clearly isn't a price → price/points confusion (cause ④).

One line of code gets you both values:

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

Causes & fixes (6 patterns)

① SL/TP too close to the current price (below the stops level)

Symptom: frequent with tight-stop scalpers and small trailing steps. Manually placing an SL "right next to" the price also gets rejected.

Cause: each symbol carries a broker-defined SYMBOL_TRADE_STOPS_LEVEL — a minimum distance in points. Any SL/TP or pending price closer than that is rejected outright. The reference price matters too: a buy position's SL/TP is validated against the Bid, a sell's against the Ask. When the spread widens, a distance that passes all day suddenly fails during news or the early-Asia session.

Fix:

  1. Check the stops level in Specification and widen the EA's SL/TP and trailing distances to at least that value
  2. In code, clamp before sending (snippet below)
  3. If your strategy genuinely needs tighter stops, look for a broker/account type with a smaller stop level

② SL/TP on the wrong side of the price (buy/sell mix-up)

Symptom: the error fires only in one direction (all buys, or all sells). The single most common bug in a freshly written EA.

Cause: the rule is simple — a buy's SL must be below the current price (Bid) and TP above it; a sell's SL above the current price (Ask) and TP below it. Copying the buy-side formula to the sell branch and forgetting to flip a sign, or swapping price - dist and price + dist, gets an instant 10016/130 from the server.

Fix:

  1. On error, dump real values: PrintFormat("type=%s sl=%.5f tp=%.5f bid=%.5f ask=%.5f", ...) — the wrong side is obvious at a glance
  2. Compute SL/TP in one shared function with a single buy/sell branch instead of duplicated copy-paste blocks

③ Modifying inside the freeze level

Symptom: new orders are fine, but modifications right before TP/SL is hit, or just before a pending order triggers, get rejected.

Cause: on symbols with a non-zero SYMBOL_TRADE_FREEZE_LEVEL, once the market comes within that distance of an activation price (TP, SL, or a pending trigger), the order is frozen — no modify, no delete. It's a server-side guard against a modification racing the execution. In practice it surfaces as a trailing stop that "fails to update one last time" right before TP.

Fix:

  1. Read SYMBOL_TRADE_FREEZE_LEVEL before each modification and skip the update when the trigger price is inside the frozen zone
  2. Widen the trailing step/interval so the EA isn't spamming modifications near the trigger
  3. This rejection is benign — if the price is that close, the level is about to execute anyway. Log it and move on.

④ Price vs points confusion (absolute price vs distance)

Symptom: the log shows sl=50.00000 or sl=0.00500 — a distance, not a price.

Cause: MqlTradeRequest.sl / .tp take an absolute price, not "50 points below entry". An EA that manages stops as distances must convert with entry ± distance * _Point before filling the request.

The related classic: pips vs points on 3/5-digit quotes. On a 5-digit broker, 1 pip = 10 points. If a parameter called "SL 50" is interpreted as points when the author meant pips, the actual distance shrinks 10×, drops below the stops level, and gets rejected.

Fix:

  1. Always build stops as NormalizeDouble(price ± dist * _Point, _Digits)
  2. State the unit (pips or points) in every input comment and do the _Point conversion in exactly one place

⑤ Market-execution brokers that want SL/TP set after entry

Symptom: the order fills fine without SL/TP, but the identical request with SL/TP attached returns Invalid stops. Typical on ECN / Market Execution accounts.

Cause: with market execution, the fill price can differ from the price at request time, so some servers refuse SL/TP inside the opening request and expect you to set them on the position afterwards. This is the classic reason MT4 ECN accounts spat out error 130 years ago, and some MT5 servers still behave the same way.

Fix:

  1. Switch to a two-step flow: send the entry without SL/TP, confirm the fill, then attach them with PositionModify() (trade.PositionModify() with CTrade)
  2. This creates a dangerous moment — an open position with no stop. Retry the modification until it succeeds, and close the position outright if it keeps failing. An unprotected position is the worst possible outcome.
  3. You can check the execution mode via SymbolInfoInteger(_Symbol, SYMBOL_TRADE_EXEMODE)

⑥ Symbol-specific quirks (gold and indices have bigger stop levels)

Symptom: the EA runs cleanly on EURUSD but sprays Invalid stops the moment you attach it to XAUUSD or an index CFD.

Cause: the stop level is set per symbol, and gold, indices, and exotics typically get much larger values than FX majors. A tight SL or trailing width tuned for a major simply doesn't reach the minimum distance on those symbols. Digit counts differ too (gold often quotes with 2–3 digits), so hardcoded _Digits assumptions break as well.

Fix:

  1. Whenever you change symbols, re-check the stops level and digits in Specification
  2. Size SL/TP from volatility (e.g. ATR) instead of fixed points — it survives symbol changes
  3. Always read _Point / _Digits / SYMBOL_TRADE_STOPS_LEVEL dynamically; never hardcode them

Broker notes

Stop and freeze levels differ per broker and per symbol — the same EA with the same settings can run for months on broker A and fail daily on broker B.

The trap to know about: brokers that report a stops level of 0. Zero usually means "validated dynamically", not "no restriction". Ultra-tight stops sail through in quiet markets, then get rejected the moment spreads widen around news or the daily rollover. If your Invalid stops appears "only occasionally", this is almost always the mechanism.

Always check on your actual account:

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

We deliberately don't quote specific broker numbers here — they change and vary by account type. The value returned on your own account is the only one that counts.


MQL5 code to prevent it (for EA developers)

The right design isn't "handle the error" — it's clamping SL/TP to the broker's minimum distance before OrderSend so 10016 never happens.

Validate and clamp stops before ordering

// Clamp SL/TP to at least the broker's minimum stop distance.
// Returns false when a stop is on the wrong side (a logic bug) — don't send.
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);
   // stop level plus a spread buffer (covers "stop level 0" dynamic brokers)
   double minDist = stopsPt * point + spread;

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

   if(type == ORDER_TYPE_BUY)
   {
      // a buy's SL/TP is validated against the Bid
      if(sl > 0 && sl >= bid) return false;              // wrong side
      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)
   {
      // a sell's SL/TP is validated against the Ask
      if(sl > 0 && sl <= ask) return false;              // wrong side
      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;
}

Three things make this robust:

  1. SYMBOL_TRADE_STOPS_LEVEL and SYMBOL_POINT are read dynamically every time — the code works on any symbol at any broker
  2. A spread buffer is added on top — this is what absorbs the "stop level 0 but rejects in fast markets" brokers
  3. Everything is finished with NormalizeDouble(price, _Digits) — stray decimal digits are their own rejection cause

Handle retcode 10016 explicitly

When a rejection does slip through, logging what you sent and what the distance was at that moment identifies which of the 6 causes you hit.

MqlTradeRequest req; MqlTradeResult res;
ZeroMemory(req); ZeroMemory(res);
// ... build req (sl/tp already passed through 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());
}

Check the freeze level before trailing modifications

// Before modifying a position, make sure the trigger price isn't frozen
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));
}

For MT4 builds, the same values come from MarketInfo(Symbol(), MODE_STOPLEVEL) and MODE_FREEZELEVEL. The logic is identical.

The EAs distributed by FXEA365 ship with this pre-trade stop validation, stop-level clamping, and fully symbol-agnostic spec reads built in, so they don't stall on Invalid stops when you switch brokers or symbols.


Priority checklist

PriorityCheckFix
🚨 firstSL/TP distance < stops levelwiden stops / implement the clamp
🚨 firstSL/TP on the wrong side for buy/selllog real values, fix the sign
⚠️ nextonly modifications fail → freeze levelskip updates near the trigger
⚠️ nexta distance passed where a price belongsconvert with price ± dist*_Point
✅ checkmarket-execution server wants stops after entryorder first, then PositionModify
🛠 devsymbol specs hardcodedread specs dynamically, use ClampStops

Summary

  • Invalid stops is 10016 (TRADE_RETCODE_INVALID_STOPS) on MT5 and 130 (ERR_INVALID_STOPS) on MT4 — different numbers, same meaning, same fixes.
  • It's rarely a math bug: the 6 real causes are stops below the minimum distance, wrong-side SL/TP, the freeze level, price/points confusion, set-stops-after-entry servers, and per-symbol quirks.
  • Developers kill it permanently by reading SYMBOL_TRADE_STOPS_LEVEL and clamping before OrderSend, plus NormalizeDouble — with a spread buffer to absorb "stop level 0" brokers.

For all error codes, see the complete MT5 error-code guide. For the other classic order rejection — insufficient margin — see ERR_NO_MONEY (134 / 10019) explained.


FAQ

Q: My SL/TP math checks out, so why Invalid stops?

Because the server rejects on distance from the current price, not correctness. Any stop closer than the broker's minimum stop distance is refused even if the value is precise. Check the stops level in the Specification window or via SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL).

Q: What's the difference between 10016 and 130?

10016 (TRADE_RETCODE_INVALID_STOPS) is the MT5 result code from OrderSend() / PositionModify(); 130 (ERR_INVALID_STOPS) is what MT4's GetLastError() returns. Only the platform differs — the meaning (badly placed stops) and the fixes are the same.

Q: It only happens during news releases. Why?

Spread widening. A buy's stops are validated against the Bid and a sell's against the Ask, so a wider spread eats into your distance. Brokers reporting a stop level of 0 also switch to dynamic rejection in fast markets. Add a spread buffer on top of the stop level (see the code above).

Q: Orders without SL/TP go through, but with SL/TP they're rejected.

That points to a market-execution server that doesn't accept stops inside the opening request. Send the entry without SL/TP, then attach them with PositionModify() after the fill — and make sure you retry on failure and close the position if the stop can't be set. Never leave a position unprotected.

Q: The EA works on EURUSD but fails constantly on gold.

Gold and index CFDs usually carry much larger stop levels than FX majors, and quote with different digit counts. Widen the SL/trailing distances to match the symbol's specification, or better, derive them from ATR so they scale automatically.

5-Day Email Course (Free)

Get one email a day covering the essentials of FX automated trading, how to read backtests correctly, and tips for choosing a broker.

* Privacy strictly protected. You can unsubscribe at any time.