Advertisement IC Markets

Subject: [CODE SHARE] Building an Advanced Custom Trailing Stop in cTrader

Master exponential money management, position sizing calculators, strict daily stop-loss limits, and overcoming FOMO on micro-timeframes.
Post Reply
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Subject: [CODE SHARE] Building an Advanced Custom Trailing Stop in cTrader

Post by PTScalper »

Hey fellow algo traders,

I see a lot of questions popping up around trailing stops in cTrader. While cTrader has a built-in server-side trailing stop (which you can trigger natively via ModifyPosition), it’s pretty basic. It starts trailing immediately based on the distance.

If you want real control—like only activating the trail after reaching a certain profit threshold—you have to code it yourself inside the OnTick() method.

I’ve put together a clean, optimized boilerplate cBot template for an advanced trailing stop. Feel free to copy this into your Automate workspace and tweak it.

The Strategy Logic:

This code doesn't just blindly trail price. It waits until your trade is safely in profit (the ActivationThreshold), and then it starts trailing behind the current price by your defined TrailingDistance.

Code: Select all

using System;
using cAlgo.API;
using cAlgo.API.Internals;

namespace cAlgo.Robots
{
    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class AdvancedTrailingStop : Robot
    {
        // Parameters exposed to the cTrader UI
        [Parameter("Activation Threshold (Pips)", Group = "Trailing Stop", DefaultValue = 15)]
        public double ActivationThreshold { get; set; }

        [Parameter("Trailing Distance (Pips)", Group = "Trailing Stop", DefaultValue = 10)]
        public double TrailingDistance { get; set; }

        [Parameter("Bot Label", Group = "Trade Management", DefaultValue = "MyStrategy")]
        public string BotLabel { get; set; }

        protected override void OnTick()
        {
            // BEST PRACTICE: Only iterate through positions managed by THIS specific bot on THIS symbol
            foreach (var position in Positions.FindAll(BotLabel, SymbolName))
            {
                ManageTrailingStop(position);
            }
        }

        private void ManageTrailingStop(Position position)
        {
            double newStopLoss;

            if (position.TradeType == TradeType.Buy)
            {
                // Calculate current floating profit in pips
                double profitInPips = (Symbol.Bid - position.EntryPrice) / Symbol.PipSize;

                if (profitInPips >= ActivationThreshold)
                {
                    // Calculate where the new SL should be
                    newStopLoss = Symbol.Bid - (TrailingDistance * Symbol.PipSize);
                    
                    // CRITICAL: Ensure we only move the SL UP for a Buy trade, never backwards
                    if (position.StopLoss == null || newStopLoss > position.StopLoss)
                    {
                        // Use Async to avoid blocking the main OnTick thread during fast markets
                        ModifyPositionAsync(position, newStopLoss, position.TakeProfit);
                    }
                }
            }
            else if (position.TradeType == TradeType.Sell)
            {
                // Calculate current floating profit in pips for a short
                double profitInPips = (position.EntryPrice - Symbol.Ask) / Symbol.PipSize;

                if (profitInPips >= ActivationThreshold)
                {
                    newStopLoss = Symbol.Ask + (TrailingDistance * Symbol.PipSize);
                    
                    // CRITICAL: Ensure we only move the SL DOWN for a Sell trade
                    if (position.StopLoss == null || newStopLoss < position.StopLoss)
                    {
                        ModifyPositionAsync(position, newStopLoss, position.TakeProfit);
                    }
                }
            }
        }
    }
}
Expert Notes on the Code
If you are new to the cTrader API, here are a few reasons why this code is structured the way it is:

Positions.FindAll() Filter: You should never just use foreach (var position in Positions). If you are running multiple bots or manual trades, a generic loop will grab everything and apply this trailing stop to trades it shouldn't touch. Passing your bot's specific label prevents cross-contamination.

