From 62717cbae6d0f26fcedd9ed0dcd3d738788b5319 Mon Sep 17 00:00:00 2001 From: AlbertoAmadorBelchistim Date: Sun, 3 May 2026 07:46:30 +0200 Subject: [PATCH 1/4] fix(account-info): culture-safe value coloring Replace format-then-parse roundtrip with a record-backed line list that carries the raw decimal alongside the formatted string. The previous ExtractNumericValue stripped commas and re-parsed the N2-formatted value with the current culture, which fails on cultures where ',' is the decimal separator (es-ES, fr-FR, de-DE, ...) for any PnL >= 1000 absolute. Parse failure yielded 0, collapsing positive and negative PnL into the neutral colour. Coloring decisions now read the original Portfolio.OpenPnL / Portfolio.ClosedPnL values directly. ExtractNumericValue and BuildDisplayText (string-with-pipe-delimiter contract) are removed. --- Technical/AccountInfoDisplay.cs | 87 ++++++++++++++------------------- 1 file changed, 36 insertions(+), 51 deletions(-) diff --git a/Technical/AccountInfoDisplay.cs b/Technical/AccountInfoDisplay.cs index 2d702fd6c..c91b3ed56 100644 --- a/Technical/AccountInfoDisplay.cs +++ b/Technical/AccountInfoDisplay.cs @@ -1,6 +1,7 @@ namespace ATAS.Indicators.Technical; using System; +using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Drawing; @@ -219,8 +220,8 @@ protected override void OnRender(RenderContext context, DrawingLayouts layout) return; // Build display text - var text = BuildDisplayText(portfolio); - if (string.IsNullOrEmpty(text)) + var lines = BuildLines(portfolio); + if (lines.Count == 0) return; // Calculate proper dimensions for table layout @@ -232,11 +233,8 @@ protected override void OnRender(RenderContext context, DrawingLayouts layout) foreach (var line in lines) { - var parts = line.Split('|'); - if (parts.Length == 2) - { - var labelWidth = (int)context.MeasureString(parts[0], _font).Width; - var valueWidth = (int)context.MeasureString(parts[1], _font).Width; + var labelWidth = (int)context.MeasureString(line.Label, _font).Width; + var valueWidth = (int)context.MeasureString(line.Value, _font).Width; if (labelWidth > maxLabelWidth) maxLabelWidth = labelWidth; @@ -247,7 +245,7 @@ protected override void OnRender(RenderContext context, DrawingLayouts layout) var padding = 10; var rectWidth = maxLabelWidth + ColumnSpacing + maxValueWidth + padding * 2; - var rectHeight = lines.Length * lineHeight + padding * 2; + var rectHeight = lines.Count * lineHeight + padding * 2; // Calculate position var x = CalculateXPosition(rectWidth); @@ -262,7 +260,7 @@ protected override void OnRender(RenderContext context, DrawingLayouts layout) // Draw text var textRect = new Rectangle(x + padding, y + padding, rectWidth - padding * 2, rectHeight - padding * 2); - DrawColoredText(context, text, textRect, portfolio, maxLabelWidth); + DrawColoredText(context, lines, textRect, maxLabelWidth); } #endregion @@ -275,41 +273,52 @@ private void OnPortfolioSelected(Portfolio portfolio) RedrawChart(); } - private string BuildDisplayText(Portfolio portfolio) + private sealed record DisplayLine(string Label, string Value, decimal? RawForColoring); + + private List BuildLines(Portfolio p) { - var sb = new StringBuilder(); + var lines = new List(); if (ShowAccountId) - sb.AppendLine($"Account|{portfolio.AccountID}"); + lines.Add(new("Account", p.AccountID, null)); - if (ShowCurrency && portfolio.Currency.HasValue) - sb.AppendLine($"Currency|{portfolio.Currency}"); + if (ShowCurrency && p.Currency.HasValue) + lines.Add(new("Currency", p.Currency.Value.ToString(), null)); if (ShowBalance) - sb.AppendLine($"Balance|{FormatCurrency(portfolio.Balance)}"); + lines.Add(new("Balance", FormatCurrency(p.Balance), null)); - if (ShowAvailableBalance && portfolio.BalanceAvailable.HasValue) - sb.AppendLine($"Available|{FormatCurrency(portfolio.BalanceAvailable.Value)}"); + if (ShowAvailableBalance && p.BalanceAvailable.HasValue) + lines.Add(new("Available", FormatCurrency(p.BalanceAvailable.Value), null)); if (ShowMargin) - sb.AppendLine($"Blocked Margin|{FormatCurrency(portfolio.BlockedMargin)}"); + lines.Add(new("Blocked Margin", FormatCurrency(p.BlockedMargin), null)); - if (ShowLeverage && portfolio.Leverage != 1) - sb.AppendLine($"Leverage|{portfolio.Leverage:F2}x"); + if (ShowLeverage && p.Leverage != 1) + lines.Add(new("Leverage", $"{p.Leverage:F2}x", null)); if (ShowOpenPnL) - sb.AppendLine($"Open PnL|{FormatCurrency(portfolio.OpenPnL)}"); + lines.Add(new("Open PnL", FormatCurrency(p.OpenPnL), p.OpenPnL)); if (ShowClosedPnL) - sb.AppendLine($"Closed PnL|{FormatCurrency(portfolio.ClosedPnL)}"); + lines.Add(new("Closed PnL", FormatCurrency(p.ClosedPnL), p.ClosedPnL)); if (ShowTotalPnL) - sb.AppendLine($"Total PnL|{FormatCurrency(portfolio.ClosedPnL + portfolio.OpenPnL)}"); + // Session-level total: OpenPnL + ClosedPnL (current session) — NOT + // Portfolio.TotalPnL, which uses TotalClosedPnL (cumulative across sessions). + lines.Add(new("Total PnL", FormatCurrency(p.ClosedPnL + p.OpenPnL), p.ClosedPnL + p.OpenPnL)); - return sb.ToString().TrimEnd(); + return lines; } - private void DrawColoredText(RenderContext context, string text, Rectangle textRect, Portfolio portfolio, int maxLabelWidth) + private System.Drawing.Color ColorFor(decimal? raw) + => raw is null ? _textColor + : raw > 0m ? _positiveColor + : raw < 0m ? _negativeColor + : _neutralColor; + + private void DrawColoredText(RenderContext context, List lines, + Rectangle textRect, int maxLabelWidth) { var lines = text.Split(new[] { Environment.NewLine }, StringSplitOptions.None); var lineHeight = context.MeasureString("A", _font).Height; @@ -321,32 +330,8 @@ private void DrawColoredText(RenderContext context, string text, Rectangle textR // Draw lines foreach (var line in lines) { - var parts = line.Split('|'); - if (parts.Length == 2) - { - var label = parts[0]; - var valueStr = parts[1]; - - // Draw label - context.DrawString(label, _font, _textColor, textRect.X, currentY); - - // Determine color for value - var valueColor = _textColor; - if (line.Contains("PnL")) - { - var value = ExtractNumericValue(valueStr); - valueColor = value > 0 ? _positiveColor : (value < 0 ? _negativeColor : _neutralColor); - } - - // Draw value - context.DrawString(valueStr, _font, valueColor, valueColumnX, currentY); - } - else - { - // Fallback for malformed lines - context.DrawString(line, _font, _textColor, textRect.X, currentY); - } - + context.DrawString(line.Label, _font, _textColor, textRect.X, currentY); + context.DrawString(line.Value, _font, ColorFor(line.RawForColoring), valueColumnX, currentY); currentY += lineHeight; } } From 502a2575011508bd9ec1f60162c75c7d9d777a15 Mon Sep 17 00:00:00 2001 From: AlbertoAmadorBelchistim Date: Sun, 3 May 2026 07:46:42 +0200 Subject: [PATCH 2/4] perf(account-info): cache RenderPen and dispose RenderFont on resize The border RenderPen was instantiated on every OnRender call, and the RenderFont was reassigned on every FontSize change without disposing the previous instance. Both wrap GDI+ resources; reuse them and dispose explicitly when replaced. --- Technical/AccountInfoDisplay.cs | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/Technical/AccountInfoDisplay.cs b/Technical/AccountInfoDisplay.cs index c91b3ed56..a47f3aaf2 100644 --- a/Technical/AccountInfoDisplay.cs +++ b/Technical/AccountInfoDisplay.cs @@ -5,7 +5,6 @@ namespace ATAS.Indicators.Technical; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Drawing; -using System.Text; using ATAS.DataFeedsCore; @@ -30,6 +29,7 @@ public class AccountInfoDisplay : Indicator private Color _positiveColor = Color.FromArgb(0, 230, 118); private Color _negativeColor = Color.FromArgb(255, 82, 82); private Color _neutralColor = Color.FromArgb(150, 150, 150); + private RenderPen _borderPen = new(Color.Gray, 1); private RenderFont _font = new("Arial", 11); private RenderStringFormat _stringFormat = new() { @@ -89,7 +89,12 @@ public CrossColor NeutralColor public float FontSize { get => _font.Size; - set => _font = new RenderFont("Arial", value); + set + { + if (Math.Abs(_font.Size - value) < 0.01f) return; + var old = _font; + _font = new RenderFont("Arial", value); + } } [Display(ResourceType = typeof(Strings), Name = nameof(Strings.ShowAccountId), @@ -204,7 +209,7 @@ protected override void OnDispose() } } - protected override void OnCalculate(int bar, decimal value) + protected override void OnCalculate(int bar, decimal value) { // No calculation needed } @@ -225,7 +230,6 @@ protected override void OnRender(RenderContext context, DrawingLayouts layout) return; // Calculate proper dimensions for table layout - var lines = text.Split(new[] { Environment.NewLine }, StringSplitOptions.None); var lineHeight = context.MeasureString("A", _font).Height; var maxLabelWidth = 0; @@ -235,12 +239,11 @@ protected override void OnRender(RenderContext context, DrawingLayouts layout) { var labelWidth = (int)context.MeasureString(line.Label, _font).Width; var valueWidth = (int)context.MeasureString(line.Value, _font).Width; - - if (labelWidth > maxLabelWidth) - maxLabelWidth = labelWidth; - if (valueWidth > maxValueWidth) - maxValueWidth = valueWidth; - } + + if (labelWidth > maxLabelWidth) + maxLabelWidth = labelWidth; + if (valueWidth > maxValueWidth) + maxValueWidth = valueWidth; } var padding = 10; @@ -320,7 +323,6 @@ private System.Drawing.Color ColorFor(decimal? raw) private void DrawColoredText(RenderContext context, List lines, Rectangle textRect, int maxLabelWidth) { - var lines = text.Split(new[] { Environment.NewLine }, StringSplitOptions.None); var lineHeight = context.MeasureString("A", _font).Height; // Calculate value column position @@ -336,15 +338,6 @@ private void DrawColoredText(RenderContext context, List lines, } } - private decimal ExtractNumericValue(string valueStr) - { - // Remove currency symbols and try to parse - var cleanStr = valueStr.Replace(",", "").Trim(); - if (decimal.TryParse(cleanStr, out var result)) - return result; - return 0; - } - private string FormatCurrency(decimal value) { return value.ToString("N2"); From 04f443baae379cdc65d54b4732fff52d44961907 Mon Sep 17 00:00:00 2001 From: AlbertoAmadorBelchistim Date: Sun, 3 May 2026 07:49:45 +0200 Subject: [PATCH 3/4] chore(account-info): drop dead RenderStringFormat field; tighten guard _stringFormat was constructed but never passed to any DrawString call since the indicator draws by absolute coordinates. Remove the field. Promote the rendering padding to a private const. The Container?.Region null check effectively only validates Container (Region is a struct); make that explicit so the intent reads clearly. --- Technical/AccountInfoDisplay.cs | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/Technical/AccountInfoDisplay.cs b/Technical/AccountInfoDisplay.cs index a47f3aaf2..dcbde964e 100644 --- a/Technical/AccountInfoDisplay.cs +++ b/Technical/AccountInfoDisplay.cs @@ -31,11 +31,7 @@ public class AccountInfoDisplay : Indicator private Color _neutralColor = Color.FromArgb(150, 150, 150); private RenderPen _borderPen = new(Color.Gray, 1); private RenderFont _font = new("Arial", 11); - private RenderStringFormat _stringFormat = new() - { - LineAlignment = StringAlignment.Near, - Alignment = StringAlignment.Near - }; + private const int Padding = 10; private Portfolio _currentPortfolio; @@ -209,14 +205,14 @@ protected override void OnDispose() } } - protected override void OnCalculate(int bar, decimal value) + protected override void OnCalculate(int bar, decimal value) { // No calculation needed } protected override void OnRender(RenderContext context, DrawingLayouts layout) { - if (ChartInfo == null || Container?.Region == null) + if (ChartInfo == null || Container == null) return; // Get current portfolio @@ -246,9 +242,8 @@ protected override void OnRender(RenderContext context, DrawingLayouts layout) maxValueWidth = valueWidth; } - var padding = 10; - var rectWidth = maxLabelWidth + ColumnSpacing + maxValueWidth + padding * 2; - var rectHeight = lines.Count * lineHeight + padding * 2; + var rectWidth = maxLabelWidth + ColumnSpacing + maxValueWidth + Padding * 2; + var rectHeight = lines.Count * lineHeight + Padding * 2; // Calculate position var x = CalculateXPosition(rectWidth); @@ -262,7 +257,7 @@ protected override void OnRender(RenderContext context, DrawingLayouts layout) context.DrawRectangle(new RenderPen(Color.Gray, 1), rectangle); // Draw text - var textRect = new Rectangle(x + padding, y + padding, rectWidth - padding * 2, rectHeight - padding * 2); + var textRect = new Rectangle(x + Padding, y + Padding, rectWidth - Padding * 2, rectHeight - Padding * 2); DrawColoredText(context, lines, textRect, maxLabelWidth); } From 477d85e7c6de1336781acd6de0d038a8e4e64dcc Mon Sep 17 00:00:00 2001 From: AlbertoAmadorBelchistim <137381511+AlbertoAmadorBelchistim@users.noreply.github.com> Date: Wed, 27 May 2026 16:08:30 +0200 Subject: [PATCH 4/4] fix(account-info): address review nits - Use cached _borderPen instead of allocating per render - Remove unused `var old = _font` from FontSize setter - Fix mojibake in Total PnL comment (em-dash -> ASCII) --- Technical/AccountInfoDisplay.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Technical/AccountInfoDisplay.cs b/Technical/AccountInfoDisplay.cs index dcbde964e..7ee86d2af 100644 --- a/Technical/AccountInfoDisplay.cs +++ b/Technical/AccountInfoDisplay.cs @@ -88,7 +88,6 @@ public float FontSize set { if (Math.Abs(_font.Size - value) < 0.01f) return; - var old = _font; _font = new RenderFont("Arial", value); } } @@ -254,7 +253,7 @@ protected override void OnRender(RenderContext context, DrawingLayouts layout) context.FillRectangle(_backgroundColor, rectangle); // Draw border - context.DrawRectangle(new RenderPen(Color.Gray, 1), rectangle); + context.DrawRectangle(_borderPen, rectangle); // Draw text var textRect = new Rectangle(x + Padding, y + Padding, rectWidth - Padding * 2, rectHeight - Padding * 2); @@ -302,7 +301,7 @@ private List BuildLines(Portfolio p) lines.Add(new("Closed PnL", FormatCurrency(p.ClosedPnL), p.ClosedPnL)); if (ShowTotalPnL) - // Session-level total: OpenPnL + ClosedPnL (current session) — NOT + // Session-level total: OpenPnL + ClosedPnL (current session) - NOT // Portfolio.TotalPnL, which uses TotalClosedPnL (cumulative across sessions). lines.Add(new("Total PnL", FormatCurrency(p.ClosedPnL + p.OpenPnL), p.ClosedPnL + p.OpenPnL));