Skip to content

Commit f34dbc9

Browse files
AlexCatarinoclaude
andauthored
Fix RelativeDailyVolume fallback denominator for intra-day gaps (#9630)
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) <noreply@anthropic.com>
1 parent 4249165 commit f34dbc9

2 files changed

Lines changed: 36 additions & 0 deletions

File tree

Indicators/RelativeDailyVolume.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ protected override decimal ComputeNextValue(TradeBar input)
114114
if (relativeDataKeys[i] > currentTimeBar)
115115
{
116116
denominator = _relativeData[relativeDataKeys[i - 1]].Current.Value;
117+
break; // Stop at the first key greater than currentTimeBar, using the greatest slot <= currentTimeBar
117118
}
118119
}
119120
}

Tests/Indicators/RelativeDailyVolumeTests.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,41 @@ public override void WarmsUpProperly()
113113
Assert.IsFalse(rdv8.IsReady);
114114
}
115115

116+
[Test]
117+
public void UsesMostRecentHistoricalSlotForIntradayGap()
118+
{
119+
// Regression test for the missing break in the fallback loop (GH #9629).
120+
// When the current time-of-day has no exact historical slot, the denominator must
121+
// come from the greatest historical slot <= the current time, not the last slot of the day.
122+
var rdv = new RelativeDailyVolume(2);
123+
var historicalDays = new[] { new DateTime(2024, 1, 1), new DateTime(2024, 1, 2) };
124+
var tradeTimes = new[]
125+
{
126+
new TimeSpan(9, 30, 0),
127+
new TimeSpan(9, 44, 0),
128+
new TimeSpan(16, 0, 0),
129+
new TimeSpan(16, 1, 0),
130+
};
131+
132+
// Two historical days, each trading at the same four times with the same volume.
133+
// Cumulative volume per slot: 09:30 -> 100, 09:44 -> 200, 16:00 -> 300, 16:01 -> 400.
134+
foreach (var day in historicalDays)
135+
{
136+
foreach (var time in tradeTimes)
137+
{
138+
rdv.Update(new TradeBar { Symbol = Symbols.AAPL, Low = 1, High = 2, Volume = 100, Time = day + time });
139+
}
140+
}
141+
142+
// Third day: a bar arrives at 09:45, which has no exact historical slot and sits in the
143+
// intra-day gap between 09:44 and 16:00. The fallback must use the 09:44 slot (denominator 200).
144+
// Without the break it would end on the 16:00 slot (denominator 300), giving 50/300 instead of 50/200.
145+
rdv.Update(new TradeBar { Symbol = Symbols.AAPL, Low = 1, High = 2, Volume = 50, Time = new DateTime(2024, 1, 3, 9, 45, 0) });
146+
147+
Assert.IsTrue(rdv.IsReady);
148+
Assert.AreEqual(50m / 200m, rdv.Current.Value);
149+
}
150+
116151
/// <summary>
117152
/// The final value of this indicator is zero because it uses the Volume of the bars it receives.
118153
/// Since RenkoBar's don't always have Volume, the final current value is zero. Therefore we

0 commit comments

Comments
 (0)