Bid vs. Ask Math: Notice how we use Symbol.Bid for calculating Buy profit and Symbol.Ask for Sell profit. Because you close a Buy at the Bid price and close a Sell at the Ask price, you must calculate your floating pips using the correct side of the spread.

The Directional Check (newStopLoss > position.StopLoss): A trailing stop should only ever lock in more profit. Without this check, every time price ticks against you, your stop loss would widen. This simple if statement ensures the SL operates like a one-way ratchet.

ModifyPositionAsync: Notice the Async suffix. Modifying a position requires a server round-trip. If you use the standard synchronous ModifyPosition() inside OnTick(), a volatile market could freeze your bot while it waits for server confirmation. Async fires the request to the server and immediately continues executing your code.

Hope this helps some of the newer devs out there! Let me know if you want to see how to add a "Step" function to this so the server isn't spammed with modification requests on every micro-pip movement.

Happy coding, and may the spread be with you.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Recommended broker for automated trading & scalping IC Markets
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: Subject: [CODE SHARE] Building an Advanced Custom Trailing Stop in cTrader

Post by PTScalper »

Plus i made a little bit better and more robust version:

If you run the previous trailing stop on a highly volatile pair during a news event, you might spam your broker's server with hundreds of ModifyPosition requests a second because the Stop Loss is trying to move up by a fraction of a pip on every single tick. Most brokers hate this and will throttle or warn you.

We fix this by introducing a Step (the SL only moves once price has moved a full X pips).

We’ll also add a Break-Even trigger. This ensures that the moment you are decently in profit, your trade becomes risk-free, long before the trailing stop takes over.

The Upgraded C# Code

Code: Select all

using System;
using cAlgo.API;
using cAlgo.API.Internals;

namespace cAlgo.Robots
{
    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class ProTrailingStop : Robot
    {
        [Parameter("Bot Label", Group = "Trade Management", DefaultValue = "MyStrategy")]
        public string BotLabel { get; set; }

        // Break-Even Parameters
        [Parameter("Enable Break-Even", Group = "Break-Even", DefaultValue = true)]
        public bool UseBreakEven { get; set; }

        [Parameter("Break-Even Trigger (Pips)", Group = "Break-Even", DefaultValue = 10)]
        public double BreakEvenTrigger { get; set; }

        [Parameter("Break-Even Buffer (Pips)", Group = "Break-Even", DefaultValue = 1.0, 
            MaxValue = 5, MinValue = 0)]
        public double BreakEvenBuffer { get; set; }

        // Trailing Stop Parameters
        [Parameter("Enable Trailing Stop", Group = "Trailing Stop", DefaultValue = true)]
        public bool UseTrailingStop { get; set; }

        [Parameter("Trail Activation (Pips)", Group = "Trailing Stop", DefaultValue = 20)]
        public double TrailActivation { get; set; }

        [Parameter("Trail Distance (Pips)", Group = "Trailing Stop", DefaultValue = 15)]
        public double TrailDistance { get; set; }

        [Parameter("Trail Step (Pips)", Group = "Trailing Stop", DefaultValue = 3.0)]
        public double TrailStep { get; set; }


        protected override void OnTick()
        {
            foreach (var position in Positions.FindAll(BotLabel, SymbolName))
            {
                if (UseBreakEven) 
                    CheckBreakEven(position);

                if (UseTrailingStop) 
                    ManageTrailingStop(position);
            }
        }

        private void CheckBreakEven(Position position)
        {
            // If the position already has a stop loss that is better than (or equal to) the entry, exit.
            if (position.StopLoss != null)
            {
                if (position.TradeType == TradeType.Buy && position.StopLoss >= position.EntryPrice) return;
                if (position.TradeType == TradeType.Sell && position.StopLoss <= position.EntryPrice) return;
            }

            double profitInPips = position.TradeType == TradeType.Buy 
                ? (Symbol.Bid - position.EntryPrice) / Symbol.PipSize 
                : (position.EntryPrice - Symbol.Ask) / Symbol.PipSize;

            if (profitInPips >= BreakEvenTrigger)
            {
                double newStopLoss = position.TradeType == TradeType.Buy
                    ? position.EntryPrice + (BreakEvenBuffer * Symbol.PipSize)
                    : position.EntryPrice - (BreakEvenBuffer * Symbol.PipSize);

                Print("Break-Even Triggered for Position PID: {0}", position.Id);
                ModifyPositionAsync(position, newStopLoss, position.TakeProfit);
            }
        }

        private void ManageTrailingStop(Position position)
        {
            double currentProfitPips = position.TradeType == TradeType.Buy 
                ? (Symbol.Bid - position.EntryPrice) / Symbol.PipSize 
                : (position.EntryPrice - Symbol.Ask) / Symbol.PipSize;

            if (currentProfitPips < TrailActivation) return;

            double newStopLoss;

            if (position.TradeType == TradeType.Buy)
            {
                newStopLoss = Symbol.Bid - (TrailDistance * Symbol.PipSize);
                
                // Only modify if there is no SL, OR if the new SL is higher by at least the Step amount
                if (position.StopLoss == null || newStopLoss >= position.StopLoss + (TrailStep * Symbol.PipSize))
                {
                    ModifyPositionAsync(position, newStopLoss, position.TakeProfit);
                }
            }
            else if (position.TradeType == TradeType.Sell)
            {
                newStopLoss = Symbol.Ask + (TrailDistance * Symbol.PipSize);
                
                // Only modify if there is no SL, OR if the new SL is lower by at least the Step amount
                if (position.StopLoss == null || newStopLoss <= position.StopLoss - (TrailStep * Symbol.PipSize))
                {
                    ModifyPositionAsync(position, newStopLoss, position.TakeProfit);
                }
            }
        }
    }
}
Why These Additions Matter
Let's break down the mechanics of what we just added.

