FX NOOKAI CODE LAB
Loading

Trailing Stop MQL5 Code: A Working EA Example

By FXNook Lab team

This MQL5 EA uses CTrade to trail both buy and sell positions, filters positions by chart symbol and magic number, and only tightens an existing stop loss. Set the trailing distance and step in points, attach the EA to a chart, and use the Strategy Tester to check that the broker accepts each modification.

The example also checks the symbol’s stop and freeze levels before sending a request, preserves the position’s take-profit, and reports rejected modifications in the Experts log. It is intended as a compact starting point: understand the distance settings and test them on the symbol and account type you plan to use.

Working MQL5 trailing-stop code

Create a new Expert Advisor in MetaEditor, replace its generated contents with the code below, and compile it as an MQL5 file. The EA manages open positions on the chart symbol that have the configured magic number. It does not open trades; another EA or a manual trade must create the position first.

#property strict
#include <Trade/Trade.mqh>

input int InpTrailingDistancePoints = 300;
input int InpTrailingStepPoints     = 50;
input ulong InpMagicNumber          = 123456;

CTrade trade;
datetime g_lastModifyAttempt = 0;

int OnInit()
{
   if(InpTrailingDistancePoints <= 0 || InpTrailingStepPoints < 0)
   {
      Print("Trailing distance must be positive and step cannot be negative.");
      return INIT_PARAMETERS_INCORRECT;
   }
   return INIT_SUCCEEDED;
}

void OnTick()
{
   TrailPositions();
}

double RoundDownToTick(double price, double tickSize, int digits)
{
   double rounded = MathFloor(price / tickSize + 1e-10) * tickSize;
   return NormalizeDouble(rounded, digits);
}

double RoundUpToTick(double price, double tickSize, int digits)
{
   double rounded = MathCeil(price / tickSize - 1e-10) * tickSize;
   return NormalizeDouble(rounded, digits);
}

void TrailPositions()
{
   MqlTick tick;
   if(!SymbolInfoTick(_Symbol, tick))
      return;

   double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
   int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);

   if(point <= 0.0)
      return;
   if(tickSize <= 0.0)
      tickSize = point;

   long stopsLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   long freezeLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_FREEZE_LEVEL);
   double brokerGapPoints = (double)MathMax(stopsLevel, freezeLevel) + 1.0;
   double trailDistance = InpTrailingDistancePoints * point;
   double stepDistance = InpTrailingStepPoints * point;

   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || !PositionSelectByTicket(ticket))
         continue;

      if(PositionGetString(POSITION_SYMBOL) != _Symbol)
         continue;
      if((ulong)PositionGetInteger(POSITION_MAGIC) != InpMagicNumber)
         continue;

      ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
      double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
      double oldSL = PositionGetDouble(POSITION_SL);
      double takeProfit = PositionGetDouble(POSITION_TP);
      double newSL = 0.0;
      double gapPoints = 0.0;
      bool shouldModify = false;

      if(type == POSITION_TYPE_BUY)
      {
         double profitPoints = (tick.bid - openPrice) / point;
         if(profitPoints < InpTrailingDistancePoints)
            continue;

         newSL = RoundDownToTick(tick.bid - trailDistance, tickSize, digits);
         gapPoints = (tick.bid - newSL) / point;
         shouldModify = (oldSL == 0.0 || newSL > oldSL + stepDistance);
      }
      else if(type == POSITION_TYPE_SELL)
      {
         double profitPoints = (openPrice - tick.ask) / point;
         if(profitPoints < InpTrailingDistancePoints)
            continue;

         newSL = RoundUpToTick(tick.ask + trailDistance, tickSize, digits);
         gapPoints = (newSL - tick.ask) / point;
         shouldModify = (oldSL == 0.0 || newSL < oldSL - stepDistance);
      }
      else
      {
         continue;
      }

      if(!shouldModify || gapPoints <= brokerGapPoints)
         continue;

      // Rate-limit retries if the server rejects a modification.
      datetime now = TimeCurrent();
      if(g_lastModifyAttempt != 0 && now - g_lastModifyAttempt < 2)
         continue;

      g_lastModifyAttempt = now;
      bool sent = trade.PositionModify(ticket, newSL, takeProfit);
      uint retcode = trade.ResultRetcode();

      if(!sent || retcode != TRADE_RETCODE_DONE)
      {
         PrintFormat("Trailing stop modify failed. ticket=%I64u retcode=%u (%s)",
                     ticket, retcode, trade.ResultRetcodeDescription());
      }
   }
}

The defaults are examples, not universal settings. Change InpTrailingDistancePoints, InpTrailingStepPoints, and InpMagicNumber in the EA inputs before testing. If the magic number does not match a position’s POSITION_MAGIC, the EA deliberately ignores that position.

