Trailing stops in MQL5: how to lock in profit without choking your winners

Trader in a dark room watching a green uptrend climb across a monitor

I once had a long on GBPJPY that was up about 70 pips. Nice trade. I’d bolted on a trailing stop the night before, set it to follow 12 pips behind price, and went to bed feeling clever. By morning the position was closed for a 9-pip gain, and the pair had run another 180 pips in my direction without me. The trailing stop did exactly what I told it to: it strangled the trade on the first pullback. That’s the whole problem with trailing stops in one story – set them too tight and they turn your best trades into your most frustrating ones.

This is the follow-up to the risk-management core from the last post. Once you size positions properly and place a sane initial stop, the next question is what to do when a trade actually works. A trailing stop is the usual answer, and it’s also where a lot of EAs quietly bleed performance. So here’s how I build one in MQL5 that protects profit without choking the winner.

What a trailing stop is actually for

The job isn’t to maximise every trade. It’s to convert an open, unrealised gain into something the market can’t fully take back, while still leaving enough slack that normal noise doesn’t knock you out. Those two goals pull against each other. Trail tight and you lock in more but exit early; trail loose and you give the trend room but hand back more on the reversal. There’s no setting that wins both – you’re choosing where on that line to sit, and the right spot depends on how the instrument moves, not on a round number that felt good.

One thing worth saying plainly: the broker does not trail for you. The trailing stop built into the MetaTrader terminal is client-side – it only moves while your terminal is open and connected. An EA-based trail is the same: it runs on OnTick, so if your platform is closed, nothing trails. If you’re serious about letting trades run, the EA lives on a VPS. Otherwise a weekend gap finds your original stop, not the one you imagined was following price.

Don’t trail from the first tick

The most common beginner version starts trailing the instant the position opens. That’s backwards. A trade that’s barely in profit hasn’t earned a tighter stop yet, and trailing immediately just drags your stop up into the noise and gets you flushed on the first wiggle.

Give the trade an activation threshold. Don’t touch the stop until the position is a set distance into profit, and only then start following. On EURUSD I’ll often wait until the trade is up roughly one ATR before the trail switches on, then keep the stop a fixed distance behind. Concrete version: long at 1.1000, activation at 20 pips, trail distance 15 pips. Nothing happens until price prints 1.1020. At that point the stop jumps to 1.1005 – already locking five pips – and from there it follows price up, never down.

input double InpTrailStartPips = 20;   // start trailing after this much profit
input double InpTrailDistPips  = 15;   // keep the stop this far behind price
input double InpTrailStepPips  = 5;    // only move when it improves by at least this

// 1 pip in price terms, correct on 3- and 5-digit quotes
double PipSize()
{
   int    digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
   double point  = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   return (digits == 3 || digits == 5) ? 10 * point : point;
}

That PipSize helper matters more than it looks. On a 5-digit EURUSD feed a “pip” is ten points, not one, and if you mix those up your 15-pip trail becomes a 1.5-pip trail that fires constantly. Brokers quote 3 and 5 digits for most pairs now, so handle it once and stop guessing.

Why your modify requests come back as “no changes”

Here’s the mistake that turns a working idea into a server-throttling mess. The naive trail recalculates a new stop on every tick and sends a modify every time. Most of those new stops are a fraction of a point different from the current one, and MetaTrader answers with retcode 10025 – no changes in request – over and over. Some brokers will also start rejecting your rapid-fire modifies as abusive.

The fix is a step gate. Only move the stop when the improvement clears a minimum distance you decide on, say five pips. Below that, leave it alone. Your stop then climbs in deliberate steps instead of twitching on every tick, and the server stops seeing a flood of pointless requests.

#include <Trade/Trade.mqh>
CTrade trade;

void TrailPosition(ulong ticket)
{
   if(!PositionSelectByTicket(ticket))
      return;

   long   type = PositionGetInteger(POSITION_TYPE);
   double open = PositionGetDouble(POSITION_PRICE_OPEN);
   double sl   = PositionGetDouble(POSITION_SL);
   double tp   = PositionGetDouble(POSITION_TP);   // keep the existing TP

   double pip        = PipSize();
   double point      = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   long   stopsLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   double minDist    = stopsLevel * point;

   if(type == POSITION_TYPE_BUY)
   {
      double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
      if(bid - open < InpTrailStartPips * pip)
         return;                                   // not in profit enough yet

      double newSl = bid - InpTrailDistPips * pip;
      if(bid - newSl < minDist)
         newSl = bid - minDist;                     // respect the broker's stop level

      // move up only, only past breakeven, only if it beats the step
      if(newSl > open && newSl > sl + InpTrailStepPips * pip)
         trade.PositionModify(ticket, NormalizeDouble(newSl, _Digits), tp);
   }
   else if(type == POSITION_TYPE_SELL)
   {
      double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
      if(open - ask < InpTrailStartPips * pip)
         return;

      double newSl = ask + InpTrailDistPips * pip;
      if(newSl - ask < minDist)
         newSl = ask + minDist;

      if(newSl < open && (sl == 0 || newSl < sl - InpTrailStepPips * pip))
         trade.PositionModify(ticket, NormalizeDouble(newSl, _Digits), tp);
   }
}
How a trailing stop ratchets upAn uptrending price line with a trailing stop beneath it that steps up on new highs and holds during pullbacks, never moving down.How a trailing stop ratchets upIt follows price up as new highs print – and never steps back down.pricetrailing stoplocked-in profitEntryTrail activatesholds on the pullbacktrail distance
A trailing stop ratchets up with price and holds on pullbacks – it never moves down.