1. The Break-Even Buffer
You'll notice I added BreakEvenBuffer. When you move a trade to break-even, you shouldn't put the Stop Loss at the exact entry price. You need to account for your broker's commission and swap fees. If you get stopped out exactly at your entry price, you will actually lose a small amount of money due to those fees. Adding a 1 or 2 pip buffer ensures a "break-even" trade actually nets out to zero or a few cents in profit.

2. The Early Exit Check in CheckBreakEven()

Code: Select all

if (position.TradeType == TradeType.Buy && position.StopLoss >= position.EntryPrice) return;
This single line is crucial for CPU optimization. If the trade is already at break-even (or better), this instantly kicks out of the method. We don't want the bot calculating floating

3. The Step Logic Calculation

Look at the core of the Trailing Stop update for a Buy trade:profit on every tick if the Stop Loss is already successfully in profit territory.

Code: Select all

newStopLoss >= position.StopLoss + (TrailStep * Symbol.PipSize)
This forces the bot to wait. If the current Stop Loss is at 1.1000 and the TrailStep is 3 pips, the bot will not fire a ModifyPosition request to the server until the newly calculated Stop Loss hits 1.1003 or higher.
This reduces server calls by roughly 90%, keeps your execution logs clean, and prevents broker throttling.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
LondonScalper
Posts: 770
Joined: Sat Sep 05, 2026 7:54 am

Re: Subject: [CODE SHARE] Building an Advanced Custom Trailing Stop in cTrader

Post by LondonScalper »

PTScalper wrote:While cTrader has a built-in server-side trailing stop ... it’s pretty basic. It starts trailing immediately based on the distance.
Custom trailing logic is worth building if you’ve already proved the entry — otherwise you’re polishing the seatbelt on a car with no engine map.

The built-in trail starting immediately is exactly why discretionary scalpers complain: you give back the first clean impulse to noise. A delay until MFE clears a threshold, stepping by structure rather than a fixed distance, or freezing the trail into news, are all reasonable upgrades — on a demo path that matches your live broker.

