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
+2 -1
View File
@@ -45,7 +45,8 @@ public abstract class AbstractBase : ITValuePublisher
/// Initializes the indicator state using the provided history.
/// </summary>
/// <param name="source">Historical data</param>
public abstract void Prime(ReadOnlySpan<double> source);
/// <param name="step">Time interval between values (default: 1 second)</param>
public abstract void Prime(ReadOnlySpan<double> source, TimeSpan? step = null);
/// <summary>
/// Updates the indicator with a single value.
+1 -1
View File
@@ -152,7 +152,7 @@ public sealed class Rsi : AbstractBase
Update(args.Value, args.IsNew);
}
public override void Prime(ReadOnlySpan<double> source)
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
foreach (var value in source)
{
+1 -1
View File
@@ -178,7 +178,7 @@ public sealed class Beta : AbstractBase
throw new NotSupportedException("Beta requires two inputs (asset and market). Use Update(asset, market).");
}
public override void Prime(ReadOnlySpan<double> source)
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
throw new NotSupportedException("Beta requires two inputs (asset and market). Use Update(asset, market).");
}
+1 -1
View File
@@ -140,7 +140,7 @@ public sealed class Covariance : AbstractBase
throw new NotSupportedException("Covariance requires two inputs. Use Update(x, y).");
}
public override void Prime(ReadOnlySpan<double> source)
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
throw new NotSupportedException("Covariance requires two inputs. Use Update(x, y).");
}
+1 -1
View File
@@ -317,7 +317,7 @@ public sealed class LinReg : 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)
{
+1 -1
View File
@@ -67,7 +67,7 @@ public sealed class Median : AbstractBase
/// <summary>
/// Initializes the indicator state using the provided history.
/// </summary>
public override void Prime(ReadOnlySpan<double> source)
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
if (source.Length == 0) return;
+1 -1
View File
@@ -187,7 +187,7 @@ public sealed class Skew : AbstractBase
_sumCu = sumCu;
}
public override void Prime(ReadOnlySpan<double> source)
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
foreach (double value in source)
{
+1 -1
View File
@@ -97,7 +97,7 @@ public sealed class StdDev : AbstractBase
Last = default;
}
public override void Prime(ReadOnlySpan<double> source)
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
_variance.Prime(source);
// Update Last based on _variance.Last
+1 -1
View File
@@ -154,7 +154,7 @@ public sealed class Variance : AbstractBase
_buffer.RecalculateSum();
}
public override void Prime(ReadOnlySpan<double> source)
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
foreach (double value in source)
{
+4
View File
@@ -314,6 +314,10 @@ public class AlmaTests
double[] wrongSizeOutput = new double[3];
Assert.Throws<ArgumentException>(() => Alma.Calculate(source.AsSpan(), output.AsSpan(), 0));
Assert.Throws<ArgumentException>(() => Alma.Calculate(source.AsSpan(), output.AsSpan(), 3, sigma: 0));
Assert.Throws<ArgumentException>(() => Alma.Calculate(source.AsSpan(), output.AsSpan(), 3, sigma: -1));
Assert.Throws<ArgumentOutOfRangeException>(() => Alma.Calculate(source.AsSpan(), output.AsSpan(), 3, offset: -0.1));
Assert.Throws<ArgumentOutOfRangeException>(() => Alma.Calculate(source.AsSpan(), output.AsSpan(), 3, offset: 1.1));
Assert.Throws<ArgumentException>(() => Alma.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), 3));
}
+5 -1
View File
@@ -175,7 +175,7 @@ public sealed class Alma : AbstractBase, IDisposable
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)
{
@@ -237,6 +237,10 @@ public sealed class Alma : AbstractBase, IDisposable
{
if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period));
if (sigma <= 0)
throw new ArgumentException("Sigma must be greater than 0", nameof(sigma));
if (offset < 0 || offset > 1)
throw new ArgumentOutOfRangeException(nameof(offset), "Offset must be between 0 and 1");
if (source.Length != output.Length)
throw new ArgumentException("Source and output must have the same length", nameof(output));
+5 -1
View File
@@ -8,9 +8,13 @@ public class BesselTests
{
Assert.Throws<ArgumentException>(() => new Bessel(0));
Assert.Throws<ArgumentException>(() => new Bessel(-1));
Assert.Throws<ArgumentException>(() => new Bessel(1));
var bessel = new Bessel(14);
var bessel = new Bessel(2);
Assert.NotNull(bessel);
var bessel14 = new Bessel(14);
Assert.NotNull(bessel14);
}
[Fact]
+10 -14
View File
@@ -45,16 +45,14 @@ public sealed class Bessel : AbstractBase, IDisposable
/// <summary>
/// Creates Bessel filter with specified length.
/// </summary>
/// <param name="length">Cutoff length (must be > 0, internally clamped to at least 2).</param>
/// <param name="length">Cutoff length (must be >= 2 for 2nd-order filter stability).</param>
public Bessel(int length)
{
if (length <= 0)
throw new ArgumentException("Length must be greater than 0", nameof(length));
if (length < 2)
throw new ArgumentException("Length must be at least 2 for 2nd-order Bessel filter", nameof(length));
int safeLength = Math.Max(length, 2);
double a = Math.Exp(-Math.PI / safeLength);
double b = 2.0 * a * Math.Cos(1.738 * Math.PI / safeLength);
double a = Math.Exp(-Math.PI / length);
double b = 2.0 * a * Math.Cos(1.738 * Math.PI / length);
_c2 = b;
_c3 = -a * a;
_c1 = 1.0 - _c2 - _c3;
@@ -91,7 +89,7 @@ public sealed class Bessel : AbstractBase, IDisposable
public override bool IsHot => _state.IsHot;
public override void Prime(ReadOnlySpan<double> source)
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
if (source.Length == 0)
return;
@@ -288,8 +286,8 @@ public sealed class Bessel : AbstractBase, IDisposable
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int length)
{
if (length <= 0)
throw new ArgumentException("Length must be greater than 0", nameof(length));
if (length < 2)
throw new ArgumentException("Length must be at least 2 for 2nd-order Bessel filter", nameof(length));
if (source.Length != output.Length)
throw new ArgumentException("Source and output must have the same length", nameof(output));
@@ -297,10 +295,8 @@ public sealed class Bessel : AbstractBase, IDisposable
if (source.Length == 0)
return;
int safeLength = Math.Max(length, 2);
double a = Math.Exp(-Math.PI / safeLength);
double b = 2.0 * a * Math.Cos(1.738 * Math.PI / safeLength);
double a = Math.Exp(-Math.PI / length);
double b = 2.0 * a * Math.Cos(1.738 * Math.PI / length);
double c2 = b;
double c3 = -a * a;
double c1 = 1.0 - c2 - c3;
+1 -1
View File
@@ -65,7 +65,7 @@ public sealed class Bilateral : AbstractBase
public override bool IsHot => _buffer.IsFull;
public override void Prime(ReadOnlySpan<double> source)
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
if (source.Length == 0) return;
+1 -1
View File
@@ -61,7 +61,7 @@ public sealed class Blma : AbstractBase, IDisposable
_hasLast = false;
}
public override void Prime(ReadOnlySpan<double> source)
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
DateTime time = DateTime.UtcNow;
foreach (var value in source)
+18 -24
View File
@@ -48,22 +48,28 @@ public sealed class Butter : AbstractBase
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void CalculateCoefficients()
private static void ComputeCoefficients(int period, out double a1, out double a2, out double b0, out double b1, out double b2, out double invA0)
{
double omega = 2.0 * Math.PI / _period;
double omega = 2.0 * Math.PI / period;
double sinOmega = Math.Sin(omega);
double cosOmega = Math.Cos(omega);
double alpha = sinOmega / Math.Sqrt(2.0);
double a0 = 1.0 + alpha;
_a1 = -2.0 * cosOmega;
_a2 = 1.0 - alpha;
a1 = -2.0 * cosOmega;
a2 = 1.0 - alpha;
_b0 = (1.0 - cosOmega) / 2.0;
_b1 = 1.0 - cosOmega;
_b2 = (1.0 - cosOmega) / 2.0;
b0 = (1.0 - cosOmega) / 2.0;
b1 = 1.0 - cosOmega;
b2 = (1.0 - cosOmega) / 2.0;
_invA0 = 1.0 / a0;
invA0 = 1.0 / a0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void CalculateCoefficients()
{
ComputeCoefficients(_period, out _a1, out _a2, out _b0, out _b1, out _b2, out _invA0);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -79,12 +85,13 @@ public sealed class Butter : AbstractBase
Init();
}
public override void Prime(ReadOnlySpan<double> source)
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
TimeSpan interval = step ?? TimeSpan.FromSeconds(1);
DateTime baseTime = DateTime.UtcNow;
for (int i = 0; i < source.Length; i++)
{
Update(new TValue(baseTime.AddTicks(i), source[i]));
Update(new TValue(baseTime + interval * i, source[i]));
}
}
@@ -166,20 +173,7 @@ public sealed class Butter : AbstractBase
throw new ArgumentOutOfRangeException(nameof(destination), "Destination span must have length >= source length.");
}
double omega = 2.0 * Math.PI / period;
double sinOmega = Math.Sin(omega);
double cosOmega = Math.Cos(omega);
double alpha = sinOmega / Math.Sqrt(2.0);
double a0 = 1.0 + alpha;
double a1 = -2.0 * cosOmega;
double a2 = 1.0 - alpha;
double b0 = (1.0 - cosOmega) / 2.0;
double b1 = 1.0 - cosOmega;
double b2 = (1.0 - cosOmega) / 2.0;
double invA0 = 1.0 / a0;
ComputeCoefficients(period, out double a1, out double a2, out double b0, out double b1, out double b2, out double invA0);
double x1 = 0, x2 = 0;
double y1 = 0, y2 = 0;
+1 -1
View File
@@ -188,7 +188,7 @@ public sealed class Conv : AbstractBase, IDisposable
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)
{
+13 -6
View File
@@ -129,7 +129,12 @@ public sealed class Dema : AbstractBase, IDisposable
var sourceValues = source.Values;
// Use current state
// Capture pre-batch state for rollback
EmaState preBatch_s1 = _state1;
EmaState preBatch_s2 = _state2;
double preBatch_lastValid = _lastValidValue;
// Use current state for calculation
EmaState s1 = _state1;
EmaState s2 = _state2;
double lastValid = _lastValidValue;
@@ -156,19 +161,21 @@ public sealed class Dema : AbstractBase, IDisposable
vSpan[i] = 2 * e1 - e2;
}
// Update instance state
// Update instance state with post-batch values
_state1 = s1;
_state2 = s2;
_p_state1 = s1;
_p_state2 = s2;
_lastValidValue = lastValid;
_p_lastValidValue = lastValid;
// Preserve pre-batch state for rollback (isNew=false)
_p_state1 = preBatch_s1;
_p_state2 = preBatch_s2;
_p_lastValidValue = preBatch_lastValid;
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
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)
{
+1 -1
View File
@@ -95,7 +95,7 @@ public sealed class Dwma : AbstractBase
Update(args.Value, args.IsNew);
}
public override void Prime(ReadOnlySpan<double> source)
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
Reset();
foreach (var value in source)
+1 -1
View File
@@ -102,7 +102,7 @@ public sealed class Ema : AbstractBase
/// Initializes the indicator state using the provided history.
/// </summary>
/// <param name="source">Historical data</param>
public override void Prime(ReadOnlySpan<double> source)
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
if (source.Length == 0) return;
+1 -1
View File
@@ -122,7 +122,7 @@ public sealed class Hma : AbstractBase
Update(args.Value, args.IsNew);
}
public override void Prime(ReadOnlySpan<double> source)
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
foreach (var value in source)
{
+55 -32
View File
@@ -25,7 +25,11 @@ public sealed class Htit : AbstractBase
double I2, double Q2, double Re, double Im,
double Period, double SmoothPeriod,
double LastValidPrice, int Index
);
)
{
// Initialize LastValidPrice to NaN to detect first valid price
public State() : this(0, 0, 0, 0, 0, 0, double.NaN, 0) { }
}
private State _state;
private State _p_state;
@@ -59,7 +63,7 @@ public sealed class Htit : AbstractBase
_i1Buffer = new RingBuffer(8);
_q1Buffer = new RingBuffer(8);
_itBuffer = new RingBuffer(8);
Init();
}
@@ -77,14 +81,14 @@ public sealed class Htit : AbstractBase
{
_state = default;
_p_state = default;
_priceBuffer.Clear();
_smoothBuffer.Clear();
_detrenderBuffer.Clear();
_i1Buffer.Clear();
_q1Buffer.Clear();
_itBuffer.Clear();
Last = new TValue(DateTime.MinValue, double.NaN);
}
@@ -101,8 +105,15 @@ public sealed class Htit : AbstractBase
_state = _p_state;
}
// Handle non-finite input: skip processing if no valid price seen yet
if (!double.IsFinite(price))
{
// If we haven't seen a valid price yet, return NaN (early exit)
if (double.IsNaN(_state.LastValidPrice))
{
return double.NaN;
}
// Otherwise, use the last valid price
price = _state.LastValidPrice;
}
else
@@ -115,12 +126,13 @@ public sealed class Htit : AbstractBase
// Need enough data for smooth calculation (4 bars) + detrender (7 bars total lag)
if (_state.Index < 7)
{
// During warmup, propagate NaN if input is NaN
_smoothBuffer.Add(price, isNew);
_detrenderBuffer.Add(0, isNew);
_i1Buffer.Add(0, isNew);
_q1Buffer.Add(0, isNew);
_itBuffer.Add(price, isNew);
return price;
return price; // May be NaN if no valid input yet
}
// 1. Smooth Price
@@ -132,14 +144,14 @@ public sealed class Htit : AbstractBase
// In streaming, we use previous period from state
double prevPeriod = _p_state.Period;
double adj = (adjSlope * prevPeriod) + adjIntercept;
double detrender = (c1 * _smoothBuffer[^1] + c2 * _smoothBuffer[^3] - c2 * _smoothBuffer[^5] - c1 * _smoothBuffer[^7]) * adj;
_detrenderBuffer.Add(detrender, isNew);
// 3. In-Phase and Quadrature
double q1 = (c1 * _detrenderBuffer[^1] + c2 * _detrenderBuffer[^3] - c2 * _detrenderBuffer[^5] - c1 * _detrenderBuffer[^7]) * adj;
double i1 = _detrenderBuffer[^4];
_q1Buffer.Add(q1, isNew);
_i1Buffer.Add(i1, isNew);
@@ -207,10 +219,11 @@ public sealed class Htit : AbstractBase
// Need at least 12 bars total (Index > 11) to have valid IT history for smoothing
if (_state.Index >= 12)
{
// NaN will propagate if IT buffer contains NaN
return (4.0 * _itBuffer[^1] + 3.0 * _itBuffer[^2] + 2.0 * _itBuffer[^3] + _itBuffer[^4]) * 0.1;
}
return price;
return price; // May be NaN if no valid input yet
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -245,7 +258,7 @@ public sealed class Htit : AbstractBase
Update(args.Value, args.IsNew);
}
public override void Prime(ReadOnlySpan<double> source)
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
foreach (var value in source)
{
@@ -284,8 +297,9 @@ public sealed class Htit : AbstractBase
// State variables
double i2 = 0, q2 = 0, re = 0, im = 0;
double period = 0, smoothPeriod = 0;
double lastValidPrice = 0;
// Initialize to NaN to detect first valid price
double lastValidPrice = double.NaN;
// Previous state variables
double p_i2 = 0, p_q2 = 0, p_re = 0, p_im = 0;
double p_period = 0, p_smoothPeriod = 0;
@@ -296,9 +310,18 @@ public sealed class Htit : AbstractBase
for (int i = 0; i < source.Length; i++)
{
double price = source[i];
// Handle non-finite input: skip processing if no valid price seen yet
if (!double.IsFinite(price))
{
price = count > 0 ? lastValidPrice : 0.0;
// If we haven't seen a valid price yet, output NaN
if (double.IsNaN(lastValidPrice))
{
output[i] = double.NaN;
continue;
}
// Otherwise, use the last valid price
price = lastValidPrice;
}
else
{
@@ -315,25 +338,25 @@ public sealed class Htit : AbstractBase
if (count > 6)
{
// 1. Smooth Price
double smooth = (4.0 * priceBuffer[pIdx] +
3.0 * priceBuffer[(pIdx - 1) & Mask63] +
2.0 * priceBuffer[(pIdx - 2) & Mask63] +
double smooth = (4.0 * priceBuffer[pIdx] +
3.0 * priceBuffer[(pIdx - 1) & Mask63] +
2.0 * priceBuffer[(pIdx - 2) & Mask63] +
priceBuffer[(pIdx - 3) & Mask63]) * 0.1;
smoothBuffer[sIdx] = smooth;
// 2. Detrender
double adj = (adjSlope * p_period) + adjIntercept;
double detrender = (c1 * smoothBuffer[sIdx] +
c2 * smoothBuffer[(sIdx - 2) & Mask7] -
c2 * smoothBuffer[(sIdx - 4) & Mask7] -
double detrender = (c1 * smoothBuffer[sIdx] +
c2 * smoothBuffer[(sIdx - 2) & Mask7] -
c2 * smoothBuffer[(sIdx - 4) & Mask7] -
c1 * smoothBuffer[(sIdx - 6) & Mask7]) * adj;
detrenderBuffer[sIdx] = detrender;
// 3. In-Phase and Quadrature
double q1 = (c1 * detrender +
c2 * detrenderBuffer[(sIdx - 2) & Mask7] -
c2 * detrenderBuffer[(sIdx - 4) & Mask7] -
double q1 = (c1 * detrender +
c2 * detrenderBuffer[(sIdx - 2) & Mask7] -
c2 * detrenderBuffer[(sIdx - 4) & Mask7] -
c1 * detrenderBuffer[(sIdx - 6) & Mask7]) * adj;
q1Buffer[sIdx] = q1;
@@ -341,14 +364,14 @@ public sealed class Htit : AbstractBase
i1Buffer[sIdx] = i1;
// 4. Advance phases
double jI = (c1 * i1 +
c2 * i1Buffer[(sIdx - 2) & Mask7] -
c2 * i1Buffer[(sIdx - 4) & Mask7] -
double jI = (c1 * i1 +
c2 * i1Buffer[(sIdx - 2) & Mask7] -
c2 * i1Buffer[(sIdx - 4) & Mask7] -
c1 * i1Buffer[(sIdx - 6) & Mask7]) * adj;
double jQ = (c1 * q1 +
c2 * q1Buffer[(sIdx - 2) & Mask7] -
c2 * q1Buffer[(sIdx - 4) & Mask7] -
double jQ = (c1 * q1 +
c2 * q1Buffer[(sIdx - 2) & Mask7] -
c2 * q1Buffer[(sIdx - 4) & Mask7] -
c1 * q1Buffer[(sIdx - 6) & Mask7]) * adj;
// 5. Phasor addition
@@ -420,14 +443,14 @@ public sealed class Htit : AbstractBase
}
else
{
// Initialization
// Initialization - propagate NaN if no valid price yet
smoothBuffer[sIdx] = price;
detrenderBuffer[sIdx] = 0;
i1Buffer[sIdx] = 0;
q1Buffer[sIdx] = 0;
itBuffer[sIdx] = price;
output[i] = price;
output[i] = price; // May be NaN if no valid input yet
// Reset state variables
p_i2 = 0; p_q2 = 0; p_re = 0; p_im = 0;
p_period = 0; p_smoothPeriod = 0;
+1 -1
View File
@@ -294,7 +294,7 @@ public sealed class Jma : AbstractBase
private void Handle(object? sender, TValueEventArgs args) => Update(args.Value, args.IsNew);
public override void Prime(ReadOnlySpan<double> source)
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
foreach (var value in source)
{
+1 -1
View File
@@ -213,7 +213,7 @@ public sealed class Kama : AbstractBase
Update(args.Value, args.IsNew);
}
public override void Prime(ReadOnlySpan<double> source)
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
foreach (var value in source)
{
+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);
}
}
+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;
}
}
}
}
+1 -1
View File
@@ -147,7 +147,7 @@ public sealed class Mgdi : 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)
{
+16 -9
View File
@@ -65,14 +65,18 @@ public sealed class Pwma : AbstractBase
private void Handle(object? sender, TValueEventArgs e) => Update(e.Value, e.IsNew);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double GetValidValue(double input)
private double GetValidValue(double input, double lastValid)
{
if (double.IsFinite(input))
return double.IsFinite(input) ? input : lastValid;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void UpdateLastValidValue(double val)
{
if (double.IsFinite(val))
{
_state.LastValidValue = input;
return input;
_state.LastValidValue = val;
}
return _state.LastValidValue;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -124,7 +128,8 @@ public sealed class Pwma : AbstractBase
{
if (isNew)
{
double val = GetValidValue(input.Value);
double val = GetValidValue(input.Value, _state.LastValidValue);
UpdateLastValidValue(val);
UpdateState(val);
_state.LastInput = val;
_p_state = _state;
@@ -134,7 +139,7 @@ public sealed class Pwma : AbstractBase
{
_state = _p_state;
_buffer.CopyFrom(_p_buffer);
double val = GetValidValue(input.Value);
double val = GetValidValue(input.Value, _state.LastValidValue);
// Recalculate for the updated last value
// We can't easily use the O(1) update formula here because we are replacing the newest value,
@@ -152,6 +157,7 @@ public sealed class Pwma : AbstractBase
_state.PSum = Math.FusedMultiplyAdd((double)n * n, diff, _state.PSum);
_buffer.UpdateNewest(val);
UpdateLastValidValue(val);
}
double count = _buffer.Count;
@@ -206,7 +212,8 @@ public sealed class Pwma : AbstractBase
for (int i = startIndex; i < len; i++)
{
double val = GetValidValue(source.Values[i]);
double val = GetValidValue(source.Values[i], _state.LastValidValue);
UpdateLastValidValue(val);
UpdateState(val);
_state.LastInput = val;
}
@@ -218,7 +225,7 @@ public sealed class Pwma : 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)
{
+1 -1
View File
@@ -73,7 +73,7 @@ public sealed class Rma : AbstractBase
/// Initializes the indicator state using the provided history.
/// </summary>
/// <param name="source">Historical data</param>
public override void Prime(ReadOnlySpan<double> source)
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
_ema.Prime(source);
Last = _ema.Last;
+1 -1
View File
@@ -91,7 +91,7 @@ public sealed class Sma : AbstractBase
/// Efficiently processes only the last 'Period' values required to sync the buffer.
/// </summary>
/// <param name="source">Historical data (only the last 'period' is actually needed)</param>
public override void Prime(ReadOnlySpan<double> source)
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
if (source.Length == 0) return;
+24 -2
View File
@@ -70,7 +70,7 @@ public sealed class Ssf : AbstractBase
public Ssf(TSeries source, int period) : this(period)
{
Prime(source.Values);
if (source.Count > 0)
if (source.Count > 0 && double.IsFinite(Last.Value))
{
Last = new TValue(source.LastTime, Last.Value);
}
@@ -81,7 +81,7 @@ public sealed class Ssf : AbstractBase
public override bool IsHot => _state.IsHot;
public override void Prime(ReadOnlySpan<double> source)
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
if (source.Length == 0) return;
@@ -105,6 +105,18 @@ public sealed class Ssf : AbstractBase
}
}
// Handle all-NaN case: if no finite value was found, set state to NaN and return
if (i == 0)
{
_state.LastValidValue = double.NaN;
_state.Ssf1 = double.NaN;
_state.Ssf2 = double.NaN;
_state.PrevInput = double.NaN;
Last = new TValue(DateTime.MinValue, double.NaN);
_p_state = _state;
return;
}
for (; i < len; i++)
{
double val = source[i];
@@ -234,6 +246,16 @@ public sealed class Ssf : AbstractBase
}
output[i] = double.NaN;
}
// Handle all-NaN case: if no finite value was found, set remaining outputs to NaN and return
if (i == len && state.Count == 0)
{
state.LastValidValue = double.NaN;
state.Ssf1 = double.NaN;
state.Ssf2 = double.NaN;
state.PrevInput = double.NaN;
return;
}
}
for (; i < len; i++)
+1 -1
View File
@@ -215,7 +215,7 @@ public sealed class Super : ITValuePublisher
UpperBand = new TValue(input.Time, upperBand);
LowerBand = new TValue(input.Time, lowerBand);
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = true });
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
return Last;
}
+95 -1
View File
@@ -171,6 +171,101 @@ public class T3Tests
Assert.Throws<ArgumentException>(() => new T3(-1));
}
[Fact]
public void Constructor_InvalidVFactor_NaN_ThrowsArgumentOutOfRangeException()
{
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new T3(5, double.NaN));
Assert.Equal("vfactor", ex.ParamName);
}
[Fact]
public void Constructor_InvalidVFactor_PositiveInfinity_ThrowsArgumentOutOfRangeException()
{
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new T3(5, double.PositiveInfinity));
Assert.Equal("vfactor", ex.ParamName);
}
[Fact]
public void Constructor_InvalidVFactor_NegativeInfinity_ThrowsArgumentOutOfRangeException()
{
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new T3(5, double.NegativeInfinity));
Assert.Equal("vfactor", ex.ParamName);
}
[Fact]
public void Constructor_InvalidVFactor_Zero_ThrowsArgumentOutOfRangeException()
{
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new T3(5, 0.0));
Assert.Equal("vfactor", ex.ParamName);
}
[Fact]
public void Constructor_InvalidVFactor_Negative_ThrowsArgumentOutOfRangeException()
{
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new T3(5, -0.5));
Assert.Equal("vfactor", ex.ParamName);
}
[Fact]
public void Constructor_InvalidVFactor_GreaterThanOne_ThrowsArgumentOutOfRangeException()
{
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new T3(5, 1.5));
Assert.Equal("vfactor", ex.ParamName);
}
[Fact]
public void Constructor_ValidVFactor_EdgeCases_DoesNotThrow()
{
// Smallest valid value just above 0
var t3_1 = new T3(5, 0.001);
Assert.NotNull(t3_1);
// Valid value of 1.0 (edge case)
var t3_2 = new T3(5, 1.0);
Assert.NotNull(t3_2);
// Typical valid values
var t3_3 = new T3(5, 0.5);
Assert.NotNull(t3_3);
var t3_4 = new T3(5, 0.7);
Assert.NotNull(t3_4);
}
[Fact]
public void BatchSpan_InvalidVFactor_NaN_ThrowsArgumentOutOfRangeException()
{
var input = new double[10];
var output = new double[10];
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => T3.Batch(input, output, 5, double.NaN));
Assert.Equal("vfactor", ex.ParamName);
}
[Fact]
public void BatchSpan_InvalidVFactor_Infinity_ThrowsArgumentOutOfRangeException()
{
var input = new double[10];
var output = new double[10];
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => T3.Batch(input, output, 5, double.PositiveInfinity));
Assert.Equal("vfactor", ex.ParamName);
}
[Fact]
public void BatchSpan_InvalidVFactor_OutOfRange_ThrowsArgumentOutOfRangeException()
{
var input = new double[10];
var output = new double[10];
var ex1 = Assert.Throws<ArgumentOutOfRangeException>(() => T3.Batch(input, output, 5, 0.0));
Assert.Equal("vfactor", ex1.ParamName);
var ex2 = Assert.Throws<ArgumentOutOfRangeException>(() => T3.Batch(input, output, 5, -0.5));
Assert.Equal("vfactor", ex2.ParamName);
var ex3 = Assert.Throws<ArgumentOutOfRangeException>(() => T3.Batch(input, output, 5, 1.5));
Assert.Equal("vfactor", ex3.ParamName);
}
private class TestPublisher : ITValuePublisher
{
public event TValuePublishedHandler? Pub;
@@ -222,4 +317,3 @@ public class T3Tests
Assert.Null(exception);
}
}
+9 -1
View File
@@ -53,6 +53,10 @@ public sealed class T3 : AbstractBase, IDisposable
{
if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period));
if (!double.IsFinite(vfactor))
throw new ArgumentOutOfRangeException(nameof(vfactor), "Volume factor must be a finite number (not NaN or Infinity)");
if (vfactor <= 0 || vfactor > 1)
throw new ArgumentOutOfRangeException(nameof(vfactor), "Volume factor must be greater than 0 and typically <= 1");
double alpha = 2.0 / (period + 1);
@@ -115,7 +119,7 @@ public sealed class T3 : AbstractBase, IDisposable
/// Initializes the indicator state using the provided history.
/// </summary>
/// <param name="source">Historical data</param>
public override void Prime(ReadOnlySpan<double> source)
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
if (source.Length == 0) return;
@@ -290,6 +294,10 @@ public sealed class T3 : AbstractBase, IDisposable
throw new ArgumentException("Period must be greater than 0", nameof(period));
if (source.Length != output.Length)
throw new ArgumentException("Source and output must have the same length", nameof(output));
if (!double.IsFinite(vfactor))
throw new ArgumentOutOfRangeException(nameof(vfactor), "Volume factor must be a finite number (not NaN or Infinity)");
if (vfactor <= 0 || vfactor > 1)
throw new ArgumentOutOfRangeException(nameof(vfactor), "Volume factor must be greater than 0 and typically <= 1");
double alpha = 2.0 / (period + 1);
double v = vfactor;
+1 -1
View File
@@ -91,7 +91,7 @@ public sealed class Tema : AbstractBase
/// Initializes the indicator state using the provided history.
/// </summary>
/// <param name="source">Historical data</param>
public override void Prime(ReadOnlySpan<double> source)
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
if (source.Length == 0) return;
+1 -1
View File
@@ -101,7 +101,7 @@ public sealed class Trima : AbstractBase, IDisposable
private void Handle(object? sender, TValueEventArgs args) => Update(args.Value, args.IsNew);
public override void Prime(ReadOnlySpan<double> source)
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
_sma1.Reset();
_sma2.Reset();
+5 -2
View File
@@ -77,7 +77,7 @@ public sealed class Usf : AbstractBase
public override bool IsHot => _state.IsHot;
public override void Prime(ReadOnlySpan<double> source)
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
if (source.Length == 0) return;
@@ -154,12 +154,15 @@ public sealed class Usf : AbstractBase
double val = GetValidValue(input.Value);
bool initialized = false;
if (_state.Count == 0)
{
_state.Usf1 = val;
_state.Usf2 = val;
_state.PrevInput1 = val;
_state.PrevInput2 = val;
_state.Count = 1;
initialized = true;
}
double usf = (_state.Count < 4)
@@ -171,7 +174,7 @@ public sealed class Usf : AbstractBase
_state.PrevInput2 = _state.PrevInput1;
_state.PrevInput1 = val;
if (isNew) _state.Count++;
if (isNew && !initialized) _state.Count++;
if (!_state.IsHot && _state.Count >= WarmupPeriod)
_state.IsHot = true;
+1 -1
View File
@@ -191,7 +191,7 @@ public sealed class Vidya : AbstractBase, IDisposable
return new TSeries(t, v);
}
public override void Prime(ReadOnlySpan<double> source)
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
if (source.Length == 0) return;
+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;
+1 -1
View File
@@ -77,7 +77,7 @@ public sealed class Atr : AbstractBase
/// This Prime method expects pre-calculated TR values or handles basic priming
/// if the user erroneously passes non-TR data. Ideally, use Batched TBarSeries.
/// </summary>
public override void Prime(ReadOnlySpan<double> source)
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
_rma.Prime(source);
Last = _rma.Last;