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.
50% Win Rate Sounds Easy… Until You Actually Trade It
Re: 50% Win Rate Sounds Easy… Until You Actually Trade It
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Re: 50% Win Rate Sounds Easy… Until You Actually Trade It
cTrader (cAlgo / C#)
Save this as a new Indicator in cTrader Automate (e.g., RealizedMetricsHUD).
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.
Re: 50% Win Rate Sounds Easy… Until You Actually Trade It
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.
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: 693
- Joined: Sat Sep 05, 2026 7:54 am
Re: 50% Win Rate Sounds Easy… Until You Actually Trade It
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.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.
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?