Advertisement IC Markets

Sticky-note daily loss limit that faces the screen

Discuss 1-minute to 15-minute price action setups, fading intraday momentum, key support/resistance zones, and proven short-term trading methodologies.
FTtrader
Posts: 954
Joined: Mon Aug 03, 2026 2:43 pm

Re: Sticky-note daily loss limit that faces the screen

Post by FTtrader »

Operational Deployment

Instance: Attach this cBot to a blank, dedicated chart (e.g., a daily chart) so it stays out of the way of your active 1m/5m execution charts.

Asynchronous Execution: The flattener uses ClosePositionAsync and CancelPendingOrderAsync. In a fast-moving scalping environment, you want the cBot firing off the close commands instantly without waiting for the server to confirm each one sequentially.

The Intercept: Because the OnTimer loop runs every 1 second, if you attempt to override the limit by manually hitting "Buy" on the platform after you've hit max loss, the cBot will instantly detect the negative daily balance and kill the new trade in under a second.
Recommended broker for automated trading & scalping IC Markets
FTtrader
Posts: 954
Joined: Mon Aug 03, 2026 2:43 pm

Re: Sticky-note daily loss limit that faces the screen

Post by FTtrader »

We can also leverage cTrader's WPF-style UI framework to build a proper dark-mode, institutional Grid layout rather than placing objects by pixel coordinates.

Here is the Pro Risk Desk cBot for cTrader, engineered with asynchronous flattener methods, strict state management, and real-time environment overrides.

cTrader: Pro Risk Desk (cBot)

Code: Select all

using System;
using System.Linq;
using cAlgo.API;
using cAlgo.API.Internals;
using cAlgo.API.Models;

