mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-23 21:18:04 +00:00
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:
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user