Refactor indicators to support optional time step in Prime method

- Updated the Prime method signature in multiple indicators (Jma, Kama, Lsma, Mama, Mgdi, Pwma, Rma, Sma, Ssf, Super, T3, Tema, Trima, Usf, Vidya, Wma, Atr) to accept an optional TimeSpan parameter for improved flexibility.
- Added unit tests for Lsma to verify Dispose functionality, ensuring proper unsubscription from the source and thread safety.
- Enhanced Mama and Wma classes to handle non-finite inputs gracefully and added checks for valid parameters in constructors.
- Introduced additional tests for T3 to validate constructor behavior with invalid volume factors.
- Ensured all indicators maintain consistent behavior when handling edge cases, such as empty buffers and non-finite values.
This commit is contained in:
Miha Kralj
2025-12-28 15:14:07 -08:00
parent af7abea6e7
commit 5c3b3fbab4
43 changed files with 661 additions and 131 deletions
+58
View File
@@ -246,4 +246,62 @@ public class WmaTests
// WMA(3) of [0, 3] -> (1*0 + 2*3) / 3 = 6/3 = 2
Assert.Equal(2, wma.Last.Value);
}
[Fact]
public void Update_IsNewFalse_OnEmptyBuffer_ThrowsInvalidOperationException()
{
var wma = new Wma(10);
// Calling Update with isNew=false on an empty buffer should throw
var exception = Assert.Throws<InvalidOperationException>(() =>
wma.Update(new TValue(DateTime.UtcNow, 100.0), isNew: false));
Assert.Contains("isNew=false", exception.Message, StringComparison.Ordinal);
Assert.Contains("buffer is empty", exception.Message, StringComparison.Ordinal);
Assert.Contains("isNew=true", exception.Message, StringComparison.Ordinal);
}
[Fact]
public void Update_IsNewFalse_AfterReset_ThrowsInvalidOperationException()
{
var wma = new Wma(10);
var gbm = new GBM();
var bars = gbm.Fetch(20, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Feed some data
for (int i = 0; i < 10; i++)
{
wma.Update(new TValue(bars[i].Time, bars[i].Close));
}
// Reset clears the buffer
wma.Reset();
// Calling Update with isNew=false after reset should throw
var exception = Assert.Throws<InvalidOperationException>(() =>
wma.Update(new TValue(bars[10].Time, bars[10].Close), isNew: false));
Assert.Contains("isNew=false", exception.Message, StringComparison.Ordinal);
Assert.Contains("buffer is empty", exception.Message, StringComparison.Ordinal);
}
[Fact]
public void Update_IsNewFalse_WithData_WorksCorrectly()
{
var wma = new Wma(10);
var gbm = new GBM();
var bars = gbm.Fetch(20, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Feed some data first
for (int i = 0; i < 5; i++)
{
wma.Update(new TValue(bars[i].Time, bars[i].Close), isNew: true);
}
// Now isNew=false should work (buffer has data)
var result = wma.Update(new TValue(bars[4].Time, bars[4].Close + 10), isNew: false);
// Should not throw and should return a finite value
Assert.True(double.IsFinite(result.Value));
}
}
+9 -1
View File
@@ -148,6 +148,14 @@ public sealed class Wma : AbstractBase, IDisposable
}
else
{
// Defensive check: isNew must be true for the first update
if (_buffer.Count == 0)
{
throw new InvalidOperationException(
"Cannot call Update with isNew=false when buffer is empty. " +
"The first update must have isNew=true to initialize state.");
}
_state = _p_state;
double val = GetValidValue(input.Value);
@@ -188,7 +196,7 @@ public sealed class Wma : AbstractBase, IDisposable
private void Handle(object? sender, TValueEventArgs e) => Update(e.Value, e.IsNew);
public override void Prime(ReadOnlySpan<double> source)
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
if (source.Length == 0) return;