While widening your Stop Loss protects you from spread spikes, it introduces a critical variable into your risk management: variable capital exposure.
If your base Stop Loss is 10 pips, trading 1 standard lot (100,000 units) risks roughly $100. If an ATR spike widens your dynamic Stop Loss to 14 pips, executing that same 1 standard lot now risks $140. If you do not account for this, your bot will take disproportionately large losses during high-volatility market regimes.
To maintain a flat risk profile (e.g., risking exactly 1% of account equity per trade), you must mathematically reduce your volumeInUnits in real-time to offset the widened finalSlPips before triggering ExecuteMarketOrder.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
An ATR-based trailing stop acts as a dynamic "ratchet." Instead of trailing price by a fixed number of pips—which often leads to premature stop-outs during volatile spikes—it trails price by a multiple of the current Average True Range. When volatility expands, the stop automatically gives the trade more breathing room. When volatility contracts, the stop tightens.
The most critical engineering rule for algorithmic trailing stops is the Modification Threshold. You must only send a modification request to the broker server if the new Stop Loss improves the current one by a meaningful minimum distance (e.g., 1 full pip). Without this, the bot will hammer the broker's server with modification requests on every micro-tick, leading to API throttling or account suspension.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
using System;
using cAlgo.API;
using cAlgo.API.Indicators;
using cAlgo.API.Internals;
namespace cAlgo.Robots
{
[Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
public class AtrTrailingStop : Robot
{
[Parameter("Trailing Label", Group = "Position Filters", DefaultValue = "MyAlgo")]
public string TargetLabel { get; set; }
[Parameter("ATR Timeframe", Group = "ATR Settings")]
public TimeFrame AtrTimeFrame { get; set; }
[Parameter("ATR Periods", Group = "ATR Settings", DefaultValue = 14)]
public int AtrPeriods { get; set; }
[Parameter("ATR Multiplier", Group = "ATR Settings", DefaultValue = 2.0, MinValue = 0.1)]
public double AtrMultiplier { get; set; }
[Parameter("Min Update Step (Pips)", Group = "Server Safety", DefaultValue = 1.0)]
public double MinUpdatePips { get; set; }
private AverageTrueRange _atr;
private double _minUpdateDistance;
protected override void OnStart()
{
// Initialize ATR on the explicitly selected timeframe, regardless of the chart the bot is attached to
_atr = Indicators.AverageTrueRange(MarketData.GetBars(AtrTimeFrame), AtrPeriods, MovingAverageType.Simple);
// Convert the pip setting to raw price value for fast mathematical comparison in OnTick
_minUpdateDistance = MinUpdatePips * Symbol.PipSize;
}
protected override void OnTick()
{
// Loop only through open positions that match this specific bot's label and symbol
var openPositions = Positions.FindAll(TargetLabel, SymbolName);
foreach (var position in openPositions)
{
ManageAtrTrailingStop(position);
}
}
private void ManageAtrTrailingStop(Position position)
{
// 1. Calculate the dynamic trail distance based on the latest closed ATR bar
double currentAtr = _atr.Result.Last(1);
double trailDistance = currentAtr * AtrMultiplier;
if (position.TradeType == TradeType.Buy)
{
// 2. Calculate the proposed new SL (Current Bid minus the ATR buffer)
double proposedSl = Symbol.Bid - trailDistance;
proposedSl = Math.Round(proposedSl, Symbol.Digits);
// 3. The Ratchet Logic: Only move the SL UP.
// If there is no SL, or the new SL is higher than the current SL by at least our safety threshold:
if (!position.StopLoss.HasValue || (proposedSl - position.StopLoss.Value >= _minUpdateDistance))
{
// Execute asynchronously to prevent thread-blocking if managing multiple positions
ModifyPositionAsync(position, proposedSl, position.TakeProfit);
}
}
else if (position.TradeType == TradeType.Sell)
{
// 2. Calculate the proposed new SL (Current Ask plus the ATR buffer)
double proposedSl = Symbol.Ask + trailDistance;
proposedSl = Math.Round(proposedSl, Symbol.Digits);
// 3. The Ratchet Logic: Only move the SL DOWN.
if (!position.StopLoss.HasValue || (position.StopLoss.Value - proposedSl >= _minUpdateDistance))
{
ModifyPositionAsync(position, proposedSl, position.TakeProfit);
}
}
}
}
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Asynchronous Modification (ModifyPositionAsync): Standard ModifyPosition() forces the bot's main thread to pause and wait for the broker to confirm the modification before moving to the next line of code. If you are trailing 5 positions simultaneously, network latency will cause the bot to miss ticks. ModifyPositionAsync fires the request and instantly moves on, maintaining tick-level precision.
Timeframe Decoupling: The ATR calculation explicitly asks for AtrTimeFrame rather than using the default MarketData.GetBars(TimeFrame). If you are scalping on a 1-minute chart, you might want your trailing stop anchored to the structural volatility of the 15-minute or 1-hour chart to avoid being shaken out by micro-noise.
The Ratchet Condition: The math (proposedSl - position.StopLoss.Value >= _minUpdateDistance) strictly guarantees that the Stop Loss can never widen to give back profits, and it prevents the bot from making hundreds of 0.1-pip adjustments.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
To dynamically size your positions based on a variable ATR Stop Loss, you must calculate the fiat risk amount and divide it by the monetary cost of the stop loss distance.
In C# cAlgo, you do not need to write complex exchange-rate conversion logic for cross-pairs (e.g., trading EUR/JPY on a USD account). The API natively handles this through the Symbol.PipValue property, which represents the account-currency fiat value of a 1-pip move for exactly 1 unit of volume.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
/// <summary>
/// Calculates the required volume to risk a specific percentage of the account balance.
/// </summary>
/// <param name="riskPercentage">The percentage of the account to risk (e.g., 1.0 for 1%)</param>
/// <param name="stopLossPips">The dynamic ATR Stop Loss distance in pips</param>
/// <returns>Normalized volume in units, safe for broker execution.</returns>
private double CalculateVolumeByRisk(double riskPercentage, double stopLossPips)
{
if (stopLossPips <= 0)
{
Print("Warning: Stop loss must be greater than zero. Returning minimum volume.");
return Symbol.VolumeInUnitsMin;
}
// 1. Calculate the absolute fiat amount you are willing to lose
// Note: Use Account.Equity instead of Account.Balance if you want floating drawdowns factored in
double maxRiskFiat = Account.Balance * (riskPercentage / 100.0);
// 2. Calculate the fiat cost of a 1-pip move for a single unit of volume
double costPerPipPerUnit = Symbol.PipValue;
// 3. Calculate the exact mathematical volume required
double exactVolume = maxRiskFiat / (stopLossPips * costPerPipPerUnit);
// 4. Broker Normalization (The Strict Enforcer)
// We strictly round DOWN. If math dictates 14,800 units, but the broker step is 1,000,
// it rounds to 14,000. Rounding up to 15,000 would exceed the 1% risk limit.
double safeVolume = Symbol.NormalizeVolumeInUnits(exactVolume, RoundingMode.Down);
// 5. Ensure the calculated volume does not fall below the broker's absolute minimum
return Math.Max(safeVolume, Symbol.VolumeInUnitsMin);
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
The RoundingMode.Down Protocol: This is the most critical safeguard in the method. Brokers enforce strict lot size increments (Symbol.VolumeInUnitsStep), usually in increments of 1,000 units (0.01 lots). If your mathematical risk allows for exactly 12,900 units, rounding to the nearest step (13,000) means you will risk slightly more than 1.0%. Forcing RoundingMode.Down ensures you never violate your maximum risk parameter.
Account.Balance vs. Account.Equity: The snippet uses Account.Balance, which calculates risk based on your closed capital. If you frequently hold multiple overlapping trades, change this to Account.Equity. Using Equity ensures that if you are currently in a 5% floating drawdown on other trades, your next position's 1% risk is sized against your true remaining capital, preventing exponential margin decay.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
To implement partial profit-taking, you must track the state of the position in the cBot's memory. If you do not explicitly record that a position has already been scaled out, the bot will continuously slice the remaining volume in half on every micro-tick once the 1R price is breached.
In C# cAlgo, the safest way to manage this state is by storing the Position.Id in a HashSet<int>.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.