Caveats from running similar ideas:
  • Client-side trails die when the platform hiccups; know what is server-side
  • Log trail moves vs fills — “smart” trails that nibble can churn costs
  • Don’t let trailing complexity justify holding a bad thesis longer
Code share is useful for the community. I’d still ask: does the trail improve expectancy after costs, or only average winner cosmetics?

Are you trailing on price distance, or on something like a swinging M1 low/high?
PropScalpDesk
Posts: 364
Joined: Sat Sep 19, 2026 7:50 pm

Re: Subject: [CODE SHARE] Building an Advanced Custom Trailing Stop in cTrader

Post by PropScalpDesk »

The step logic helps, but there's a second source of request spam it doesn't catch. ModifyPositionAsync returns straight away, and position.StopLoss doesn't change until the server confirms. In a fast market, several ticks arrive before that confirmation, each one still sees the old stop, and each one sends another modify. CheckBreakEven has the same issue, since its early return depends on the stop already being at entry.

A small guard fixes both:

Code: Select all

// needs: using System.Collections.Generic;
private readonly HashSet<int> _pending = new HashSet<int>();
// before sending:
if (_pending.Contains(position.Id)) return;
_pending.Add(position.Id);
ModifyPositionAsync(position, newStopLoss, position.TakeProfit,
    r => _pending.Remove(position.Id));
One more thing on BreakEvenBuffer: 1 pip covers commission on EURUSD raw pricing, but if someone runs this on XAUUSD, where PipSize is 0.01 at most brokers, a 1-pip buffer is one cent and doesn't cover anything. It might be worth expressing the buffer in account currency per lot rather than pips.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: Subject: [CODE SHARE] Building an Advanced Custom Trailing Stop in cTrader

Post by PTScalper »

LondonScalper wrote: Fri Sep 11, 2026 8:47 pm
PTScalper wrote:While cTrader has a built-in server-side trailing stop ... it’s pretty basic. It starts trailing immediately based on the distance.
Custom trailing logic is worth building if you’ve already proved the entry — otherwise you’re polishing the seatbelt on a car with no engine map.

The built-in trail starting immediately is exactly why discretionary scalpers complain: you give back the first clean impulse to noise. A delay until MFE clears a threshold, stepping by structure rather than a fixed distance, or freezing the trail into news, are all reasonable upgrades — on a demo path that matches your live broker.

Caveats from running similar ideas:
  • Client-side trails die when the platform hiccups; know what is server-side
  • Log trail moves vs fills — “smart” trails that nibble can churn costs
  • Don’t let trailing complexity justify holding a bad thesis longer
Code share is useful for the community. I’d still ask: does the trail improve expectancy after costs, or only average winner cosmetics?

Are you trailing on price distance, or on something like a swinging M1 low/high?
Hi Londonscalper,

The "seatbelt on a car with no engine map" is a perfect analogy. Optimizing a trailing stop before confirming the entry's base edge is completely backward—it usually just obscures a negative expectancy system.

To answer your question: trailing on raw price action structure (like swinging M1 or M15 highs/lows) beats a fixed price distance every time. Fixed pip distances ignore market context and current volatility. If gold or a spot forex pair breaks out, the market breathes. A rigid 10-pip trail gets clipped by a standard, healthy pullback, giving back the impulse to noise. Trailing just beyond the most recent structural swing low respects the microstructure of the move.

You also raised critical points regarding execution. Holding the original stop until Maximum Favorable Excursion (MFE) clears a specific threshold ensures you aren't choking the trade early. Furthermore, pushing the hard SL to the broker's server protects the position if the platform drops, while enforcing a minimum step distance prevents "nibbling" that spams the server and invites slippage.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: Subject: [CODE SHARE] Building an Advanced Custom Trailing Stop in cTrader

Post by PTScalper »

Here is an MQL5 implementation that handles all three of those upgrades: it waits for an MFE threshold, finds the recent structural swing low, and enforces a minimum step distance to eliminate server churn.

Code: Select all

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