How the buy and sell stop calculations work

A buy position is closed at the Bid, so its protective stop is calculated below the current Bid: Bid - trailing distance. A sell position is closed at the Ask, so its stop is calculated above the current Ask: Ask + trailing distance. The code uses SymbolInfoTick to read both prices for the chart symbol.

The EA first checks whether the position has moved at least the configured trailing distance in profit. For a buy, that means Bid minus the open price is at least the distance. For a sell, it means the open price minus Ask is at least the distance. Until that condition passes, the stop is left alone.

Once trailing starts, the stop moves only in a protective direction. A buy stop is changed only when the new value is above the existing stop by more than the configured step; a sell stop is changed only when the new value is below the existing stop by more than the step. An unset stop, whose value is zero, can be set as soon as the profit and broker-distance conditions pass.

This direction check matters when price pulls back. A buy stop that has moved up must not be lowered again, and a sell stop that has moved down must not be raised again. The EA preserves the position’s existing take-profit by passing takeProfit as the third argument to PositionModify.

Set the trailing distance and step correctly

Both inputs use points, not pips or raw price units. The point size is available through SYMBOL_POINT; it depends on the symbol’s quotation format. On a typical five-digit EURUSD quote, one point is 0.00001, so 300 points is 0.00300, often described as 30 pips. On a three-digit JPY quote, one point is commonly 0.001, so 300 points is 0.300, also commonly 30 pips.

Those pip conversions are common conventions, not a substitute for reading the symbol specification. Some instruments use different tick sizes or quotation formats. The code rounds the candidate stop to SYMBOL_TRADE_TICK_SIZE as well as normalizing it to the symbol’s digits, because decimal precision alone does not always make a price a valid trading increment.

For a first test, try a distance of 300 points and a step of 50 points on a five-digit forex symbol. The distance controls how far behind the current closing-side price the stop follows. The step controls how much the candidate must improve before the EA sends another modification; a larger step generally means fewer, larger updates.

Trailing does not begin just because a position exists. With a 300-point distance, a buy must first have at least 300 points of profit measured from its open price to Bid, and a sell must have at least 300 points measured from its open price to Ask. If that threshold is never reached, the absence of a trailing-stop change is expected.

Respect broker stop and freeze levels

SYMBOL_TRADE_STOPS_LEVEL reports the broker’s minimum stop distance in points. SYMBOL_TRADE_FREEZE_LEVEL reports a distance around the market where trade operations can be restricted. If a proposed stop is too close, or an operation is inside a freeze area, the server may refuse the modification.

The example compares the candidate stop’s gap from Bid or Ask with the larger of those two levels, plus a one-point margin. If the candidate is too close, it skips the request rather than repeatedly submitting a stop that is expected to be invalid. It also rate-limits modification attempts to one every two seconds across the EA instance, which reduces repeated requests when a server rejects a modification for another reason.

This is a conservative pre-check, not a promise that every request will be accepted. Broker rules, market movement between the check and the request, trading permissions, and account conditions can still affect the result. If the configured trailing distance is smaller than the broker’s required gap, the EA may have no valid candidate at that distance; choose a larger distance or inspect the symbol’s settings.

Use the ticket-based CTrade overload

MQL5 provides PositionModify(symbol, sl, tp) and PositionModify(ticket, sl, tp). This example uses the ticket overload after selecting each position with PositionSelectByTicket. It identifies the exact position being managed, which is particularly important on hedging accounts where several positions can exist for one symbol.

On a netting account, there is normally one position per symbol, but filtering by magic number is still useful when the EA should manage only positions associated with its own trades. The position’s magic number is read from POSITION_MAGIC; the EA does not infer ownership from the chart or from the position comment.

The Boolean return from trade.PositionModify tells you whether the method passed its basic request checks. It does not, by itself, prove that the trade server completed the modification. The code also reads trade.ResultRetcode() and logs the code and description unless it receives TRADE_RETCODE_DONE. A common rejection, TRADE_RETCODE_INVALID_STOPS, means the requested stop is invalid for the current market or broker rules; TRADE_RETCODE_FROZEN indicates a freeze restriction.

Common reasons the trailing stop does not move

