Page 1 of 1

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

Posted: Tue Aug 11, 2026 10:06 pm
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.

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

Posted: Tue Aug 11, 2026 10:10 pm
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.