MT5 trailing stop: how it works and why it stops working

7 min readMetaKit

A trailing stop is a stop loss that follows price in your favour and never moves back. Long EURUSD at 1.0850 with a 30-pip trail: price rises to 1.0900, the stop rises to 1.0870; price drops back to 1.0880, the stop stays at 1.0870. It locks in profit on the way up and gets you out when the move turns.

MetaTrader 5 has one built in. It also has a limitation that catches out almost every trader who relies on it, usually on a day it matters.

How to set a trailing stop in MT5

  1. Open the Trade tab in the Toolbox (Ctrl+T).
  2. Right-click the open position.
  3. Choose Trailing Stop, then a distance in points (15, 20, 50…) or Custom to enter your own.

A point is the smallest price increment, so on a 5-digit EURUSD quote 300 points is 30 pips; on gold quoted to 2 digits, 300 points is $3.00. The distance must be at least the symbol's stops level (in the symbol specification), or MT5 can't place the stop it's trying to move.

The trailing stop doesn't activate immediately. MT5 waits until the position is in profit by the trailing distance, then starts moving the stop. With a 300-point trail on a long at 1.0850, nothing happens until price reaches 1.0880; from then on the stop follows 300 points behind.

Why your MT5 trailing stop stops working

Here's the part the menu doesn't tell you. The MT5 trailing stop runs in your terminal, not on the broker's server. MetaQuotes says so in the platform documentation: the terminal watches the price and sends a "modify stop loss" request each time it needs to move the stop.

So the trailing stop only trails while that terminal is open, logged in and connected:

  • Close MetaTrader, and the trail stops.
  • Shut the laptop lid, lose Wi-Fi, or let Windows restart for an update, and the trail stops.
  • The phone app doesn't help: trailing stops set on desktop aren't run by MT5 mobile.

What remains on the server is the last stop loss the terminal set. That still protects you, it just stops following price. A trade that ran 100 pips in your favour overnight and came all the way back can close at the stop from when you went to bed, not the one you thought you'd have.

This is why "my trailing stop didn't work" is one of the most common MT5 complaints. It worked exactly as designed. It just needed a computer that never sleeps.

Your options for a trailing stop that doesn't sleep

1. A Windows VPS running MT5 24/7. The traditional answer: rent a VPS, install MT5, log in, set the trailing stop there. It works, but you're now maintaining a Windows machine, and the trail still dies if that terminal crashes or the VPS reboots. The no-VPS automation post covers the trade-offs.

2. An Expert Advisor. A trailing-stop EA does the same job with more options (ATR-based, breakeven first, step trailing). It still needs a running terminal, so it inherits the same problem.

3. Trail from code over an API. Your account runs in an always-on terminal and a small script moves the stop. This is the "server-side" trailing stop: the logic runs somewhere that isn't your laptop, and the stop it sets lives on the broker's server like any other.

A server-side trailing stop over the MetaKit API

When an account is connected to MetaKit, its terminal runs in the cloud and stays logged in. The API exposes the two things a trailing stop needs: read open positions with their current price, and modify a position's stop. The script below trails every open position on an account. It can run anywhere Python runs: a $5 Linux box, a container, a Raspberry Pi.

"""Trail the stop loss on every open position, 24/7."""
import os
import time
 
import requests
 
BASE = "https://api.metakit.cloud/v1"
ACCOUNT = int(os.environ["METAKIT_ACCOUNT"])
TRAIL_PIPS = 30          # distance behind price
STEP_PIPS = 5            # only move the stop in steps of at least this much
POLL_SECONDS = 10
 
s = requests.Session()
s.headers["Authorization"] = f"Bearer {os.environ['METAKIT_KEY']}"
specs: dict[str, dict] = {}
 
 
def spec(symbol: str) -> dict:
    """Symbol spec, cached: digits and stops_level rarely change."""
    if symbol not in specs:
        r = s.get(f"{BASE}/accounts/{ACCOUNT}/symbols/{symbol}", timeout=15)
        r.raise_for_status()
        specs[symbol] = r.json()
    return specs[symbol]
 
 
def pip(sp: dict) -> float:
    # A pip is 10 points on 5- and 3-digit quotes, 1 point otherwise.
    point = 10 ** -sp["digits"]
    return point * 10 if sp["digits"] in (3, 5) else point
 
 
