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
+175
View File
@@ -37,6 +37,62 @@ public class MamaTests
Assert.Equal(0.0, result.Value);
}
[Fact]
public void Update_InfinityInputs_DoesNotHang()
{
var mama = new Mama();
// Warmup with valid data to get past initialization phase
for (int i = 0; i < 60; i++)
{
mama.Update(new TValue(DateTime.UtcNow, 100.0 + i));
}
// Test positive infinity - should not hang
var result1 = mama.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
Assert.True(double.IsFinite(result1.Value), "Positive infinity should produce finite result");
// Test negative infinity - should not hang
var result2 = mama.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity));
Assert.True(double.IsFinite(result2.Value), "Negative infinity should produce finite result");
// Test NaN - should not hang
var result3 = mama.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.True(double.IsFinite(result3.Value), "NaN should produce finite result");
}
[Fact]
public void Calculate_Span_WithNonFiniteValues_DoesNotHang()
{
var data = new double[100];
var gbm = new GBM(startPrice: 100, seed: 42);
// Fill with mostly valid data
for (int i = 0; i < 100; i++)
{
data[i] = gbm.Next().Close;
}
// Insert non-finite values at various points
data[20] = double.NaN;
data[40] = double.PositiveInfinity;
data[60] = double.NegativeInfinity;
data[80] = double.NaN;
var output = new double[100];
var famaOutput = new double[100];
// This should complete without hanging
Mama.Calculate(data, output, famaOutput: famaOutput);
// Verify all outputs are finite (no NaN or Infinity propagation)
for (int i = 0; i < 100; i++)
{
Assert.True(double.IsFinite(output[i]), $"MAMA output at index {i} should be finite");
Assert.True(double.IsFinite(famaOutput[i]), $"FAMA output at index {i} should be finite");
}
}
[Fact]
public void Update_Series_ReturnsSameCount()
{
@@ -231,4 +287,123 @@ public class MamaTests
Assert.True(mamaPrimed.IsHot);
Assert.Equal(resultNormal.Value, resultPrimed.Value, precision: 9);
}
[Fact]
public void Calculate_Span_WithFamaOutput_ProducesCorrectValues()
{
int count = 100;
var data = new double[count];
var gbm = new GBM(startPrice: 100, seed: 42);
for (int i = 0; i < count; i++) data[i] = gbm.Next().Close;
var mamaOutput = new double[count];
var famaOutput = new double[count];
Mama.Calculate(data, mamaOutput, famaOutput: famaOutput);
var mama = new Mama();
for (int i = 0; i < count; i++)
{
mama.Update(new TValue(DateTime.UtcNow, data[i]));
Assert.Equal(mama.Last.Value, mamaOutput[i], precision: 8);
Assert.Equal(mama.Fama.Value, famaOutput[i], precision: 8);
}
}
[Fact]
public void Calculate_Span_WithoutFamaOutput_BackwardsCompatible()
{
int count = 100;
var data = new double[count];
var gbm = new GBM(startPrice: 100, seed: 42);
for (int i = 0; i < count; i++) data[i] = gbm.Next().Close;
var output1 = new double[count];
var output2 = new double[count];
// Call without famaOutput parameter (backwards compatibility)
Mama.Calculate(data, output1);
// Call with empty famaOutput span
Mama.Calculate(data, output2, famaOutput: Span<double>.Empty);
// Both should produce identical MAMA results
for (int i = 0; i < count; i++)
{
Assert.Equal(output1[i], output2[i], precision: 12);
}
}
[Fact]
public void Calculate_Span_FamaOutput_ThrowsOnSmallBuffer()
{
var data = new double[10];
var mamaOutput = new double[10];
var famaOutput = new double[5];
var ex = Assert.Throws<ArgumentOutOfRangeException>(() =>
Mama.Calculate(data, mamaOutput, famaOutput: famaOutput));
Assert.Equal("famaOutput", ex.ParamName);
}
[Fact]
public void Calculate_Span_FamaInitialization_MatchesInstanceMethod()
{
// Test that during initialization phase, FAMA output matches instance method behavior
int count = 10;
var data = new double[count];
var gbm = new GBM(startPrice: 100, seed: 42);
for (int i = 0; i < count; i++) data[i] = gbm.Next().Close;
// Get values from span calculation
var mamaOutput = new double[count];
var famaOutput = new double[count];
Mama.Calculate(data, mamaOutput, famaOutput: famaOutput);
// Get values from instance method
var mama = new Mama();
for (int i = 0; i < count; i++)
{
mama.Update(new TValue(DateTime.UtcNow, data[i]));
// Both MAMA and FAMA should match between span and instance methods
Assert.Equal(mama.Last.Value, mamaOutput[i], precision: 8);
Assert.Equal(mama.Fama.Value, famaOutput[i], precision: 8);
}
}
[Fact]
public void Calculate_Span_AllModes_ProduceSameResult()
{
int count = 100;
var data = new double[count];
var gbm = new GBM(startPrice: 100, seed: 42);
for (int i = 0; i < count; i++) data[i] = gbm.Next().Close;
// 1. Streaming Mode (instance method)
var mama = new Mama();
var streamingMama = new double[count];
var streamingFama = new double[count];
for (int i = 0; i < count; i++)
{
mama.Update(new TValue(DateTime.UtcNow, data[i]));
streamingMama[i] = mama.Last.Value;
streamingFama[i] = mama.Fama.Value;
}
// 2. Span Mode (static method with FAMA)
var spanMama = new double[count];
var spanFama = new double[count];
Mama.Calculate(data, spanMama, famaOutput: spanFama);
// 3. Verify MAMA matches
for (int i = 0; i < count; i++)
{
Assert.Equal(streamingMama[i], spanMama[i], precision: 8);
}
// 4. Verify FAMA matches
for (int i = 0; i < count; i++)
{
Assert.Equal(streamingFama[i], spanFama[i], precision: 8);
}
}
}
+22 -4
View File
@@ -107,6 +107,12 @@ public sealed class Mama : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double NormalizeAngle(double angle)
{
// Guard against non-finite inputs to prevent infinite loop
if (!double.IsFinite(angle))
{
return 0.0; // Return neutral angle for invalid inputs
}
while (angle <= -Math.PI) angle += TwoPi;
while (angle > Math.PI) angle -= TwoPi;
return angle;
@@ -256,7 +262,7 @@ public sealed class Mama : 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)
{
@@ -270,13 +276,17 @@ public sealed class Mama : AbstractBase
return mama.Update(source);
}
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, double fastLimit = 0.5, double slowLimit = 0.05)
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, double fastLimit = 0.5, double slowLimit = 0.05, Span<double> famaOutput = default)
{
if (source.Length == 0) return;
if (output.Length < source.Length)
{
throw new ArgumentOutOfRangeException(nameof(output), "Output buffer must be at least as large as the input buffer.");
}
if (!famaOutput.IsEmpty && famaOutput.Length < source.Length)
{
throw new ArgumentOutOfRangeException(nameof(famaOutput), "FAMA output buffer must be at least as large as the input buffer.");
}
// Stack allocate buffers for high performance (size 8 for power of 2 masking)
// We need 7 elements, but 8 allows & 7 masking
@@ -290,9 +300,9 @@ public sealed class Mama : AbstractBase
int count = 0;
// State variables
double period = 0, mama = 0, sumPr = 0;
double period = 0, mama = 0, fama = 0, sumPr = 0;
double i2 = 0, q2 = 0, re = 0, im = 0, lastValidPrice = 0;
double p_period = 0, p_phase = 0, p_mama = 0;
double p_period = 0, p_phase = 0, p_mama = 0, p_fama = 0;
double p_i2 = 0, p_q2 = 0, p_re = 0, p_im = 0;
// Constants
@@ -408,6 +418,7 @@ public sealed class Mama : AbstractBase
// Final indicators
mama = alpha * priceBuffer[bufferIdx] + (1.0 - alpha) * p_mama;
fama = FamaAlphaFactor * alpha * mama + (1.0 - FamaAlphaFactor * alpha) * p_fama;
// Update previous state
p_i2 = i2;
@@ -417,6 +428,7 @@ public sealed class Mama : AbstractBase
p_period = period;
p_phase = phase;
p_mama = mama;
p_fama = fama;
}
else
{
@@ -424,6 +436,7 @@ public sealed class Mama : AbstractBase
sumPr += price;
double avg = count > 0 ? sumPr / count : price;
mama = avg;
fama = avg;
// Init simple state
smoothBuffer[bufferIdx] = 0;
@@ -433,6 +446,7 @@ public sealed class Mama : AbstractBase
// Set initial p_state
p_mama = avg;
p_fama = avg;
p_period = 0; // Initial period state
p_phase = 0;
@@ -441,6 +455,10 @@ public sealed class Mama : AbstractBase
}
output[i] = mama;
if (!famaOutput.IsEmpty)
{
famaOutput[i] = fama;
}
}
}
}