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);
}
}
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.
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.