namespace cAlgo.Robots
{
    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class ProRiskDesk : Robot
    {
        // =========================================================================
        // 1. RISK PARAMETERS (Sunday Prep)
        // =========================================================================
        [Parameter("Daily Loss Limit ($)", Group = "Capital Risk", DefaultValue = 500.0, MinValue = 0, Step = 50)]
        public double MaxDailyLoss { get; set; }

        [Parameter("Value of 1R ($)", Group = "Capital Risk", DefaultValue = 100.0, MinValue = 1, Step = 10)]
        public double R_Value { get; set; }

        [Parameter("Max Trades Per Day", Group = "Behavioral Circuit Breakers", DefaultValue = 10, MinValue = 1)]
        public int MaxTrades { get; set; }

        [Parameter("Max Consecutive Losses", Group = "Behavioral Circuit Breakers", DefaultValue = 3, MinValue = 1)]
        public int MaxStreak { get; set; }

        [Parameter("Flash Chart Override", Group = "Environment", DefaultValue = true)]
        public bool FlashChart { get; set; }

        // =========================================================================
        // 2. STATE TRACKING
        // =========================================================================
        private double _peakDailyEquity;
        private DateTime _currentDay;
        private bool _isHalted;
        private string _haltReason;
        private Color _originalBgColor;
        private bool _alertFired;

        // UI Components
        private Border _mainBorder;
        private TextBlock _uiTitle;
        private Border _titleBorder;
        private TextBlock _uiPnL;
        private TextBlock _uiTargetR;
        private TextBlock _uiDrawdown;
        private TextBlock _uiTrades;
        private TextBlock _uiStreak;

        protected override void OnStart()
        {
            _originalBgColor = Chart.ColorSettings.BackgroundColor;
            ResetDailyState();
            InitializeHUD();
            
            // Sub-second enforcement independent of tick volume
            Timer.Start(TimeSpan.FromSeconds(1));
        }

        protected override void OnStop()
        {
            // Clean up environment on removal
            Chart.ColorSettings.BackgroundColor = _originalBgColor;
            Chart.RemoveControl(_mainBorder);
        }

        protected override void OnTick() { EnforceRiskLimits(); }
        protected override void OnTimer() { EnforceRiskLimits(); }

        // =========================================================================
        // 3. CORE LOGIC & LINQ PARSING
        // =========================================================================
        private void EnforceRiskLimits()
        {
            // Session Rollover
            if (Server.Time.Date != _currentDay)
            {
                ResetDailyState();
            }

            // High-Water Mark Tracking
            _peakDailyEquity = Math.Max(_peakDailyEquity, Account.Equity);
            double peakDrawdown = _peakDailyEquity - Account.Equity;

            // Isolate today's executions
            var tradesToday = History.Where(t => t.ClosingTime >= _currentDay).ToList();
            int totalTrades = tradesToday.Count;

            // PnL Math
            double realizedPnL = tradesToday.Sum(t => t.NetProfit); // NetProfit natively includes swap/commission
            double floatingPnL = Positions.Sum(p => p.NetProfit);
            double totalPnL = realizedPnL + floatingPnL;
            double pnlInR = totalPnL / Math.Max(R_Value, 1);

            // Behavioral Metric: Consecutive Losses using LINQ
            int consecLosses = 0;
            foreach (var trade in tradesToday.OrderByDescending(t => t.ClosingTime))
            {
                if (trade.NetProfit < 0) consecLosses++;
                else break; // Streak broken
            }

            // Halt Triggers
            bool limitHit = totalPnL <= -MaxDailyLoss;
            bool tiltHit = consecLosses >= MaxStreak;
            bool overtradeHit = totalTrades >= MaxTrades;

            if (limitHit || tiltHit || overtradeHit)
            {
                _isHalted = true;
                _haltReason = limitHit ? "HARD LIMIT MET" : (tiltHit ? "TILT: MAX LOSS STREAK" : "OVERTRADING CAP MET");
                
                FlattenAccountAsync();

                if (!_alertFired)
                {
                    Print($"RISK DESK OVERRIDE: {_haltReason}");
                    _alertFired = true;

                    // Physical environment override
                    if (FlashChart)
                    {
                        Chart.ColorSettings.BackgroundColor = limitHit ? Color.FromHex("#3A0000") : Color.FromHex("#2A0033");
                    }
                }
            }

            UpdateHUD(totalPnL, pnlInR, peakDrawdown, totalTrades, consecLosses);
        }

        private void ResetDailyState()
        {
            _currentDay = Server.Time.Date;
            _peakDailyEquity = Account.Equity;
            _isHalted = false;
            _alertFired = false;
            _haltReason = string.Empty;
            Chart.ColorSettings.BackgroundColor = _originalBgColor;
        }

        // =========================================================================
        // 4. ASYNC EXECUTION INTERCEPT
        // =========================================================================
        private void FlattenAccountAsync()
        {
            // Non-blocking async closure is critical for high-frequency scalping safety
            foreach (var pos in Positions)
            {
                ClosePositionAsync(pos);
            }
            foreach (var order in PendingOrders)
            {
                CancelPendingOrderAsync(order);
            }
        }

        // =========================================================================
        // 5. WPF-STYLE UI RENDERING
        // =========================================================================
        private void InitializeHUD()
        {
            _uiTitle = CreateText("RISK DESK: ACTIVE", Color.White, 11, FontWeight.ExtraBold);
            _uiTitle.HorizontalAlignment = HorizontalAlignment.Center;
            _uiTitle.Margin = new Thickness(0, 5, 0, 5);

            _titleBorder = new Border { BackgroundColor = Color.FromHex("#00ACC1"), Child = _uiTitle };

            _uiPnL = CreateValueText("$0.00");
            _uiTargetR = CreateValueText("0.00 R");
            _uiDrawdown = CreateValueText("$0.00");
            _uiTrades = CreateValueText("0 / 0");
            _uiStreak = CreateValueText("0 / 0");

            var layoutPanel = new StackPanel { Orientation = Orientation.Vertical };
            layoutPanel.AddChild(_titleBorder);
            layoutPanel.AddChild(CreateDataRow("Daily PnL", _uiPnL));
            layoutPanel.AddChild(CreateDataRow("Target (R)", _uiTargetR));
            layoutPanel.AddChild(CreateDataRow("Peak Drawdown", _uiDrawdown));
            layoutPanel.AddChild(CreateDataRow("Trades Today", _uiTrades));
            layoutPanel.AddChild(CreateDataRow("Loss Streak", _uiStreak));

            _mainBorder = new Border
            {
                BackgroundColor = Color.FromHex("#131722"),
                BorderColor = Color.FromHex("#2A2E39"),
                BorderThickness = new Thickness(1),
                HorizontalAlignment = HorizontalAlignment.Right,
                VerticalAlignment = VerticalAlignment.Bottom,
                Margin = new Thickness(20),
                Width = 240,
                Child = layoutPanel
            };

            Chart.AddControl(_mainBorder);
        }

        private Grid CreateDataRow(string label, TextBlock valueBlock)
        {
            var lbl = CreateText(label, Color.DarkGray, 11, FontWeight.Normal);
            lbl.HorizontalAlignment = HorizontalAlignment.Left;
            valueBlock.HorizontalAlignment = HorizontalAlignment.Right;
            
            var rowGrid = new Grid(1, 2) { Margin = new Thickness(10, 5, 10, 5) };
            rowGrid.AddChild(lbl, 0, 0);
            rowGrid.AddChild(valueBlock, 0, 1);
            return rowGrid;
        }

        private TextBlock CreateText(string text, Color color, double size, FontWeight weight)
        {
            return new TextBlock
            {
                Text = text,
                ForegroundColor = color,
                FontSize = size,
                FontWeight = weight,
                FontFamily = "Consolas" // Enforces tabular monospacing
            };
        }

        private TextBlock CreateValueText(string text)
        {
            return CreateText(text, Color.White, 11, FontWeight.Bold);
        }

        private void UpdateHUD(double pnl, double pnlInR, double peakDrawdown, int tradesToday, int consecLosses)
        {
            if (_isHalted)
            {
                _uiTitle.Text = _haltReason;
                _titleBorder.BackgroundColor = Color.FromHex("#8B0000"); // Dark Red
                _mainBorder.BackgroundColor = Color.Black;
            }
            else
            {
                _uiTitle.Text = "RISK DESK: ACTIVE";
                _titleBorder.BackgroundColor = Color.FromHex("#00ACC1"); // Teal
            }

            _uiPnL.Text = $"{(pnl >= 0 ? "+" : "-")}${Math.Abs(pnl):F2}";
            _uiPnL.ForegroundColor = pnl >= 0 ? Color.LimeGreen : Color.OrangeRed;

            _uiTargetR.Text = $"{(pnlInR >= 0 ? "+" : "")}{pnlInR:F2} R";
            _uiTargetR.ForegroundColor = pnlInR >= 0 ? Color.LimeGreen : Color.OrangeRed;

            _uiDrawdown.Text = $"${peakDrawdown:F2}";
            _uiDrawdown.ForegroundColor = Color.DarkGray;

            _uiTrades.Text = $"{tradesToday} / {MaxTrades}";
            _uiTrades.ForegroundColor = tradesToday >= MaxTrades ? Color.Red : Color.White;

            _uiStreak.Text = $"{consecLosses} / {MaxStreak}";
            _uiStreak.ForegroundColor = consecLosses >= MaxStreak ? Color.Red : Color.White;
        }
    }
}
FTtrader
Posts: 954
Joined: Mon Aug 03, 2026 2:43 pm

Re: Sticky-note daily loss limit that faces the screen

Post by FTtrader »

Why this is a superior architecture for C#:

True Async Execution: Notice the use of ClosePositionAsync and CancelPendingOrderAsync. In MQL, closing trades is a synchronous, blocking action that queues up. If you are scalping raw PA and need the circuit breaker to trigger instantly while 4 orders are open, .Async drops them all simultaneously without hanging the thread.

LINQ over Loops: Pulling your daily trade count and consecutive losses takes exactly 2 lines of clean LINQ (Where, OrderByDescending, Sum) rather than tracking reverse index positions through an arbitrary historical array pool.

WPF UI Engine: Leveraging cTrader’s internal Automate UI wrapper (Border, StackPanel, Grid, TextBlock), we can build a dynamic UI that scales cleanly with the platform's DPI settings, rather than manually painting pixels onto a chart axis like in MetaTrader.

Run this on a clean cTrader chart (e.g., Daily timeframe). If you hit a behavioral tilt limit (e.g., 3 consecutive losses), the background goes dark purple and the engine locks out. If you hit the hard cash limit, the chart flashes maroon, terminating the session.
Post Reply