Page 2 of 2

Re: Free Tool: Dynamic ATR Stop Loss Calculator for MT4 & MT5 Scalpers

Posted: Sun Sep 20, 2026 9:19 am
by FTtrader
To elevate this to an institutional standard in cTrader, we must leverage your C# background and completely replace the basic DrawStaticText with cTrader’s native WPF-style UI framework.

For a scalper trading raw price action and liquidity sweeps, a static indicator is not enough. We must separate the historical buffer calculations from the live telemetry. In this professional grade version:

Event-Driven Telemetry: Indicator buffers are calculated historically in Calculate(), but the Risk Desk HUD is pushed to OnTick() so your position sizing and status react instantly to live market data without waiting for a new bar.

Dynamic Spread Inclusion: As a scalper, the spread is your execution enemy. This version dynamically calculates the live spread and factors it directly into the stop distance and position sizing calculations to prevent spread-widening from hunting your stop.

WPF-Style UI Overlay: We build a cleanly structured, modular UI panel using Border, StackPanel, and TextBlock controls that updates efficiently via memory references rather than redrawing text objects on every tick.

Re: Free Tool: Dynamic ATR Stop Loss Calculator for MT4 & MT5 Scalpers

Posted: Sun Sep 20, 2026 9:19 am
by FTtrader
Pro level of that ctrader code:

Code: Select all

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