The side-of-price bug that trails the wrong way

Look closely at which price each branch uses. A long position is closed at the Bid, so a long trails off the Bid. A short is closed at the Ask, so it trails off the Ask. Mixing these up is one of the most common silent bugs in homemade EAs – the stop ends up one spread too generous or too tight, and on a wide-spread pair like GBPJPY that’s the difference between holding the trade and getting clipped. It compiles, it runs, it even mostly works, which is exactly why it’s so easy to ship.

Notice the code also refuses to set a stop worse than breakeven, because of the newSl > open guard on the long side. That bakes in a small free feature: the first time the trail fires, your worst case is already a tiny win, never a loss. I treat that as non-negotiable.

Stops level strikes again, this time on the modify

If you read the risk-management post you’ve met SYMBOL_TRADE_STOPS_LEVEL already. It comes back to bite here too. A trailing stop naturally creeps toward the current price, and the moment your new stop sits closer than the broker’s minimum distance, the modify is rejected with 10016, invalid stops. That’s why the code clamps newSl to minDist before sending it. There’s a sibling, SYMBOL_TRADE_FREEZE_LEVEL, that’s nastier: inside the freeze band you can’t modify the order at all because it’s considered too close to triggering. On a fast move your trail can hit that wall and simply stop updating until price steps back. Worth logging when it happens, so you’re not mystified later.

A tight trail cuts the winner shortAn uptrend with a shakeout dip. A tight trailing stop is hit on the dip and exits early. A looser trailing stop survives the dip and stays in for the rest of the move.A tight trail cuts the winner shortSame trend, two stop distances, two very different exits.pricetight stoploose stopentryTight stopout on the shakeoutLoose stop — survives the dip, rides the run
Tight in red, knocked out on the shakeout. Loose in green, still in for the run.

Letting volatility set the distance

A fixed 15-pip trail is fine until the market changes character. The cleaner approach ties the trail distance to ATR, so the stop sits wide when the pair is thrashing and tightens as things calm down. Swap the fixed distance for an ATR multiple and the rest of the logic – the step gate, the stops-level clamp, the breakeven guard – stays exactly the same.

int atrHandle;   // created once in OnInit with iATR(_Symbol, _Period, 14)

double AtrTrailDistance(double mult)
{
   double atr[];
   if(CopyBuffer(atrHandle, 0, 0, 1, atr) <= 0)
      return 0.0;
   return atr[0] * mult;   // already in price units - do not multiply by pip
}

A variant I like for strong trends is the chandelier exit: instead of trailing a set distance behind the current price, you trail it below the highest high since entry, minus an ATR multiple. It hangs back during pullbacks and only ratchets up when the trade makes a genuinely new high, which keeps you in runners far longer than a price-distance trail. Same plumbing, different anchor.

Breakeven first, then trail

In practice I rarely run a pure trail. I run two stages. Stage one moves the stop to breakeven plus a pip or two once the trade clears a first threshold – that takes the risk off the table. Stage two starts the ATR trail only after a second, larger threshold. The early move protects capital; the later trail protects profit. Folding both into one function is easy, because “breakeven” is nothing more than a trail whose target stop is the entry price, gated by a smaller activation distance.

Calling it without hammering the server

You don’t need to trail on literally every tick. Running the logic once per new bar is calmer, sends far fewer requests, and on most timeframes loses you nothing. A simple new-bar check is enough.

datetime lastBarTime = 0;

bool IsNewBar()
{
   datetime t = iTime(_Symbol, _Period, 0);
   if(t == lastBarTime)
      return false;
   lastBarTime = t;
   return true;
}

void OnTick()
{
   if(!IsNewBar())
      return;
   for(int i = PositionsTotal() - 1; i >= 0; i--)
      TrailPosition(PositionGetTicket(i));
}

There’s a tester trap hiding in bar-based logic. If you read the current, still-forming bar to make decisions, your backtest sees information that didn’t exist live, and the results flatter you. Trail off completed bars, and run the Strategy Tester on “every tick based on real ticks” before you believe any equity curve a trailing EA produces. Stops are exactly where optimistic modelling lies to you most.

What still goes wrong

Trailing on every tick and drowning in 10025 rejections – the first thing I check when an EA feels sluggish.

Trailing off the wrong price side, so longs and shorts behave subtly differently and you can’t work out why one direction underperforms.

Setting the activation threshold smaller than the trail distance, which lets the stop jump above entry before the trade has really moved and produces a string of tiny scratched wins that look fine in the stats and feel awful in practice.

Forgetting the trade context entirely – leaving a trailing EA on a home laptop that sleeps, then wondering why the stop didn’t follow overnight.

The next lever after this is partial exits – scaling out of part of the position at a target while letting a trailing remainder run. That changes the math on both the stop and the position sizing from the previous post, and it’s where exit logic starts to get genuinely interesting. I’ll build that one next. Until then, put this trail on a demo, watch where it pulls you out, and tune the activation and distance to the pair rather than copying my numbers – GBPJPY and EURUSD do not want the same settings, and the trade I lost at the top of this post is proof.

By Forex Real Trader

Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like