DEV Community

Gueta Quant
Gueta Quant

Posted on Originally published at guetaquant.com

cTrader cBot Risk Engine: High-Precision C# Dynamic Stop-Loss Architecture

Static stop-losses break across volatility regimes. A fixed 20-pip stop that is conservative on EUR/USD during Asia session is noise-level on XAU/USD during New York open. In cTrader Automate (C#), the robust pattern is a dedicated risk engine: signal logic decides direction, the engine decides size and stop distance from live volatility.

This post documents the architecture — educational, no trade signals, no return claims.


1. Separate Sizing From Signals

The most common cBot flaw is inline risk math inside OnBar(): volume computed next to entry conditions, stop distance hardcoded. When volatility doubles, the same stop gets hunted and the same volume carries twice the dollar risk.

Split the cBot into two units:

  • Signal module — produces direction only (e.g., breakout, trend filter).
  • Risk engine — given direction, computes stop distance from ATR and volume from a fixed monetary budget.
// RiskEngine.cs — pure logic, no cTrader dependencies (unit-testable)
public static class RiskEngine
{
    public static double StopDistancePrice(double atr, double multiplier)
    {
        return atr * multiplier;
    }

    public static long VolumeInUnits(double riskMoney, double stopDistancePrice,
                                     double tickValuePerUnit, double pipSize,
                                     long volumeMin, long volumeMax, long volumeStep,
                                     out bool clampedToMin, out double effectiveRisk)
    {
        double raw = riskMoney / (stopDistancePrice / pipSize * tickValuePerUnit);
        long floored = (long)Math.Floor(raw / volumeStep) * volumeStep;
        clampedToMin = false;
        if (floored < volumeMin) { floored = volumeMin; clampedToMin = true; }
        long finalVolume = Math.Min(floored, volumeMax);
        effectiveRisk = finalVolume * (stopDistancePrice / pipSize * tickValuePerUnit);
        return finalVolume;
    }
}
Enter fullscreen mode Exit fullscreen mode

Note the clampedToMin flag: when the budgeted volume falls below the broker minimum, the engine clamps up and reports the effective risk, which now exceeds the budget. Silently reporting the original budget while carrying higher exposure is the exact failure we dissected in The Silent Position Sizer Bug — never repeat it.

2. ATR-Based Dynamic Stops

Average True Range adapts the stop to current volatility. A 1.5x–2.0x ATR stop keeps the exit outside normal noise on any symbol and any session:

private double GetAtrStopPips(int atrPeriod = 14, double multiplier = 2.0)
{
    var atr = Indicators.AverageTrueRange(atrPeriod, MovingAverageType.Exponential);
    double stopPrice = atr.Result.LastValue * multiplier;
    return stopPrice / Symbol.PipSize;
}
Enter fullscreen mode Exit fullscreen mode

Because the stop distance is measured, not assumed, the same cBot behaves sanely on a 0.0001-pip Forex pair and a 2-decimal metal without retuning magic numbers.

3. Wiring It Into the cBot

Volume in cTrader is expressed in units (100,000 units = 1.0 standard lot on FX). Always read the broker limits from the symbol — never hardcode them:

protected override void OnBar()
{
    if (Positions.Count > 0) return; // single-position example
    if (!GetSignal()) return;

    double stopPips = GetAtrStopPips();
    double stopPrice = stopPips * Symbol.PipSize;

    long volume = RiskEngine.VolumeInUnits(
        RiskMoney, stopPrice, Symbol.TickValue / Symbol.TickSize * Symbol.PipSize,
        Symbol.PipSize, Symbol.VolumeInUnitsMin,
        Symbol.VolumeInUnitsMax, Symbol.VolumeInUnitsStep,
        out bool clamped, out double effectiveRisk);

    if (clamped)
        Print("WARNING: volume clamped to broker minimum. " +
              "Effective risk {0} exceeds budget {1}.", effectiveRisk, RiskMoney);

    ExecuteMarketOrder(TradeType.Buy, SymbolName, volume,
                       "atr-risk", stopPips, null);
}
Enter fullscreen mode Exit fullscreen mode

4. Trailing Without Chasing

Move the stop only when price has banked a multiple of the ATR distance — ratcheting on every tick bleeds to spread:

private void TrailAtr(Position position, double atrDistancePrice)
{
    double newStop = Symbol.Bid - 1.0 * atrDistancePrice; // long example
    if (newStop > position.StopLoss + Symbol.PipSize)
        ModifyPosition(position, newStop, position.TakeProfit);
}
Enter fullscreen mode Exit fullscreen mode

5. Validation Checklist (Demo First)

  1. Run 60 days on demo across at least two volatility regimes (e.g., Forex + metal).
  2. Log every clamp event; if clamps exceed ~5% of trades, the risk budget is too small for the account — top up or widen the stop, never silence the warning.
  3. Backtest with real-tick data; reject any result you cannot reproduce forward on demo.

Educational content under Colombia SFC Decreto 2555 de 2010: this is financial-engineering study material, not investment advice. No signals, no managed accounts.

By **Mahdi Goodarzi* (g.dev/mahdigoodarzi), Gueta Quant — interactive risk tools at guetaquant.com. Companion guide: cTrader Copy setup in Colombia.*

Top comments (0)