From 898c8aba1c2d529db64d63272cb49f8121930a6a Mon Sep 17 00:00:00 2001 From: Alexandre Catarino Date: Mon, 20 Jul 2026 15:40:55 +0100 Subject: [PATCH] Fix RelativeDailyVolume fallback denominator for intra-day gaps The fallback loop that runs when the current time-of-day has no exact historical slot was missing a break, so it kept overwriting the denominator with later slots and ended on the last slot of the day instead of the greatest slot <= the current time. This gave a far too small ratio for securities with intra-day data gaps. Add the break and a regression test covering the gap scenario. Co-Authored-By: Claude Opus 4.8 (1M context) --- Indicators/RelativeDailyVolume.cs | 1 + Tests/Indicators/RelativeDailyVolumeTests.cs | 35 ++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/Indicators/RelativeDailyVolume.cs b/Indicators/RelativeDailyVolume.cs index 72afc4d40421..cca43f2c9a99 100644 --- a/Indicators/RelativeDailyVolume.cs +++ b/Indicators/RelativeDailyVolume.cs @@ -114,6 +114,7 @@ protected override decimal ComputeNextValue(TradeBar input) if (relativeDataKeys[i] > currentTimeBar) { denominator = _relativeData[relativeDataKeys[i - 1]].Current.Value; + break; // Stop at the first key greater than currentTimeBar, using the greatest slot <= currentTimeBar } } } diff --git a/Tests/Indicators/RelativeDailyVolumeTests.cs b/Tests/Indicators/RelativeDailyVolumeTests.cs index 07c8cf5614ec..678611e54361 100644 --- a/Tests/Indicators/RelativeDailyVolumeTests.cs +++ b/Tests/Indicators/RelativeDailyVolumeTests.cs @@ -113,6 +113,41 @@ public override void WarmsUpProperly() Assert.IsFalse(rdv8.IsReady); } + [Test] + public void UsesMostRecentHistoricalSlotForIntradayGap() + { + // Regression test for the missing break in the fallback loop (GH #9629). + // When the current time-of-day has no exact historical slot, the denominator must + // come from the greatest historical slot <= the current time, not the last slot of the day. + var rdv = new RelativeDailyVolume(2); + var historicalDays = new[] { new DateTime(2024, 1, 1), new DateTime(2024, 1, 2) }; + var tradeTimes = new[] + { + new TimeSpan(9, 30, 0), + new TimeSpan(9, 44, 0), + new TimeSpan(16, 0, 0), + new TimeSpan(16, 1, 0), + }; + + // Two historical days, each trading at the same four times with the same volume. + // Cumulative volume per slot: 09:30 -> 100, 09:44 -> 200, 16:00 -> 300, 16:01 -> 400. + foreach (var day in historicalDays) + { + foreach (var time in tradeTimes) + { + rdv.Update(new TradeBar { Symbol = Symbols.AAPL, Low = 1, High = 2, Volume = 100, Time = day + time }); + } + } + + // Third day: a bar arrives at 09:45, which has no exact historical slot and sits in the + // intra-day gap between 09:44 and 16:00. The fallback must use the 09:44 slot (denominator 200). + // Without the break it would end on the 16:00 slot (denominator 300), giving 50/300 instead of 50/200. + rdv.Update(new TradeBar { Symbol = Symbols.AAPL, Low = 1, High = 2, Volume = 50, Time = new DateTime(2024, 1, 3, 9, 45, 0) }); + + Assert.IsTrue(rdv.IsReady); + Assert.AreEqual(50m / 200m, rdv.Current.Value); + } + /// /// The final value of this indicator is zero because it uses the Volume of the bars it receives. /// Since RenkoBar's don't always have Volume, the final current value is zero. Therefore we