// --- Trailing Parameters ---
input int    TrailingActivationPoints = 150; // Delay until MFE clears this (e.g., 15 pips)
input int    SwingLookbackBars        = 5;   // Look back for the M1 structural low/high
input int    BufferPoints             = 20;  // Cushion below the swing low / above the swing high
input double MinTrailStep             = 50;  // Prevent server spam (only move if > 5 pips)

void TrailStopByStructure()
{
    for(int i = PositionsTotal() - 1; i >= 0; i--)
    {
        ulong ticket = PositionGetTicket(i);
        if(PositionGetString(POSITION_SYMBOL) == _Symbol)
        {
            double currentPrice = PositionGetDouble(POSITION_PRICE_CURRENT);
            double openPrice    = PositionGetDouble(POSITION_PRICE_OPEN);
            double currentSL    = PositionGetDouble(POSITION_SL);
            long type           = PositionGetInteger(POSITION_TYPE);
            
            if(type == POSITION_TYPE_BUY)
            {
                // 1. Wait until MFE clears our activation threshold
                if(currentPrice - openPrice > TrailingActivationPoints * _Point)
                {
                    // 2. Find the structural swing low based on raw price action
                    int lowestShift = iLowest(_Symbol, PERIOD_M1, MODE_LOW, SwingLookbackBars, 1);
                    double swingLow = iLow(_Symbol, PERIOD_M1, lowestShift);
                    
                    double newSL = NormalizeDouble(swingLow - (BufferPoints * _Point), _Digits);
                    
                    // 3. Ensure the trail only moves UP, and steps by a meaningful amount to avoid churn
                    if(newSL > currentSL + (MinTrailStep * _Point) && newSL < currentPrice)
                    {
                        trade.PositionModify(ticket, newSL, PositionGetDouble(POSITION_TP));
                    }
                }
            }
            else if(type == POSITION_TYPE_SELL)
            {
                // 1. Wait until MFE clears our activation threshold
                if(openPrice - currentPrice > TrailingActivationPoints * _Point)
                {
                    // 2. Find the structural swing high
                    int highestShift = iHighest(_Symbol, PERIOD_M1, MODE_HIGH, SwingLookbackBars, 1);
                    double swingHigh = iHigh(_Symbol, PERIOD_M1, highestShift);
                    
                    double newSL = NormalizeDouble(swingHigh + (BufferPoints * _Point), _Digits);
                    
                    // 3. Ensure the trail only moves DOWN, and steps by a meaningful amount
                    if((newSL < currentSL - (MinTrailStep * _Point) || currentSL == 0) && newSL > currentPrice)
                    {
                        trade.PositionModify(ticket, newSL, PositionGetDouble(POSITION_TP));
                    }
                }
            }
        }
    }
}
This logic directly answers the expectancy vs. cosmetics question. By ignoring the first wave of noise and locking in behind real structural walls, you stop cutting the tails off your outsized winners, which is where true positive expectancy actually comes from after trading costs.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
LondonScalper
Posts: 770
Joined: Sat Sep 05, 2026 7:54 am

Re: Subject: [CODE SHARE] Building an Advanced Custom Trailing Stop in cTrader

Post by LondonScalper »

Structure over fixed distance, agreed, with one condition from my own testing: the swing has to come from the timeframe the entry came from. Trailing an M5 entry behind M1 swing lows looks sensible and behaves like a tight fixed stop, because M1 on gold prints a new low every few minutes.

On the code, 150 points as the activation threshold is 15 pips on a five-digit pair but only $1.50 on XAUUSD at most brokers, which is nothing. I'd express the activation as a multiple of the initial risk instead, say 1R, so the same input means the same thing on every symbol.

The minimum step is the part I'd keep exactly as written. Before I had one, a trail on a quiet morning was modifying the order every few seconds, and a couple of those modifications were rejected at the worst possible moment. I'd also put a buffer of at least the current spread beyond the swing low, otherwise the stop sits exactly where everyone else's does.
Post Reply