mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-08 05:57:43 +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:
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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).");
|
||||
}
|
||||
|
||||
@@ -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).");
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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++)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user