◆ New — Strategy Validator, Code Fixer & Prop Compliance Checker are live. Try one free →
Home / Learn / How to Fix OrderSend Error 130 (Invalid Stops) in MQL4
MQL4

How to Fix OrderSend Error 130 (Invalid Stops) in MQL4

A practical guide to diagnosing and fixing MetaTrader 4's OrderSend error 130 (ERR_INVALID_STOPS): stops placed inside the broker's MODE_STOPLEVEL distance, prices measured from the wrong side of the spread, unnormalized doubles, and ECN accounts that reject stops attached to a market order. Includes a copy-paste validation helper and the two-step OrderSend-then-OrderModify pattern. This is educational material only, not financial advice — correct order mechanics make an EA's risk explicit, they do not make it profitable, and trading carries a real risk of loss.

What does OrderSend error 130 actually mean?

Error 130 is ERR_INVALID_STOPS: the trade server rejected your order because the stop-loss, take-profit, or pending-order open price violates the broker's rules for where those levels may sit. In almost every case one of four things happened — the stop is too close to the current price (inside the broker's minimum stop distance), the stop is on the wrong side of the price, the price value was not normalized to the symbol's digits, or you are on an ECN-type account that refuses stops attached directly to a market OrderSend().

The error is raised server-side, which is why it feels arbitrary: the exact same EA can run cleanly on one broker and throw 130 on another, or run for weeks and then fail during a news spike. The rules it violates are per-symbol, per-broker properties that you must query at runtime rather than hard-code.

The canonical first move is to print everything the server saw:

4
int ticket = OrderSend(Symbol(), OP_BUY, 0.10, Ask, 3, sl, tp, "ea", 12345, 0, clrBlue);
if(ticket < 0)
   Print("OrderSend failed, error ", GetLastError(),
         "  Ask=", DoubleToString(Ask, Digits),
         "  Bid=", DoubleToString(Bid, Digits),
         "  SL=",  DoubleToString(sl, Digits),
         "  TP=",  DoubleToString(tp, Digits),
         "  StopLevel=", MarketInfo(Symbol(), MODE_STOPLEVEL));

With that one log line you can usually see the violation immediately — a stop-loss two points from Bid on a symbol whose stop level is 20, or a take-profit below the market on a buy. The rest of this guide walks each cause and its fix, ending with a defensive helper you can drop into any MQL4 EA.

Cause 1: stops inside the broker's minimum distance (MODE_STOPLEVEL)

Most brokers enforce a minimum distance between the current market price and any stop-loss, take-profit, or pending-order price. In MQL4 you read it with MarketInfo(Symbol(), MODE_STOPLEVEL) — the value is in points, so multiply by Point to get a price distance. On a 5-digit EUR/USD feed a stop level of 20 points is 2 pips; hard-coding "2 pips" in your EA breaks the moment you attach it to a symbol where the broker demands 50.

The subtlety that trips people up is which side of the spread the check uses. In MT4, a buy opens at Ask but closes at Bid, so the server validates a buy's SL and TP against Bid: Bid - SL >= stopLevel and TP - Bid >= stopLevel. A sell opens at Bid but closes at Ask, so the checks mirror: SL - Ask >= stopLevel and Ask - TP >= stopLevel. An EA that computes a buy stop as Ask - 20 * Point on a wide-spread symbol can violate the Bid-side check even though the arithmetic "looks" right — and spreads widen dramatically around news, which is exactly when EAs mysteriously start throwing 130.

The robust pattern is to clamp your desired stop to the legal boundary:

4
RefreshRates();
double minDist = MarketInfo(Symbol(), MODE_STOPLEVEL) * Point;
double slBuy   = NormalizeDouble(MathMin(desiredSl, Bid - minDist), Digits);
double tpBuy   = NormalizeDouble(MathMax(desiredTp, Bid + minDist), Digits);

Call RefreshRates() first inside any loop or long-running start()/OnTick() logic so Bid/Ask are current, not stale from the top of the tick. Note also MODE_FREEZELEVEL: a separate zone around the price inside which existing orders may not be modified — a frequent cause of related failures in trailing-stop code.

Educational note: clamping keeps orders legal; it also means your effective risk differs from the intended one. Log when clamping happens rather than hiding it.

Cause 2: unnormalized prices — NormalizeDouble and Digits

The second classic cause is sending a price with floating-point residue. Compute Ask - 20 * Point and the double you get may be 1.1050300000000001. Some trade servers reject a price that does not sit exactly on the symbol's price grid, and the error they return is 130. The fix is mechanical: every price you pass to OrderSend() or OrderModify() — open price, SL, and TP — goes through NormalizeDouble(price, Digits), where Digits is the built-in for the current chart's symbol (use MarketInfo(sym, MODE_DIGITS) when trading another symbol).