def trail_once():
    r = s.get(f"{BASE}/accounts/{ACCOUNT}/positions", params={"limit": 100}, timeout=15)
    r.raise_for_status()
    for p in r.json()["data"]:
        sp = spec(p["symbol"])
        size = pip(sp)
        trail, step = TRAIL_PIPS * size, STEP_PIPS * size
        min_gap = sp["stops_level"] * 10 ** -sp["digits"]
        price = p["priceCurrent"]          # bid for a long, ask for a short
        long = p["type"] == "buy"
 
        # Don't start trailing until the trade is in profit by the trail distance.
        in_profit = (price - p["priceOpen"]) if long else (p["priceOpen"] - price)
        if in_profit < trail:
            continue
 
        target = price - max(trail, min_gap) if long else price + max(trail, min_gap)
        target = round(target, sp["digits"])
        current = p["sl"] or 0
 
        # Only ever move the stop in your favour, and only in meaningful steps.
        better = (target > current + step) if long else (current == 0 or target < current - step)
        if not better:
            continue
 
        resp = s.patch(
            f"{BASE}/accounts/{ACCOUNT}/positions/{p['ticket']}",
            json={"sl": target},
            # Same key for the same move: a retried request can't double-apply.
            headers={"Idempotency-Key": f"trail-{p['ticket']}-{target}"},
            timeout=20,
        )
        if resp.ok:
            print(f"{p['symbol']} #{p['ticket']}: SL {current} -> {target}")
        else:
            print(f"{p['symbol']} #{p['ticket']}: {resp.status_code} {resp.json()['error']['code']}")
 
 
while True:
    try:
        trail_once()
    except requests.RequestException as e:
        print("network error, retrying:", e)
    time.sleep(POLL_SECONDS)

It needs a full API key on a full slot, because moving a stop is a trade action; the scopes post explains why read-only keys can't. Three design choices worth copying even if you write your own:

  • It only ratchets. The stop moves toward profit, never back. A bug here would turn a trailing stop into a widening stop, which is worse than none.
  • It moves in steps. Re-modifying the stop on every half-pip tick spams the broker and gains nothing. A 5-pip step means a handful of modifications per trade.
  • It respects stops_level. The API refuses a stop inside the broker's minimum distance with 422 invalid_stops, so the target is never placed closer than that.

The ten-second poll is a sensible default for swing and intraday trades. The trail runs at "close enough" rather than tick-perfect, which is fine: the stop itself sits on the broker's server and triggers on the exact price, only the moving of it is periodic.

ATR trailing stops: let volatility set the distance

A fixed 30-pip trail is too tight for gold on a news day and too loose for EURUSD in the Asian session. An ATR trailing stop sizes the distance from recent volatility instead, typically 2 to 3 times the 14-period Average True Range on your trading timeframe.

You can compute ATR from candles in the same script:

def atr(symbol: str, timeframe: str = "H1", period: int = 14) -> float:
    r = s.get(f"{BASE}/accounts/{ACCOUNT}/candles",
              params={"symbol": symbol, "timeframe": timeframe, "count": period + 1}, timeout=15)
    bars = r.json()["data"]
    trs = [max(b["high"] - b["low"], abs(b["high"] - a["close"]), abs(b["low"] - a["close"]))
           for a, b in zip(bars, bars[1:])]
    return sum(trs) / len(trs)

Then use trail = 2.5 * atr(p["symbol"]) instead of a fixed pip count. Cache it per symbol for the length of a bar; volatility doesn't change every ten seconds.

Trailing stop mistakes to avoid

  • Trailing too tight. A trail smaller than the normal noise of the pair gets you stopped out of good trades. Look at the average bar range first.
  • Trailing from the entry tick. Starting the trail before the trade has room to breathe turns it into a very tight fixed stop. Activate it once the trade is in profit, as both MT5 and the script above do.
  • Forgetting the spread. A long's stop triggers on the bid; a short's on the ask. Around rollover the spread can jump enough to hit a tight trail without price really moving. The spread post covers when.
  • Assuming it's running. If you rely on the built-in MT5 trailing stop, the terminal has to be on. That's the whole post in one line.

If your account is on a prop firm challenge, a trail that locks in profit is also a drawdown tool: see the prop firm drawdown post for how trailing drawdown rules are measured.