From 550269054010be182963c7723203cbda0565800b Mon Sep 17 00:00:00 2001 From: AlbertoAmadorBelchistim Date: Sat, 19 Sep 2026 22:37:51 +0200 Subject: [PATCH] fix(ClusterSearch): keep realtime results consistent with a full bar recalculation The incremental OnNewTrades path only re-evaluates the windows that contain the new trades. Several settings make a window's result depend on other data, so the forming bar showed different clusters than a recalculation until the bar closed: - MinPercent/MaxPercent: every level's percentage changes with the bar's total volume. - OnlyOneSelectionPerBar: when the selected level stops passing the filters, or another level becomes larger, the selection has to move to another level. - Body and wick locations: the ranges depend on Open/Close, which move without a new High or Low. These cases now use the existing full-bar update. In addition, the incremental loop used High - (PriceRange - 1) as its upper bound while the full calculation evaluates window starts up to the top of the price ranges (the High for location Any), so windows starting near the High were not updated live. The loop now uses the same upper bound as the ranges. --- Technical/ClusterSearch.cs | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/Technical/ClusterSearch.cs b/Technical/ClusterSearch.cs index 25d7c9c7..6901d1a3 100644 --- a/Technical/ClusterSearch.cs +++ b/Technical/ClusterSearch.cs @@ -138,8 +138,8 @@ protected override void OnNewTrades(IEnumerable trades) if (!isValid) return; - // Exact zero Ask/Bid searches can depend on price levels that were not directly hit - // by the last trade, so the incremental path may miss live updates until refresh. + // Some settings make a level's result depend on data other than the levels hit by the + // new trades; the incremental path would then diverge from a full recalculation. if (RequiresFullBarUpdateOnNewTrades()) { CalculateBarFull(bar); @@ -150,6 +150,16 @@ protected override void OnNewTrades(IEnumerable trades) var totalVolume = GetTotalVolume(bar); var ranges = GetPriceRanges(bar, endPrice); + // The full calculation evaluates every window start inside the ranges, which for some + // locations reach above endPrice (up to the High). Use the same upper bound here. + var upperPrice = endPrice; + + foreach (var range in ranges) + { + if (range.To > upperPrice) + upperPrice = range.To; + } + _renderDataSeries[bar] = _lastSeriesBar; foreach (var trade in trades) @@ -158,7 +168,7 @@ protected override void OnNewTrades(IEnumerable trades) ? trade.Price - (PriceRange - 1) * InstrumentInfo.TickSize : trade.Price; - for (var price = Math.Max(candle.Low, startPrice); price <= Math.Min(endPrice, trade.Price); price += InstrumentInfo.TickSize) + for (var price = Math.Max(candle.Low, startPrice); price <= Math.Min(upperPrice, trade.Price); price += InstrumentInfo.TickSize) { var inRange = false; @@ -784,6 +794,19 @@ private bool RequiresFullBarUpdateOnNewTrades() if (CalcType is CalcMode.MaxVolume) return true; + // The percentage of every level changes with the total volume of the bar, + // not only the levels hit by the new trades. + if (MinPercent != 0 || MaxPercent != 0) + return true; + + // The selected level may stop passing the filters and another level may have to replace it. + if (OnlyOneSelectionPerBar) + return true; + + // Body and wick ranges depend on Open/Close, which can move without a new High or Low. + if (PriceLoc is PriceLocation.Body or PriceLocation.UpperWick or PriceLocation.LowerWick or PriceLocation.AtUpperLowerWick) + return true; + return !AutoFilter && CalcType is CalcMode.Ask or CalcMode.Bid && MinimumFilter.Enabled