4
double sl = NormalizeDouble(Ask - slPoints * Point, Digits);
double tp = NormalizeDouble(Ask + tpPoints * Point, Digits);

A related unit bug lives in the 4-digit versus 5-digit broker split. Legacy MQL4 code written for 4-digit quotes assumed 1 point = 1 pip. On a 5-digit feed, Point is ten times smaller, so "20 points" silently becomes 2 pips — which then lands inside the broker's stop level and triggers 130. The standard defensive adjustment computes a pip factor once:

4
double pip = Point;
if(Digits == 3 || Digits == 5)
   pip = Point * 10;   // 5-digit / 3-digit broker: 1 pip = 10 points
double sl = NormalizeDouble(Ask - slPips * pip, Digits);

Decide explicitly whether your EA's inputs mean pips or points, name them accordingly, and convert in one place. Mixed units are the kind of bug that passes a quick demo test on one broker and fails live on another — precisely the class of silent breakage a pre-flight audit of your MQL4 source should flag before real money is anywhere near it.

Cause 3: ECN accounts — the two-step OrderSend then OrderModify pattern

On many ECN/STP-style accounts the broker fills market orders raw and refuses SL/TP values passed inside OrderSend() itself — even values that respect the stop level. Confusingly, MODE_STOPLEVEL on such accounts often reports 0, so the distance checks from earlier all pass and you still get error 130. The accepted pattern is two-step: send the market order naked (SL = 0, TP = 0), then immediately attach the stops with OrderModify():

4
// Two-step entry for ECN-type accounts
double slDist = 200 * Point;   // desired stop distance, in points
double tpDist = 400 * Point;

int ticket = OrderSend(Symbol(), OP_BUY, 0.10, Ask, 3, 0, 0, "ea", 12345, 0, clrBlue);
if(ticket < 0)
   Print("OrderSend failed: ", GetLastError());
else if(OrderSelect(ticket, SELECT_BY_TICKET))
  {
   double sl = NormalizeDouble(OrderOpenPrice() - slDist, Digits);
   double tp = NormalizeDouble(OrderOpenPrice() + tpDist, Digits);
   if(!OrderModify(ticket, OrderOpenPrice(), sl, tp, 0, clrBlue))
      Print("OrderModify failed: ", GetLastError(),
            " — position is OPEN and UNPROTECTED, handle this!");
  }

Two honest caveats. First, the stops are computed from OrderOpenPrice() — the actual fill, not the Ask you requested — so slippage doesn't skew the intended distance. Second, and more important: between the OrderSend() and a successful OrderModify(), the position exists with no stop at all, and the modify can itself fail (requotes, freeze level, disconnect). Production code must treat a failed modify as an emergency — retry with a short loop, and if stops cannot be attached, consider closing the position rather than leaving it unprotected. An EA that logs the failure and carries on has a risk-management hole that no backtest will ever reveal, because the tester's modify never fails.

A defensive pre-flight check you can paste into any EA

Rather than patching each 130 as it appears, validate stops before sending them. This helper encodes the Bid/Ask asymmetry and the stop-level rule in one place:

4
// Returns true when sl/tp are legal for a market order of the given type.
// Pass 0 for "no stop" / "no target".
bool StopsValid(int type, double sl, double tp)
  {
   RefreshRates();
   double minDist = MarketInfo(Symbol(), MODE_STOPLEVEL) * Point;
   if(type == OP_BUY)
      return (sl == 0 || Bid - sl >= minDist)
          && (tp == 0 || tp - Bid >= minDist);
   if(type == OP_SELL)
      return (sl == 0 || sl - Ask >= minDist)
          && (tp == 0 || Ask - tp >= minDist);
   return false;
  }

Call it after normalizing and before OrderSend(); when it returns false, log the values and either clamp to the boundary or skip the trade — an explicit, audited decision instead of a server rejection. For pending orders, the same minDist also constrains the distance between the current price and the requested open price (Ask for buy-stops/limits, Bid for sell-side), so extend the helper accordingly if you trade pendings.

Finally, a word on why this matters beyond tidiness. The MT4 strategy tester almost never surfaces error 130 the way live servers do: it uses a fixed spread, never widens stop levels around news, and never requotes. So an EA can produce a clean backtest while carrying order-placement code that will fail live — trades silently skipped, or worse, positions opened without protection. That gap between tester behaviour and server behaviour is exactly where an independent audit of the order pipeline earns its keep. None of this is financial advice: fixing error 130 makes an EA's execution correct, and correct execution is a precondition for meaningful testing — it says nothing about whether the strategy has an edge. Trading carries a real risk of loss.

Key takeaways

Educational only — not financial advice. Trading involves substantial risk of loss.

Catch the bug that compiles.Run auditGet Pro