Advertisement IC Markets

50% Win Rate Sounds Easy… Until You Actually Trade It

Master exponential money management, position sizing calculators, strict daily stop-loss limits, and overcoming FOMO on micro-timeframes.
PTScalper
Site Admin
Posts: 2648
Joined: Mon Jul 20, 2026 1:28 pm

Re: 50% Win Rate Sounds Easy… Until You Actually Trade It

Post by PTScalper »

In cTrader (cAlgo), we don't need to manually run an expensive loop on every single tick (OnCalculate) just to update a dashboard. Instead, we can use a purely event-driven architecture by subscribing to History.HistoryItemAdded. We can also heavily leverage LINQ to calculate the metrics in just a few lines of code.

Furthermore, cTrader's NetProfit property natively includes commissions and swaps, eliminating the need to manually sum them up.

Here is the professional, production-ready C# implementation for cTrader.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Recommended broker for automated trading & scalping IC Markets
PTScalper
Site Admin
Posts: 2648
Joined: Mon Jul 20, 2026 1:28 pm

Re: 50% Win Rate Sounds Easy… Until You Actually Trade It

Post by PTScalper »

cTrader (cAlgo / C#)

Save this as a new Indicator in cTrader Automate (e.g., RealizedMetricsHUD).

Code: Select all

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

namespace cAlgo
{
    [Indicator(IsOverlay = true, TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class RealizedMetricsHUD : Indicator
    {
        [Parameter("Filter by Symbol", DefaultValue = true, Group = "History Filters")]
        public bool FilterBySymbol { get; set; }

        [Parameter("Filter by Label (Empty = All)", DefaultValue = "", Group = "History Filters")]
        public string FilterLabel { get; set; }

        [Parameter("Start Date (yyyy-MM-dd)", DefaultValue = "2024-01-01", Group = "History Filters")]
        public string StartDateStr { get; set; }

        private DateTime _startDate;
        private Border _hudBorder;
        private StackPanel _mainPanel;
        
        // UI Elements for dynamic updating
        private TextBlock _tbTotalTrades, _tbWinRate, _tbRRRatio, _tbProfitFactor, _tbExpectancy, _tbAvgWinLoss, _tbNetProfit;

        protected override void Initialize()
        {
            // Parse Date
            if (!DateTime.TryParse(StartDateStr, out _startDate))
                _startDate = DateTime.MinValue;

            // Build UI
            BuildDashboardUI();

            // Subscribe to history events (Event-Driven update = Zero tick-level performance hit)
            History.HistoryItemAdded += OnHistoryItemAdded;

            // Initial Calculation
            UpdateMetrics();
        }

        public override void Calculate(int index)
        {
            // Intentionally empty. We use event-driven updates instead of per-tick loops.
        }

        private void OnHistoryItemAdded(HistoryItemAddedEventArgs obj)
        {
            // Only recalculate if the new trade matches our filters
            if (FilterBySymbol && obj.HistoryItem.SymbolName != SymbolName) return;
            if (!string.IsNullOrEmpty(FilterLabel) && obj.HistoryItem.Label != FilterLabel) return;
            
            UpdateMetrics();
        }

        private void UpdateMetrics()
        {
            // 1. LINQ Filter History
            var trades = History.Where(t => 
                (!FilterBySymbol || t.SymbolName == SymbolName) &&
                (string.IsNullOrEmpty(FilterLabel) || t.Label == FilterLabel) &&
                t.ClosingTime >= _startDate
            ).ToList();

            int totalTrades = trades.Count;
            var wins = trades.Where(t => t.NetProfit > 0).ToList();
            var losses = trades.Where(t => t.NetProfit < 0).ToList();

            // NetProfit inherently includes GrossProfit, Commissions, and Swaps in cTrader
            double grossProfit = wins.Sum(t => t.NetProfit);
            double grossLoss = Math.Abs(losses.Sum(t => t.NetProfit));
            double netProfit = trades.Sum(t => t.NetProfit);

            // 2. Derive Metrics
            double winRate = totalTrades > 0 ? ((double)wins.Count / totalTrades) * 100.0 : 0.0;
            double avgWin = wins.Count > 0 ? grossProfit / wins.Count : 0.0;
            double avgLoss = losses.Count > 0 ? grossLoss / losses.Count : 0.0;
            
            double rrRatio = avgLoss > 0 ? avgWin / avgLoss : 0.0;
            double profitFactor = grossLoss > 0 ? grossProfit / grossLoss : (grossProfit > 0 ? 99.0 : 0.0);
            double expectancy = (winRate / 100.0 * avgWin) - ((1.0 - winRate / 100.0) * avgLoss);

            // 3. Dispatch to UI Thread
            Chart.SetControlText(_tbTotalTrades, totalTrades.ToString());
            
            Chart.SetControlText(_tbWinRate, $"{winRate:F2}%");
            _tbWinRate.ForegroundColor = winRate >= 50.0 ? Color.MediumSeaGreen : Color.Crimson;

            Chart.SetControlText(_tbRRRatio, $"1 : {rrRatio:F2}");
            _tbRRRatio.ForegroundColor = rrRatio >= 1.5 ? Color.MediumSeaGreen : (rrRatio >= 1.0 ? Color.Goldenrod : Color.Crimson);

            Chart.SetControlText(_tbProfitFactor, $"{profitFactor:F2}");
            _tbProfitFactor.ForegroundColor = profitFactor >= 1.5 ? Color.MediumSeaGreen : (profitFactor >= 1.0 ? Color.Goldenrod : Color.Crimson);

            Chart.SetControlText(_tbExpectancy, $"${expectancy:F2}");
            _tbExpectancy.ForegroundColor = expectancy > 0 ? Color.MediumSeaGreen : Color.Crimson;

            Chart.SetControlText(_tbAvgWinLoss, $"${avgWin:F2} / ${avgLoss:F2}");
            
            Chart.SetControlText(_tbNetProfit, $"${netProfit:F2}");
            _tbNetProfit.ForegroundColor = netProfit >= 0 ? Color.MediumSeaGreen : Color.Crimson;
        }

        private void BuildDashboardUI()
        {
            _mainPanel = new StackPanel { Orientation = Orientation.Vertical };

            // Initialize TextBlocks
            _tbTotalTrades  = CreateValueTextBlock();
            _tbWinRate      = CreateValueTextBlock();
            _tbRRRatio      = CreateValueTextBlock();
            _tbProfitFactor = CreateValueTextBlock();
            _tbExpectancy   = CreateValueTextBlock();
            _tbAvgWinLoss   = CreateValueTextBlock(Color.Silver);
            _tbNetProfit    = CreateValueTextBlock();

            // Build Rows
            _mainPanel.AddChild(CreateRow("Total Trades:", _tbTotalTrades));
            _mainPanel.AddChild(CreateRow("Win Rate:", _tbWinRate));
            _mainPanel.AddChild(CreateRow("Realized R:R:", _tbRRRatio));
            _mainPanel.AddChild(CreateRow("Profit Factor:", _tbProfitFactor));
            _mainPanel.AddChild(CreateRow("Expectancy / Trade:", _tbExpectancy));
            _mainPanel.AddChild(CreateRow("Avg Win / Loss:", _tbAvgWinLoss));
            _mainPanel.AddChild(CreateRow("Net Profit:", _tbNetProfit));

            _hudBorder = new Border
            {
                VerticalAlignment = VerticalAlignment.Top,
                HorizontalAlignment = HorizontalAlignment.Right,
                BackgroundColor = Color.FromArgb(230, 20, 24, 35),
                BorderColor = Color.FromArgb(255, 60, 65, 80),
                BorderThickness = new Thickness(1),
                Margin = new Thickness(0, 30, 70, 0),
                Padding = new Thickness(10, 10, 10, 10),
                Child = _mainPanel
            };

            Chart.AddControl(_hudBorder);
        }

        // --- UI Helper Methods ---
        private DockPanel CreateRow(string title, TextBlock valueBlock)
        {
            var row = new DockPanel { Margin = new Thickness(0, 2, 0, 2), Width = 210 };
            
            var titleBlock = new TextBlock 
            { 
                Text = title, 
                ForegroundColor = Color.WhiteSmoke,
                HorizontalAlignment = HorizontalAlignment.Left
            };

            row.AddChild(titleBlock, Dock.Left);
            row.AddChild(valueBlock, Dock.Right);

            return row;
        }

        private TextBlock CreateValueTextBlock(Color? defaultColor = null)
        {
            return new TextBlock
            {
                Text = "-",
                ForegroundColor = defaultColor ?? Color.White,
                FontWeight = FontWeight.SemiBold,
                HorizontalAlignment = HorizontalAlignment.Right
            };
        }
    }

    // Helper extension to safely update UI thread from event handlers
    public static class ChartExtensions
    {
        public static void SetControlText(this Chart chart, TextBlock control, string text)
        {
            chart.Indicator.BeginInvokeOnMainThread(() => control.Text = text);
        }
    }
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 2648
Joined: Mon Jul 20, 2026 1:28 pm

Re: 50% Win Rate Sounds Easy… Until You Actually Trade It

Post by PTScalper »

Why this architecture is superior for cTrader:

Zero Tick-Cost: The Calculate() loop is intentionally empty. The UI only recalculates when a new item hits your trade history (History.HistoryItemAdded), making the indicator virtually zero-latency on your chart.

LINQ Power: C# allows us to slice up the history using standard LINQ expressions (.Where(t => t.NetProfit > 0)).

Thread Safety: When a trade closes, the event can sometimes fire on a background thread. The ChartExtensions.SetControlText helper forces the UI update back onto the main thread via BeginInvokeOnMainThread so cTrader's WPF engine won't throw cross-thread exceptions.

WPF-Style Layouts: Instead of manually calculating X/Y pixel coordinates like MT4/MT5, this builds a dynamic layout using DockPanel and StackPanel, meaning it perfectly auto-resizes regardless of the font or data length.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
LondonScalper
Posts: 690
Joined: Sat Sep 05, 2026 7:54 am

Re: 50% Win Rate Sounds Easy… Until You Actually Trade It

Post by LondonScalper »

PTScalper wrote:In cTrader (cAlgo), we don't need to manually run an expensive loop on every single tick (OnCalculate) just to update a dashboard. Instead, we can use a purely event-driven architecture by subscribing to History.HistoryItemAdded.
Event-driven stats panels are neat. Living a true ~50% win rate with positive expectancy still feels worse than the brochure because losers cluster and humans hate clusters.

I size for the streak the test already showed, not for the average win rate on the sales page. A 50% book with 1.5R winners can be fine; a 50% book traded with revenge size after three losers is not.

Dashboards do not fix that. A written walk-away after N full losses does.

How many consecutive full losses does your live plan allow before the session ends?
Post Reply