namespace cAlgo
{
    [Indicator(IsOverlay = true, TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class InstitutionalRiskDeskPro : Indicator
    {
        // =========================================================================
        // 1. INPUTS & CONFIGURATION
        // =========================================================================
        [Parameter("Risk Per Trade (%)", Group = "Risk Management", DefaultValue = 1.0, MinValue = 0.1, Step = 0.1)]
        public double RiskPercent { get; set; }

        [Parameter("Max Stop Cap (Pips)", Group = "Risk Management", DefaultValue = 15.0)]
        public double MaxStopPips { get; set; }

        [Parameter("Include Spread in Risk", Group = "Risk Management", DefaultValue = true)]
        public bool IncludeSpread { get; set; }

        [Parameter("ATR Period", Group = "Volatility Engine", DefaultValue = 14)]
        public int AtrPeriod { get; set; }

        [Parameter("ATR Multiplier", Group = "Volatility Engine", DefaultValue = 1.5)]
        public double AtrMult { get; set; }

        [Parameter("Enforce Session", Group = "Session Constraints", DefaultValue = true)]
        public bool UseSession { get; set; }

        [Parameter("Start Hour (UTC)", Group = "Session Constraints", DefaultValue = 8)]
        public int StartHour { get; set; }

        [Parameter("End Hour (UTC)", Group = "Session Constraints", DefaultValue = 12)]
        public int EndHour { get; set; }

        // =========================================================================
        // 2. OUTPUT BUFFERS & STATE
        // =========================================================================
        [Output("Long Valid", LineColor = "#00BFA5", Thickness = 2)]
        public IndicatorDataSeries LongValid { get; set; }

        [Output("Long Invalid", LineColor = "#FF5252", Thickness = 2)]
        public IndicatorDataSeries LongInvalid { get; set; }

        [Output("Short Valid", LineColor = "#00BFA5", Thickness = 2)]
        public IndicatorDataSeries ShortValid { get; set; }

        [Output("Short Invalid", LineColor = "#FF5252", Thickness = 2)]
        public IndicatorDataSeries ShortInvalid { get; set; }

        private AverageTrueRange _atr;
        
        // UI Object References for Tick Updates
        private TextBlock _statusText, _sizeText, _stopText, _spreadText, _sessionText;
        private Border _uiPanel;

        // =========================================================================
        // 3. INITIALIZATION & UI CONSTRUCTION
        // =========================================================================
        protected override void Initialize()
        {
            _atr = Indicators.AverageTrueRange(AtrPeriod, MovingAverageType.Simple);
            ConstructUI();
        }

        private void ConstructUI()
        {
            var grid = new Grid(6, 2)
            {
                BackgroundColor = Color.FromArgb(220, 25, 25, 25),
                ShowGridLines = true,
                Margin = new Thickness(10)
            };

            // Helpers for clean UI generation
            TextBlock AddHeader(string text, int row, int col)
            {
                var tb = new TextBlock { Text = text, ForegroundColor = Color.Gray, Margin = new Thickness(5), FontWeight = FontWeight.Bold };
                grid.AddChild(tb, row, col);
                return tb;
            }
            TextBlock AddValue(int row, int col)
            {
                var tb = new TextBlock { Text = "--", ForegroundColor = Color.White, Margin = new Thickness(5, 5, 10, 5), HorizontalAlignment = HorizontalAlignment.Right };
                grid.AddChild(tb, row, col);
                return tb;
            }

            // Headers
            AddHeader("INSTITUTIONAL RISK DESK", 0, 0);
            var titleRight = AddHeader(SymbolName, 0, 1);
            titleRight.HorizontalAlignment = HorizontalAlignment.Right;
            titleRight.ForegroundColor = Color.White;

            // Rows
            AddHeader("Session Status:", 1, 0);
            _sessionText = AddValue(1, 1);

            AddHeader("Live Spread:", 2, 0);
            _spreadText = AddValue(2, 1);

            AddHeader("Stop Dist (Pips):", 3, 0);
            _stopText = AddValue(3, 1);

            AddHeader("Suggested Volume:", 4, 0);
            _sizeText = AddValue(4, 1);

            AddHeader("VERDICT:", 5, 0);
            _statusText = AddValue(5, 1);
            _statusText.FontWeight = FontWeight.ExtraBold;

            _uiPanel = new Border
            {
                BorderColor = Color.DimGray,
                BorderThickness = new Thickness(1),
                CornerRadius = 3,
                Child = grid,
                HorizontalAlignment = HorizontalAlignment.Right,
                VerticalAlignment = VerticalAlignment.Bottom,
                Margin = new Thickness(0, 0, 20, 40)
            };

            Chart.AddControl(_uiPanel);
        }

        // =========================================================================
        // 4. HISTORICAL BUFFER CALCULATIONS
        // =========================================================================
        public override void Calculate(int index)
        {
            if (index < AtrPeriod) return;

            double atrValue = _atr.Result[index];
            double stopDist = atrValue * AtrMult;
            
            // Historical calculation does not include live spread to keep lines stable
            double stopPips = stopDist / Symbol.PipSize;

            bool withinCap = stopPips <= MaxStopPips;
            DateTime barTime = Bars.OpenTimes[index];
            bool inSession = !UseSession || (barTime.Hour >= StartHour && barTime.Hour < EndHour);
            bool isValid = withinCap && inSession;

            double closePrice = Bars.ClosePrices[index];

            if (isValid)
            {
                LongValid[index] = closePrice - stopDist;
                LongInvalid[index] = double.NaN;
                ShortValid[index] = closePrice + stopDist;
                ShortInvalid[index] = double.NaN;
            }
            else
            {
                LongValid[index] = double.NaN;
                LongInvalid[index] = closePrice - stopDist;
                ShortValid[index] = double.NaN;
                ShortInvalid[index] = closePrice + stopDist;
            }
        }

        // =========================================================================
        // 5. LIVE TICK TELEMETRY & EXECUTION LOGIC
        // =========================================================================
        protected override void OnTick()
        {
            if (double.IsNaN(_atr.Result.LastValue)) return;

            // 1. Gather Live Volatility & Spread
            double liveSpreadPips = Symbol.Spread / Symbol.PipSize;
            double atrDist = _atr.Result.LastValue * AtrMult;
            double atrPips = atrDist / Symbol.PipSize;
            
            // Scalpers must account for spread in their stop distance
            double totalRiskPips = IncludeSpread ? atrPips + liveSpreadPips : atrPips;

            // 2. Budget & Session Validation
            bool withinCap = totalRiskPips <= MaxStopPips;
            DateTime now = Server.Time;
            bool inSession = !UseSession || (now.Hour >= StartHour && now.Hour < EndHour);
            bool isValid = withinCap && inSession;

            // 3. Exact Position Sizing Math
            double riskAmount = Account.Balance * (RiskPercent / 100.0);
            double exactVolume = 0;
            
            if (totalRiskPips > 0 && Symbol.PipValue > 0)
            {
                exactVolume = riskAmount / (totalRiskPips * Symbol.PipValue);
            }

            double normalizedVolume = Symbol.NormalizeVolumeInUnits(exactVolume, RoundingMode.Down);
            if (normalizedVolume < Symbol.VolumeInUnitsMin || !isValid) 
            {
                normalizedVolume = 0;
            }

            // 4. Update UI Components Efficiently
            _sessionText.Text = inSession ? "ACTIVE" : "CLOSED";
            _sessionText.ForegroundColor = inSession ? Color.FromHex("#00BFA5") : Color.Gray;

            _spreadText.Text = $"{Math.Round(liveSpreadPips, 1)} pips";
            
            _stopText.Text = $"{Math.Round(totalRiskPips, 1)}";
            _stopText.ForegroundColor = withinCap ? Color.White : Color.FromHex("#FF5252");

            _sizeText.Text = isValid ? $"{normalizedVolume} Units" : "--";
            
            if (!inSession)
            {
                _statusText.Text = "OUT OF SESSION";
                _statusText.ForegroundColor = Color.Gray;
                _uiPanel.BorderColor = Color.Gray;
            }
            else if (!withinCap)
            {
                _statusText.Text = "REJECT: OVER CAP";
                _statusText.ForegroundColor = Color.FromHex("#FF5252");
                _uiPanel.BorderColor = Color.FromHex("#FF5252");
            }
            else
            {
                _statusText.Text = "EXECUTE: IN BUDGET";
                _statusText.ForegroundColor = Color.FromHex("#00BFA5");
                _uiPanel.BorderColor = Color.FromHex("#00BFA5");
            }
        }
    }
}