CauseWhat to check or change
Wrong symbol or magic numberThe EA filters to _Symbol and InpMagicNumber. Attach it to the position’s symbol and set the input to the position’s actual magic number.
Not enough profit yetThe position must reach the trailing distance in points before the first stop is placed. Compare the open price with Bid for a buy or Ask for a sell.
Distance is too small for the brokerRead the symbol’s stop and freeze levels. Increase the distance if the calculated stop cannot clear the broker gap.
Position is in a freeze areaWait for the market to move far enough from the stop, then let the EA evaluate it again. A frozen modification can be refused even when the EA is running.
Algo Trading or EA trading permission is offEnable Algo Trading and check the EA’s permissions in its Common settings. A program can receive ticks while being unable to send trade requests.
Trade request failedRead the Experts and Journal logs, including the retcode printed by the EA. The server response gives more useful information than the fact that the visible stop did not change.

A compile with no errors only confirms that MetaEditor accepted the source; it does not confirm that the EA will receive eligible positions or that a broker will accept every request. If an EA compiles but does not trade as expected, the diagnostic steps in MT5 EA Not Taking Trades: Checks, Causes, and Fixes can help separate program logic from trading permissions and broker constraints.

Test the code in MetaTrader 5

  1. Open the Strategy Tester with the EA, a symbol, and a test period that produces both buy and sell positions. Since this example manages positions rather than opening them, use an EA or test setup that creates positions with the configured magic number.
  2. Enable visual mode and watch the position’s stop-loss line. Confirm that a buy stop appears below Bid and moves upward, while a sell stop appears above Ask and moves downward.
  3. Run tests on symbols with different digits and tick sizes. Compare the configured point values with the symbol specifications, and look for invalid-stop retcodes in the Experts and Journal tabs.
  4. Try a distance below and above the symbol’s stop or freeze level. The EA should skip candidates that fail its distance pre-check; when the server rejects another request, note the retcode and adjust the cause rather than relying only on the chart.

Visual mode shows the sequence of price and stop changes, while the logs show whether a request was rejected. Test buys and sells separately if the EA that creates positions does not produce both directions during the same run. Past or tested results do not guarantee future results.

Adapting the example to an existing EA

In an existing MQL5 EA, keep the helper functions and inputs, then call TrailPositions() from its OnTick handler. If the EA already has an OnTick, add the call to that function rather than defining a second one. If it manages positions on a timer or uses a different event design, call the trailing routine from the appropriate existing event.

For multiple symbols, replace the chart-only assumptions deliberately: obtain the symbol for each selected position, read its tick, point, digits, tick size, stop level, and freeze level, and calculate the candidate using that symbol’s values. Do not reuse the chart’s Bid, Ask, or point for a position on another instrument. You may also replace the magic-number filter if your EA uses a different ownership rule, but keep an explicit rule so unrelated positions are not modified.

This is MQL5 code. MT4 uses a different trade model: positions and the CTrade class in this example do not transfer directly, and MT4 commonly modifies an order with OrderModify. When porting, account for the platform’s order and position differences instead of copying the MQL5 function calls into an MQL4 file.

If the code compiles but its position filters or trade logic are unclear, FXNook AI Code Lab can check and explain MQL4 or MQL5 files. Its EA debugger is relevant when an EA is not behaving as expected, and Fix MQL4/MQL5 errors is available for compile errors.

Frequently asked questions

How do I trail a stop loss in MQL5?

Read the current Bid and Ask, calculate a stop at the chosen distance, and use CTrade::PositionModify to update the position. For buys, trail below Bid and only raise the stop; for sells, trail above Ask and only lower it. Filter the positions you intend to manage and check broker distance limits before sending modifications.

How do I modify a position's stop loss with CTrade?

Create a CTrade object and call PositionModify(ticket, newSL, existingTP) to target a specific position while keeping its take-profit unchanged. Check both the method’s Boolean result and ResultRetcode(), because a request that passes basic checks can still be rejected by the server.

Why is my MQL5 trailing stop not moving?

Common causes include insufficient profit, a symbol or magic-number filter that excludes the position, a candidate inside the broker’s stop or freeze distance, and disabled trading permissions. Read the EA’s Experts and Journal logs for the trade retcode, then compare the configured point distance with the symbol specification.

Questions

How do I trail a stop loss in MQL5?

Read the current Bid and Ask, calculate a stop at the chosen distance, and use CTrade::PositionModify to update the position. Trail a buy below Bid and only raise its stop; trail a sell above Ask and only lower its stop. Filter the positions you want to manage and account for broker stop and freeze limits.

How do I modify a position's stop loss with CTrade?

Create a CTrade object and call PositionModify(ticket, newSL, existingTP) to modify a specific position while preserving its take-profit. Check the Boolean return value and ResultRetcode(), since the server can reject a request even when the method passes its basic checks.

Why is my MQL5 trailing stop not moving?

The position may not have reached the profit threshold, may not match the EA’s symbol or magic-number filter, or may be too close to the market under broker stop or freeze rules. Check Algo Trading permissions and read the Experts and Journal logs for the trade retcode.