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
+84
View File
@@ -220,4 +220,88 @@ public class LsmaTests
source.Add(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100, lsma.Last.Value);
}
[Fact]
public void Dispose_UnsubscribesFromSource()
{
var source = new TSeries();
var lsma = new Lsma(source, 5);
// Verify subscription works
source.Add(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100, lsma.Last.Value);
// Dispose and verify unsubscription
lsma.Dispose();
// Add more data - lsma should NOT update
source.Add(new TValue(DateTime.UtcNow, 200));
Assert.Equal(100, lsma.Last.Value); // Should remain at previous value
}
[Fact]
public void Dispose_IsIdempotent()
{
var source = new TSeries();
var lsma = new Lsma(source, 5);
source.Add(new TValue(DateTime.UtcNow, 100));
// Multiple Dispose calls should not throw
// Suppressing S3966: Multiple Dispose calls are intentional to test idempotency
#pragma warning disable S3966
lsma.Dispose();
lsma.Dispose();
lsma.Dispose();
#pragma warning restore S3966
// Verify still unsubscribed
source.Add(new TValue(DateTime.UtcNow, 200));
Assert.Equal(100, lsma.Last.Value);
}
[Fact]
public async System.Threading.Tasks.Task Dispose_IsThreadSafe()
{
var source = new TSeries();
var lsma = new Lsma(source, 5);
source.Add(new TValue(DateTime.UtcNow, 100));
// Dispose from multiple threads simultaneously
var tasks = new System.Threading.Tasks.Task[10];
for (int i = 0; i < tasks.Length; i++)
{
tasks[i] = System.Threading.Tasks.Task.Run(() => lsma.Dispose());
}
await System.Threading.Tasks.Task.WhenAll(tasks);
// Verify unsubscribed
source.Add(new TValue(DateTime.UtcNow, 200));
Assert.Equal(100, lsma.Last.Value);
}
[Fact]
public void Dispose_WithoutSource_DoesNotThrow()
{
// Lsma created without source parameter
var lsma = new Lsma(5);
// Should not throw even though there's no source to unsubscribe from
// Suppressing S3966: Multiple Dispose calls are intentional to test idempotency
#pragma warning disable S3966
lsma.Dispose();
lsma.Dispose(); // Idempotent
#pragma warning restore S3966
// Verify state remains valid
Assert.False(lsma.IsHot);
}
[Fact]
public void Constructor_NullSource_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => new Lsma(null!, 5));
}
}
+28 -8
View File
@@ -23,9 +23,14 @@ namespace QuanTAlib;
///
/// IsHot:
/// Becomes true when the buffer is full (period samples processed).
///
/// Disposal:
/// When constructed with an ITValuePublisher source, Lsma subscribes to the source's Pub event.
/// Call Dispose() to unsubscribe and prevent memory leaks, especially in long-running applications
/// or when creating many short-lived indicator instances.
/// </remarks>
[SkipLocalsInit]
public sealed class Lsma : AbstractBase
public sealed class Lsma : AbstractBase, IDisposable
{
private readonly int _period;
private readonly int _offset;
@@ -34,6 +39,8 @@ public sealed class Lsma : AbstractBase
private readonly double _sum_x;
private readonly double _denominator;
private readonly TValuePublishedHandler _handler;
private ITValuePublisher? _source;
private int _disposed;
[StructLayout(LayoutKind.Auto)]
private record struct State(double SumY, double SumXY, double LastVal, double LastValidValue);
@@ -76,7 +83,8 @@ public sealed class Lsma : AbstractBase
public Lsma(ITValuePublisher source, int period, int offset = 0) : this(period, offset)
{
source.Pub += _handler;
_source = source ?? throw new ArgumentNullException(nameof(source));
_source.Pub += _handler;
}
private void Handle(object? sender, TValueEventArgs e) => Update(e.Value, e.IsNew);
@@ -213,11 +221,8 @@ public sealed class Lsma : AbstractBase
int len = source.Count;
var t = new List<long>(len);
var v = new List<double>(len);
for (int i = 0; i < len; i++)
{
t.Add(0);
v.Add(0);
}
CollectionsMarshal.SetCount(t, len);
CollectionsMarshal.SetCount(v, len);
var tSpan = CollectionsMarshal.AsSpan(t);
var vSpan = CollectionsMarshal.AsSpan(v);
@@ -265,7 +270,7 @@ public sealed class Lsma : AbstractBase
return new TSeries(t, v);
}
public override void Prime(ReadOnlySpan<double> source)
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
foreach (var value in source)
{
@@ -394,4 +399,19 @@ public sealed class Lsma : AbstractBase
Last = default;
_tickCount = 0;
}
/// <summary>
/// Disposes the Lsma instance, unsubscribing from the source publisher if subscribed.
/// This method is idempotent and thread-safe.
/// </summary>
public void Dispose()
{
// Use Interlocked.CompareExchange for thread-safe, idempotent disposal
if (Interlocked.CompareExchange(ref _disposed, 1, 0) == 0 && _source != null)
{
_source.Pub -= _handler;
_source = null;
}
GC.SuppressFinalize(this);
}
}