From 8facfe42c5c0edb181e160b937842c2817b596b9 Mon Sep 17 00:00:00 2001 From: AlbertoAmadorBelchistim Date: Sat, 4 Jul 2026 12:18:45 +0200 Subject: [PATCH 1/2] feat(AccountInfoDisplay): add per-trade max open PnL rows Adds two optional rows to the account info panel, both disabled by default so existing setups are unaffected: - Max open PnL: highest open PnL reached during the current trade, shown while a position is open on the selected account/instrument. - Last trade max PnL: the same value for the last closed trade, kept visible while flat. Useful to review how much unrealized profit a trade reached before it was closed (maximum favorable excursion). Tracking is runtime-only (no persistence): the state machine follows flat -> open -> flat transitions of TradingManager.Position, seeds the baseline with the portfolio open PnL at entry, and resets whenever the selected portfolio changes. The two toggle names and row labels are declared inline like the existing row labels in this file; no localization keys exist for them yet, so they follow the same transitional pattern as the Half Gap settings in DailyLines. --- Technical/AccountInfoDisplay.cs | 55 +++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/Technical/AccountInfoDisplay.cs b/Technical/AccountInfoDisplay.cs index 7ee86d2af..61982ad86 100644 --- a/Technical/AccountInfoDisplay.cs +++ b/Technical/AccountInfoDisplay.cs @@ -35,6 +35,10 @@ public class AccountInfoDisplay : Indicator private Portfolio _currentPortfolio; + private bool _wasPositionOpen; + private decimal _currentTradeMaxOpenPnl; + private decimal? _lastTradeMaxOpenPnl; + #endregion #region Properties @@ -128,6 +132,14 @@ public float FontSize Description = nameof(Strings.ShowTotalPnLDescription), GroupName = nameof(Strings.Settings))] public bool ShowTotalPnL { get; set; } = false; + [Display(Name = "Show max open PnL", GroupName = "Settings", + Description = "Show the maximum open PnL reached during the current trade.")] + public bool ShowMaxOpenPnL { get; set; } = false; + + [Display(Name = "Show last trade max open PnL", GroupName = "Settings", + Description = "Keep the maximum open PnL of the last closed trade visible while flat.")] + public bool ShowLastTradeMaxOpenPnL { get; set; } = false; + [Display(ResourceType = typeof(Strings), Name = nameof(Strings.HorizontalPosition), GroupName = nameof(Strings.LayoutGroup))] public HorizontalAlignment HorizontalPosition { get; set; } = HorizontalAlignment.Left; @@ -219,6 +231,9 @@ protected override void OnRender(RenderContext context, DrawingLayouts layout) if (portfolio == null) return; + if (ShowMaxOpenPnL || ShowLastTradeMaxOpenPnL) + UpdateMaxOpenPnl(portfolio); + // Build display text var lines = BuildLines(portfolio); if (lines.Count == 0) @@ -267,9 +282,43 @@ protected override void OnRender(RenderContext context, DrawingLayouts layout) private void OnPortfolioSelected(Portfolio portfolio) { _currentPortfolio = portfolio; + + _wasPositionOpen = false; + _currentTradeMaxOpenPnl = 0m; + _lastTradeMaxOpenPnl = null; + RedrawChart(); } + private void UpdateMaxOpenPnl(Portfolio portfolio) + { + var position = TradingManager?.Position; + var security = TradingManager?.Security; + + var isOpen = position != null && security != null + && string.Equals(position.AccountID, portfolio.AccountID, StringComparison.Ordinal) + && position.Security != null + && string.Equals(position.Security.Code, security.Code, StringComparison.Ordinal) + && position.IsInPosition && position.Volume != 0m; + + if (isOpen) + { + if (!_wasPositionOpen) + { + _wasPositionOpen = true; + _currentTradeMaxOpenPnl = portfolio.OpenPnL; + } + else if (portfolio.OpenPnL > _currentTradeMaxOpenPnl) + _currentTradeMaxOpenPnl = portfolio.OpenPnL; + } + else if (_wasPositionOpen) + { + _wasPositionOpen = false; + _lastTradeMaxOpenPnl = _currentTradeMaxOpenPnl; + _currentTradeMaxOpenPnl = 0m; + } + } + private sealed record DisplayLine(string Label, string Value, decimal? RawForColoring); private List BuildLines(Portfolio p) @@ -297,6 +346,12 @@ private List BuildLines(Portfolio p) if (ShowOpenPnL) lines.Add(new("Open PnL", FormatCurrency(p.OpenPnL), p.OpenPnL)); + if (ShowMaxOpenPnL && _wasPositionOpen) + lines.Add(new("Max open PnL", FormatCurrency(_currentTradeMaxOpenPnl), _currentTradeMaxOpenPnl)); + + if (ShowLastTradeMaxOpenPnL && !_wasPositionOpen && _lastTradeMaxOpenPnl.HasValue) + lines.Add(new("Last trade max PnL", FormatCurrency(_lastTradeMaxOpenPnl.Value), _lastTradeMaxOpenPnl.Value)); + if (ShowClosedPnL) lines.Add(new("Closed PnL", FormatCurrency(p.ClosedPnL), p.ClosedPnL)); From d5e27da0723a58230d3b4d63ef83a4cba9be9901 Mon Sep 17 00:00:00 2001 From: AlbertoAmadorBelchistim Date: Sat, 19 Sep 2026 22:01:53 +0200 Subject: [PATCH 2/2] fix(AccountInfoDisplay): track max open PnL per position, independent of rendering The maximum was taken from Portfolio.OpenPnL, which also contains the PnL of other positions on the same account, and it was only sampled in OnRender, so peaks and position transitions between two frames were missed. A direct reversal was not recognized as a new trade. The value now comes from the chart position's own UnrealizedPnL. It is updated from OnPositionChanged and from the position's PropertyChanged notifications (raised on every PnL recalculation), not from rendering. The trade state follows the sign of the position volume: opening starts a trade, closing ends it, and a change of sign ends the current trade and starts a new one. Tracking is reset when the portfolio or the security changes. --- Technical/AccountInfoDisplay.cs | 143 +++++++++++++++++++++++++------- 1 file changed, 115 insertions(+), 28 deletions(-) diff --git a/Technical/AccountInfoDisplay.cs b/Technical/AccountInfoDisplay.cs index 61982ad86..fa0347bb2 100644 --- a/Technical/AccountInfoDisplay.cs +++ b/Technical/AccountInfoDisplay.cs @@ -35,7 +35,10 @@ public class AccountInfoDisplay : Indicator private Portfolio _currentPortfolio; - private bool _wasPositionOpen; + // Per-trade max open PnL. Updated from position events (not from rendering), guarded by _tradeLock. + private readonly object _tradeLock = new(); + private Position _trackedPosition; + private int _tradeSide; // sign of the tracked trade: 1 long, -1 short, 0 flat private decimal _currentTradeMaxOpenPnl; private decimal? _lastTradeMaxOpenPnl; @@ -204,7 +207,9 @@ protected override void OnInitialize() if (TradingManager != null) { TradingManager.PortfolioSelected += OnPortfolioSelected; + TradingManager.SecuritySelected += OnSecuritySelected; _currentPortfolio = TradingManager.Portfolio; + ResetTradeTracking(); } } @@ -213,7 +218,19 @@ protected override void OnDispose() if (TradingManager != null) { TradingManager.PortfolioSelected -= OnPortfolioSelected; + TradingManager.SecuritySelected -= OnSecuritySelected; } + + AttachPosition(null); + } + + protected override void OnPositionChanged(Position position) + { + if (!IsChartPosition(position)) + return; + + AttachPosition(position); + UpdateTrade(position); } protected override void OnCalculate(int bar, decimal value) @@ -231,9 +248,6 @@ protected override void OnRender(RenderContext context, DrawingLayouts layout) if (portfolio == null) return; - if (ShowMaxOpenPnL || ShowLastTradeMaxOpenPnL) - UpdateMaxOpenPnl(portfolio); - // Build display text var lines = BuildLines(portfolio); if (lines.Count == 0) @@ -282,41 +296,100 @@ protected override void OnRender(RenderContext context, DrawingLayouts layout) private void OnPortfolioSelected(Portfolio portfolio) { _currentPortfolio = portfolio; + ResetTradeTracking(); + RedrawChart(); + } - _wasPositionOpen = false; - _currentTradeMaxOpenPnl = 0m; - _lastTradeMaxOpenPnl = null; - + private void OnSecuritySelected(Security security) + { + ResetTradeTracking(); RedrawChart(); } - private void UpdateMaxOpenPnl(Portfolio portfolio) + private void OnTrackedPositionPropertyChanged(object sender, PropertyChangedEventArgs e) { - var position = TradingManager?.Position; + // UnrealizedPnL raises PropertyChanged on every recalculation, so every peak is seen, + // including the ones that happen between two rendered frames. + if (e.PropertyName is nameof(Position.UnrealizedPnL) or nameof(Position.Volume) or nameof(Position.IsInPosition) or null or "") + UpdateTrade((Position)sender); + } + + private bool IsChartPosition(Position position) + { + var portfolio = _currentPortfolio ?? TradingManager?.Portfolio; var security = TradingManager?.Security; - var isOpen = position != null && security != null + return position != null && portfolio != null && security != null && string.Equals(position.AccountID, portfolio.AccountID, StringComparison.Ordinal) && position.Security != null - && string.Equals(position.Security.Code, security.Code, StringComparison.Ordinal) - && position.IsInPosition && position.Volume != 0m; + && string.Equals(position.Security.Code, security.Code, StringComparison.Ordinal); + } - if (isOpen) + private void AttachPosition(Position position) + { + lock (_tradeLock) { - if (!_wasPositionOpen) - { - _wasPositionOpen = true; - _currentTradeMaxOpenPnl = portfolio.OpenPnL; - } - else if (portfolio.OpenPnL > _currentTradeMaxOpenPnl) - _currentTradeMaxOpenPnl = portfolio.OpenPnL; + if (ReferenceEquals(_trackedPosition, position)) + return; + + if (_trackedPosition != null) + _trackedPosition.PropertyChanged -= OnTrackedPositionPropertyChanged; + + _trackedPosition = position; + + if (_trackedPosition != null) + _trackedPosition.PropertyChanged += OnTrackedPositionPropertyChanged; } - else if (_wasPositionOpen) + } + + private void ResetTradeTracking() + { + lock (_tradeLock) { - _wasPositionOpen = false; - _lastTradeMaxOpenPnl = _currentTradeMaxOpenPnl; + _tradeSide = 0; _currentTradeMaxOpenPnl = 0m; + _lastTradeMaxOpenPnl = null; } + + var position = TradingManager?.Position; + var tracked = IsChartPosition(position) ? position : null; + AttachPosition(tracked); + + if (tracked != null) + UpdateTrade(tracked); + } + + private void UpdateTrade(Position position) + { + // The position's own PnL is used, not Portfolio.OpenPnL, which includes other positions of the account. + var side = position.IsInPosition ? Math.Sign(position.Volume) : 0; + var pnl = position.UnrealizedPnL; + var changed = false; + + lock (_tradeLock) + { + if (side != _tradeSide) + { + // Close (side -> 0) or reversal (side -> -side) ends the current trade. + if (_tradeSide != 0) + _lastTradeMaxOpenPnl = _currentTradeMaxOpenPnl; + + // Opening or reversing starts a new trade. + if (side != 0) + _currentTradeMaxOpenPnl = pnl; + + _tradeSide = side; + changed = true; + } + else if (side != 0 && pnl > _currentTradeMaxOpenPnl) + { + _currentTradeMaxOpenPnl = pnl; + changed = true; + } + } + + if (changed && (ShowMaxOpenPnL || ShowLastTradeMaxOpenPnL)) + RedrawChart(); } private sealed record DisplayLine(string Label, string Value, decimal? RawForColoring); @@ -346,11 +419,25 @@ private List BuildLines(Portfolio p) if (ShowOpenPnL) lines.Add(new("Open PnL", FormatCurrency(p.OpenPnL), p.OpenPnL)); - if (ShowMaxOpenPnL && _wasPositionOpen) - lines.Add(new("Max open PnL", FormatCurrency(_currentTradeMaxOpenPnl), _currentTradeMaxOpenPnl)); + if (ShowMaxOpenPnL || ShowLastTradeMaxOpenPnL) + { + int side; + decimal currentMax; + decimal? lastMax; + + lock (_tradeLock) + { + side = _tradeSide; + currentMax = _currentTradeMaxOpenPnl; + lastMax = _lastTradeMaxOpenPnl; + } + + if (ShowMaxOpenPnL && side != 0) + lines.Add(new("Max open PnL", FormatCurrency(currentMax), currentMax)); - if (ShowLastTradeMaxOpenPnL && !_wasPositionOpen && _lastTradeMaxOpenPnl.HasValue) - lines.Add(new("Last trade max PnL", FormatCurrency(_lastTradeMaxOpenPnl.Value), _lastTradeMaxOpenPnl.Value)); + if (ShowLastTradeMaxOpenPnL && side == 0 && lastMax.HasValue) + lines.Add(new("Last trade max PnL", FormatCurrency(lastMax.Value), lastMax.Value)); + } if (ShowClosedPnL) lines.Add(new("Closed PnL", FormatCurrency(p.ClosedPnL), p.ClosedPnL));