style patterns

This commit is contained in:
Miha Kralj
2026-01-25 16:01:45 -08:00
parent 2836f253c4
commit e59665c8f0
399 changed files with 6892 additions and 1323 deletions
+34
View File
@@ -100,7 +100,9 @@ public sealed class Bessel : AbstractBase
public Bessel(int length)
{
if (length < 2)
{
throw new ArgumentException("Length must be at least 2 for 2nd-order Bessel filter", nameof(length));
}
double a = Math.Exp(-Math.PI / length);
double b = 2.0 * a * Math.Cos(1.738 * Math.PI / length);
@@ -171,7 +173,9 @@ public sealed class Bessel : AbstractBase
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
if (source.Length == 0)
{
return;
}
Reset();
@@ -209,9 +213,13 @@ public sealed class Bessel : AbstractBase
{
double val = source[i];
if (double.IsFinite(val))
{
_state.LastValidValue = val;
}
else
{
val = _state.LastValidValue;
}
_state.F2 = _state.F1;
_state.F1 = val;
@@ -223,9 +231,13 @@ public sealed class Bessel : AbstractBase
{
double val = source[i];
if (double.IsFinite(val))
{
_state.LastValidValue = val;
}
else
{
val = _state.LastValidValue;
}
double filt = Math.FusedMultiplyAdd(_c3, _state.F2,
Math.FusedMultiplyAdd(_c2, _state.F1, _c1 * val));
@@ -236,7 +248,9 @@ public sealed class Bessel : AbstractBase
}
if (_state.Count >= WarmupPeriod)
{
_state.IsHot = true;
}
Last = new TValue(DateTime.MinValue, _state.F1);
@@ -310,7 +324,9 @@ public sealed class Bessel : AbstractBase
}
if (!_state.IsHot && _state.Count >= WarmupPeriod)
{
_state.IsHot = true;
}
Last = new TValue(input.Time, filt);
PubEvent(Last);
@@ -330,7 +346,9 @@ public sealed class Bessel : AbstractBase
public override TSeries Update(TSeries source)
{
if (source.Count == 0)
{
return [];
}
int len = source.Count;
var t = new List<long>(len);
@@ -404,9 +422,13 @@ public sealed class Bessel : AbstractBase
{
double val = source[i];
if (double.IsFinite(val))
{
state.LastValidValue = val;
}
else
{
val = state.LastValidValue;
}
state.F2 = state.F1;
state.F1 = val;
@@ -419,9 +441,13 @@ public sealed class Bessel : AbstractBase
{
double val = source[i];
if (double.IsFinite(val))
{
state.LastValidValue = val;
}
else
{
val = state.LastValidValue;
}
double filt = Math.FusedMultiplyAdd(c3, state.F2,
Math.FusedMultiplyAdd(c2, state.F1, c1 * val));
@@ -433,7 +459,9 @@ public sealed class Bessel : AbstractBase
}
if (!state.IsHot && state.Count >= warmupPeriod)
{
state.IsHot = true;
}
}
/// <summary>
@@ -479,13 +507,19 @@ public sealed class Bessel : AbstractBase
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int 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));
}
if (source.Length == 0)
{
return;
}
double a = Math.Exp(-Math.PI / length);
double b = 2.0 * a * Math.Cos(1.738 * Math.PI / length);
@@ -142,7 +142,10 @@ public sealed class BilateralValidationTests : IDisposable
_history.RemoveAt(0);
}
if (_history.Count == 0) return double.NaN;
if (_history.Count == 0)
{
return double.NaN;
}
double sigmaS = Math.Max(_length * _sigmaSRatio, 1e-10);
@@ -178,7 +181,10 @@ public sealed class BilateralValidationTests : IDisposable
private static double CalculateStDev(List<double> values)
{
if (values.Count < 2) return 0;
if (values.Count < 2)
{
return 0;
}
double avg = values.Average();
double sumSqDiff = values.Sum(d => (d - avg) * (d - avg));
+24 -4
View File
@@ -46,7 +46,9 @@ public sealed class Bilateral : AbstractBase
public Bilateral(int period, double sigmaSRatio = 0.5, double sigmaRMult = 1.0)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
_period = period;
_sigmaSRatio = sigmaSRatio;
@@ -74,7 +76,10 @@ public sealed class Bilateral : AbstractBase
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
if (source.Length == 0) return;
if (source.Length == 0)
{
return;
}
_buffer.Clear();
_state = default;
@@ -128,7 +133,10 @@ public sealed class Bilateral : AbstractBase
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
int len = source.Count;
var t = new System.Collections.Generic.List<long>(len);
@@ -337,10 +345,14 @@ public sealed class Bilateral : AbstractBase
public static void Calculate(ReadOnlySpan<double> source, Span<double> destination, int period, double sigmaSRatio = 0.5, double sigmaRMult = 1.0)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
if (destination.Length < source.Length)
{
throw new ArgumentException("Destination must have length >= source length", nameof(destination));
}
// Rent arrays for large periods to avoid heap allocations
double[]? rentedSpatialWeights = null;
@@ -424,7 +436,10 @@ public sealed class Bilateral : AbstractBase
int currentNewestIdx = windowIdx;
windowIdx = (windowIdx + 1) % period;
if (count < period) count++;
if (count < period)
{
count++;
}
// Calculate StDev
double invCount = 1.0 / count;
@@ -480,9 +495,14 @@ public sealed class Bilateral : AbstractBase
finally
{
if (rentedSpatialWeights != null)
{
ArrayPool<double>.Shared.Return(rentedSpatialWeights);
}
if (rentedWindow != null)
{
ArrayPool<double>.Shared.Return(rentedWindow);
}
}
}
@@ -514,4 +534,4 @@ public sealed class Bilateral : AbstractBase
}
base.Dispose(disposing);
}
}
}
+1 -1
View File
@@ -134,4 +134,4 @@ public class BpfIndicatorTests
Assert.Equal(60, indicator.LowerPeriod);
Assert.Equal(20, indicator.UpperPeriod);
}
}
}
+1 -1
View File
@@ -116,4 +116,4 @@ public class BpfTests
// Last value should be close to 0
Assert.True(Math.Abs(output[^1]) < 1e-6);
}
}
}
+2 -1
View File
@@ -20,7 +20,8 @@ public class BpfValidationTests
double[] sine15 = new double[T]; // Period 15 (Inside band, 10 < P < 40). Should pass.
double[] sine100 = new double[T]; // Period 100 (Too slow, should be blocked by HP(40)). HP(40) passes P < 40.
for (int i = 0; i < T; i++) {
for (int i = 0; i < T; i++)
{
sine5[i] = Math.Sin(2 * Math.PI * i / 5.0);
sine15[i] = Math.Sin(2 * Math.PI * i / 15.0);
sine100[i] = Math.Sin(2 * Math.PI * i / 100.0);
+16 -3
View File
@@ -53,9 +53,14 @@ public sealed class Bpf : AbstractBase
public Bpf(int lowerPeriod, int upperPeriod)
{
if (lowerPeriod < 1)
{
throw new ArgumentOutOfRangeException(nameof(lowerPeriod), "Lower period must be >= 1");
}
if (upperPeriod < 1)
{
throw new ArgumentOutOfRangeException(nameof(upperPeriod), "Upper period must be >= 1");
}
LowerPeriod = lowerPeriod;
UpperPeriod = upperPeriod;
@@ -95,7 +100,10 @@ public sealed class Bpf : AbstractBase
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
double[] values = source.Values.ToArray();
double[] results = new double[values.Length];
@@ -186,7 +194,9 @@ public sealed class Bpf : AbstractBase
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int lowerPeriod, int upperPeriod)
{
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output spans must be of the same length.", nameof(output));
}
// Coefficients
double sqrt2Pi = Math.Sqrt(2.0) * Math.PI;
@@ -213,7 +223,10 @@ public sealed class Bpf : AbstractBase
if (source.Length > 0)
{
lastValid = source[0];
if (!double.IsFinite(lastValid)) lastValid = 0;
if (!double.IsFinite(lastValid))
{
lastValid = 0;
}
}
for (int i = 0; i < source.Length; i++)
@@ -260,4 +273,4 @@ public sealed class Bpf : AbstractBase
}
base.Dispose(disposing);
}
}
}
+1 -1
View File
@@ -111,4 +111,4 @@ public class Cheby1Tests
Assert.Equal(iterativeResults[i], spanResults[i], 1e-10);
}
}
}
}
@@ -20,7 +20,10 @@ public class Cheby1ValidationTests
var filter = new Cheby1(20, 1.0); // Wn = 1/20
var input = new TSeries();
for (int i = 0; i < 100; i++) input.Add(new TValue(DateTime.UtcNow, 100)); // Impulse/Step
for (int i = 0; i < 100; i++)
{
input.Add(new TValue(DateTime.UtcNow, 100)); // Impulse/Step
}
var output = filter.Update(input);
+23 -3
View File
@@ -46,9 +46,14 @@ public sealed class Cheby1 : AbstractBase
public Cheby1(int period, double ripple = 1.0)
{
if (period < 2)
{
throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 2");
}
if (ripple <= 0)
{
throw new ArgumentOutOfRangeException(nameof(ripple), "Ripple must be > 0");
}
Period = period;
Ripple = ripple;
@@ -105,7 +110,10 @@ public sealed class Cheby1 : AbstractBase
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
double[] values = source.Values.ToArray();
double[] results = new double[values.Length];
@@ -178,7 +186,9 @@ public sealed class Cheby1 : AbstractBase
_state.Filt1 = filt;
if (_state.Count < 2)
{
_state.Count++;
}
}
Last = new TValue(input.Time, filt);
@@ -197,12 +207,19 @@ public sealed class Cheby1 : AbstractBase
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period, double ripple = 1.0)
{
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output spans must be of the same length.", nameof(output));
}
if (period < 2)
{
throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 2");
}
if (ripple <= 0)
{
throw new ArgumentOutOfRangeException(nameof(ripple), "Ripple must be > 0");
}
// Coefficients
double safeRipple = Math.Max(ripple, 0.01);
@@ -239,7 +256,10 @@ public sealed class Cheby1 : AbstractBase
if (source.Length > 0)
{
lastValid = source[0];
if (!double.IsFinite(lastValid)) lastValid = 0;
if (!double.IsFinite(lastValid))
{
lastValid = 0;
}
}
for (int i = 0; i < source.Length; i++)
@@ -285,4 +305,4 @@ public sealed class Cheby1 : AbstractBase
}
base.Dispose(disposing);
}
}
}
+1 -1
View File
@@ -109,4 +109,4 @@ public class Cheby2Tests
Assert.Equal(iterativeResults[i], spanResults[i], 1e-10);
}
}
}
}
@@ -21,9 +21,11 @@ public class Cheby2ValidationTests
Assert.True(Math.Abs(result1) > 0);
// Verify stability (should decay towards zero for lowpass IIR)
for(int i=0; i<50; i++)
for (int i = 0; i < 50; i++)
{
filter.Update(new TValue(DateTime.UtcNow, 0.0));
}
Assert.True(Math.Abs(filter.Last.Value) < 1e-5);
}
}
}
+22 -2
View File
@@ -46,9 +46,14 @@ public sealed class Cheby2 : AbstractBase
public Cheby2(int period, double attenuation = 5.0)
{
if (period < 2)
{
throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 2");
}
if (attenuation <= 0)
{
throw new ArgumentOutOfRangeException(nameof(attenuation), "Attenuation must be > 0");
}
Period = period;
Attenuation = attenuation;
@@ -123,7 +128,10 @@ public sealed class Cheby2 : AbstractBase
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
double[] values = source.Values.ToArray();
double[] results = new double[values.Length];
@@ -192,7 +200,9 @@ public sealed class Cheby2 : AbstractBase
_state.Filt1 = filt;
if (_state.Count < 2)
{
_state.Count++;
}
}
Last = new TValue(input.Time, filt);
@@ -211,12 +221,19 @@ public sealed class Cheby2 : AbstractBase
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period, double attenuation = 5.0)
{
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output spans must be of the same length.", nameof(output));
}
if (period < 2)
{
throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 2");
}
if (attenuation <= 0)
{
throw new ArgumentOutOfRangeException(nameof(attenuation), "Attenuation must be > 0");
}
// Coefficients
double safeAtten = Math.Max(attenuation, 0.1);
@@ -268,7 +285,10 @@ public sealed class Cheby2 : AbstractBase
if (source.Length > 0)
{
lastValid = source[0];
if (!double.IsFinite(lastValid)) lastValid = 0;
if (!double.IsFinite(lastValid))
{
lastValid = 0;
}
}
for (int i = 0; i < source.Length; i++)
@@ -109,4 +109,4 @@ public class EllipticIndicatorTests
indicator.Period = 50;
Assert.Equal(50, indicator.Period);
}
}
}
+1 -1
View File
@@ -101,4 +101,4 @@ public class EllipticTests
Assert.False(filter.IsHot);
Assert.Equal(0, filter.Last.Value);
}
}
}
@@ -49,9 +49,13 @@ public sealed class EllipticValidationTests : IDisposable
private static double StdDev(List<double> values)
{
if (values.Count < 2) return 0;
if (values.Count < 2)
{
return 0;
}
double avg = values.Average();
double sumSq = values.Sum(v => Math.Pow(v - avg, 2));
return Math.Sqrt(sumSq / (values.Count - 1));
}
}
}
+30 -6
View File
@@ -46,7 +46,9 @@ public sealed class Elliptic : AbstractBase
public Elliptic(int period)
{
if (period < 2)
{
throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 2");
}
Period = period;
Name = $"Elliptic({period})";
@@ -54,14 +56,20 @@ public sealed class Elliptic : AbstractBase
// Precompute coefficients based on hardcoded Rp=1dB, Rs=40dB
double Wc = Math.Tan(Math.PI / period);
if (Wc < 1e-9) Wc = 1e-9;
if (Wc < 1e-9)
{
Wc = 1e-9;
}
double omega_z_scaled = C_wz * Wc;
double sigma_scaled = C_sigma * Wc;
double Kp_scaled = C_Kp_norm * Wc * Wc;
double a0_denom = 1.0 - 2.0 * sigma_scaled + Kp_scaled;
if (Math.Abs(a0_denom) < 1e-9) a0_denom = 1e-9;
if (Math.Abs(a0_denom) < 1e-9)
{
a0_denom = 1e-9;
}
const double norm_factor = C_Kp_norm / (C_k * C_wz * C_wz);
@@ -111,7 +119,10 @@ public sealed class Elliptic : AbstractBase
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
double[] values = source.Values.ToArray();
double[] results = new double[values.Length];
@@ -206,21 +217,31 @@ public sealed class Elliptic : AbstractBase
endState = default;
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output spans must be of the same length.", nameof(output));
}
if (period < 2)
{
throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 2");
}
// Precompute coefficients based on hardcoded Rp=1dB, Rs=40dB
double Wc = Math.Tan(Math.PI / period);
if (Wc < 1e-9) Wc = 1e-9;
if (Wc < 1e-9)
{
Wc = 1e-9;
}
double omega_z_scaled = C_wz * Wc;
double sigma_scaled = C_sigma * Wc;
double Kp_scaled = C_Kp_norm * Wc * Wc;
double a0_denom = 1.0 - 2.0 * sigma_scaled + Kp_scaled;
if (Math.Abs(a0_denom) < 1e-9) a0_denom = 1e-9;
if (Math.Abs(a0_denom) < 1e-9)
{
a0_denom = 1e-9;
}
const double norm_factor = C_Kp_norm / (C_k * C_wz * C_wz);
@@ -249,7 +270,10 @@ public sealed class Elliptic : AbstractBase
if (source.Length > 0)
{
lastValid = source[0];
if (!double.IsFinite(lastValid)) lastValid = 0;
if (!double.IsFinite(lastValid))
{
lastValid = 0;
}
}
for (int i = 0; i < source.Length; i++)
+1 -1
View File
@@ -158,4 +158,4 @@ public class GaussIndicatorTests
indicator.Sigma = 2.0;
Assert.Equal(2.0, indicator.Sigma);
}
}
}
+6 -2
View File
@@ -85,8 +85,10 @@ public class GaussTests
Assert.False(double.IsNaN(result.Value));
// Fill full buffer with NaNs
for (int i=0; i<20; i++)
for (int i = 0; i < 20; i++)
{
gauss.Update(new TValue(DateTime.MinValue, double.NaN));
}
// Should evaluate to NaN if all are NaN
Assert.True(double.IsNaN(gauss.Last.Value));
@@ -100,11 +102,13 @@ public class GaussTests
Assert.False(gauss.IsHot);
for (int i = 0; i < 6; i++)
{
gauss.Update(new TValue(DateTime.MinValue, i));
}
Assert.False(gauss.IsHot);
gauss.Update(new TValue(DateTime.MinValue, 6));
Assert.True(gauss.IsHot);
}
}
}
+10 -2
View File
@@ -24,7 +24,11 @@ public class GaussValidationTests : IDisposable
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (_disposed)
{
return;
}
if (disposing)
{
_testData?.Dispose();
@@ -96,9 +100,13 @@ public class GaussValidationTests : IDisposable
}
if (wSum > 0)
{
result[i] = val / wSum;
}
else
{
result[i] = source[i]; // Fallback if no weights applied (shouldn't happen with valid sigma)
}
}
return result;
@@ -163,4 +171,4 @@ public class GaussValidationTests : IDisposable
}
_output.WriteLine("Span mode successfully validated against reference implementation");
}
}
}
+12 -1
View File
@@ -148,9 +148,13 @@ public sealed class Gauss : AbstractBase
}
if (wSum > 0)
{
result /= wSum;
}
else
{
result = input.Value;
}
}
else
{
@@ -187,7 +191,10 @@ public sealed class Gauss : AbstractBase
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
// Calculate using static method for performance
var resultValues = new double[source.Count];
@@ -294,9 +301,13 @@ public sealed class Gauss : AbstractBase
}
if (wSum > 0)
{
output[i] = result / wSum;
}
else
{
output[i] = double.NaN;
}
}
}
+1 -1
View File
@@ -156,4 +156,4 @@ public class HannIndicatorTests
indicator.Length = 10;
Assert.Equal(10, indicator.Length);
}
}
}
+6 -2
View File
@@ -141,11 +141,15 @@ public class HannTests
Assert.False(hann.IsHot);
// Fill it
for(int i=0; i<5; i++) hann.Update(new TValue(DateTime.UtcNow, 100));
for (int i = 0; i < 5; i++)
{
hann.Update(new TValue(DateTime.UtcNow, 100));
}
Assert.True(hann.IsHot);
hann.Reset();
Assert.False(hann.IsHot);
Assert.Equal(0, hann.Last.Value);
}
}
}
+9 -1
View File
@@ -23,7 +23,11 @@ public class HannValidationTests : IDisposable
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (_disposed)
{
return;
}
if (disposing)
{
_testData?.Dispose();
@@ -71,9 +75,13 @@ public class HannValidationTests : IDisposable
}
if (wSum > double.Epsilon)
{
result[i] = acc / wSum;
}
else
{
result[i] = source[i];
}
}
return result;
+4 -1
View File
@@ -150,7 +150,10 @@ public sealed class Hann : AbstractBase
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
var resultValues = new double[source.Count];
Calculate(source.Values, resultValues, Length);
+1 -1
View File
@@ -165,4 +165,4 @@ public class HpIndicatorTests
indicator.Lambda = 500;
Assert.Equal(500, indicator.Lambda);
}
}
}
+1 -1
View File
@@ -88,4 +88,4 @@ public class HpValidationTests : IDisposable
return result;
}
}
}
+10 -4
View File
@@ -149,7 +149,7 @@ public sealed class Hp : AbstractBase
}
else
{
_state.Trend = currentTrend;
_state.Trend = currentTrend;
}
Last = new TValue(input.Time, currentTrend);
@@ -159,7 +159,10 @@ public sealed class Hp : AbstractBase
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
var resultValues = new double[source.Count];
Calculate(source.Values, resultValues, Lambda);
@@ -203,7 +206,10 @@ public sealed class Hp : AbstractBase
throw new ArgumentException("Source and output spans must be of equal length.", nameof(output));
}
if (source.Length == 0) return;
if (source.Length == 0)
{
return;
}
double s = Math.Sqrt(lambda);
double alpha = (s * 0.5 - 1.0) / (s * 0.5 + 1.0);
@@ -242,4 +248,4 @@ public sealed class Hp : AbstractBase
}
base.Dispose(disposing);
}
}
}
+1 -1
View File
@@ -152,4 +152,4 @@ public class HpfIndicatorTests
indicator.Length = 50;
Assert.Equal(50, indicator.Length);
}
}
}
+28 -6
View File
@@ -36,7 +36,10 @@ public sealed class Hpf : AbstractBase
public Hpf(int length = 40)
{
if (length < 2) throw new ArgumentOutOfRangeException(nameof(length), "Length must be at least 2.");
if (length < 2)
{
throw new ArgumentOutOfRangeException(nameof(length), "Length must be at least 2.");
}
Length = length;
@@ -45,7 +48,9 @@ public sealed class Hpf : AbstractBase
double sinW = Math.Sin(omega);
if (Math.Abs(cosW) < 1e-15)
{
throw new ArgumentOutOfRangeException(nameof(length), "Length produces an unstable coefficient set (cos(ω)≈0).");
}
double a = (cosW + sinW - 1.0) / cosW;
double oneMinusA = 1.0 - a;
@@ -86,14 +91,22 @@ public sealed class Hpf : AbstractBase
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
foreach (double v in source)
{
Update(new TValue(DateTime.MinValue, v), isNew: true);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override TValue Update(TValue input, bool isNew = true)
{
if (isNew) _pState = _state;
else _state = _pState;
if (isNew)
{
_pState = _state;
}
else
{
_state = _pState;
}
double price = input.Value;
@@ -140,8 +153,8 @@ public sealed class Hpf : AbstractBase
double src1 = _state.Src1;
double src2 = _state.Src2;
double hp1 = _state.Hp1;
double hp2 = _state.Hp2;
double hp1 = _state.Hp1;
double hp2 = _state.Hp2;
double d2 = Math.FusedMultiplyAdd(-2.0, src1, price + src2);
@@ -161,7 +174,10 @@ public sealed class Hpf : AbstractBase
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
var output = new double[source.Count];
@@ -183,7 +199,9 @@ public sealed class Hpf : AbstractBase
Last = new TValue(times[^1], output[^1]);
for (int i = 0; i < source.Count; i++)
{
result.Add(new TValue(times[i], output[i]));
}
return result;
}
@@ -205,7 +223,9 @@ public sealed class Hpf : AbstractBase
out (double Hp1, double Hp2, double Src1, double Src2, int Samples, bool HasSrc) state)
{
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output spans must be of equal length.", nameof(output));
}
if (source.Length == 0)
{
@@ -219,7 +239,9 @@ public sealed class Hpf : AbstractBase
double sinW = Math.Sin(omega);
if (Math.Abs(cosW) < 1e-15)
{
throw new ArgumentOutOfRangeException(nameof(length), "Length produces an unstable coefficient set (cos(ω)≈0).");
}
double a = (cosW + sinW - 1.0) / cosW;
double oneMinusA = 1.0 - a;
+1 -1
View File
@@ -38,4 +38,4 @@ public class KalmanIndicatorTests
Assert.Equal(1, indicator.LinesSeries[0].Count);
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
}
}
}
+2 -2
View File
@@ -125,7 +125,7 @@ public class KalmanTests
// 3. Streaming Update
filter.Reset();
var streamingResults = new List<double>();
foreach(var item in series)
foreach (var item in series)
{
streamingResults.Add(filter.Update(item).Value);
}
@@ -136,4 +136,4 @@ public class KalmanTests
Assert.Equal(tseriesResult[i].Value, streamingResults[i], 1e-9);
}
}
}
}
@@ -85,4 +85,4 @@ public class KalmanValidationTests : IDisposable
Assert.Equal(expected[i], actual[i], 1e-9);
}
}
}
}
+27 -5
View File
@@ -71,8 +71,15 @@ public sealed class Kalman : AbstractBase
/// <exception cref="ArgumentOutOfRangeException">Thrown when q or r are not positive.</exception>
public Kalman(double q = 0.01, double r = 0.1)
{
if (q <= 0) throw new ArgumentOutOfRangeException(nameof(q), "q must be positive.");
if (r <= 0) throw new ArgumentOutOfRangeException(nameof(r), "r must be positive.");
if (q <= 0)
{
throw new ArgumentOutOfRangeException(nameof(q), "q must be positive.");
}
if (r <= 0)
{
throw new ArgumentOutOfRangeException(nameof(r), "r must be positive.");
}
ProcessNoise = q;
MeasurementNoise = r;
@@ -112,8 +119,14 @@ public sealed class Kalman : AbstractBase
public override TValue Update(TValue input, bool isNew = true)
{
// "rewind" behavior for isNew=false
if (isNew) _pState = _state;
else _state = _pState;
if (isNew)
{
_pState = _state;
}
else
{
_state = _pState;
}
double z = input.Value;
@@ -164,7 +177,10 @@ public sealed class Kalman : AbstractBase
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
var output = new double[source.Count];
@@ -174,7 +190,9 @@ public sealed class Kalman : AbstractBase
var result = new TSeries();
var times = source.Times;
for (int i = 0; i < source.Count; i++)
{
result.Add(new TValue(times[i], output[i]));
}
_state = new State { X = endX, P = endP, Samples = endSamples };
_pState = _state;
@@ -186,7 +204,9 @@ public sealed class Kalman : AbstractBase
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
foreach (double v in source)
{
Update(new TValue(DateTime.MinValue, v), isNew: true);
}
}
/// <summary>
@@ -203,7 +223,9 @@ public sealed class Kalman : AbstractBase
out int endSamples)
{
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output must be same length.", nameof(output));
}
double x = 0.0;
double p = 0.0;
+2 -2
View File
@@ -61,7 +61,7 @@ public class LoessValidationTests : IDisposable
// Check after warmup
for (int i = 10; i < 50; i++)
{
Assert.Equal(expected[i], actual[i], 1e-6);
Assert.Equal(expected[i], actual[i], 1e-6);
}
}
}
}
+37 -12
View File
@@ -156,7 +156,10 @@ public sealed class Loess : AbstractBase
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
// Use static Calculate for performance on the whole series
var resultValues = new double[source.Count];
@@ -175,13 +178,18 @@ public sealed class Loess : AbstractBase
// Restore Snap history if possible
if (startup > 0)
{
double lastFinite = 0;
bool found = false;
for(int k=startup-1; k>=0; k--)
{
if (double.IsFinite(source.Values[k])) { lastFinite = source.Values[k]; found=true; break; }
}
if(found) { _snap.LastFiniteInput = lastFinite; _snap.HasFiniteInput = true; _pSnap = _snap; }
double lastFinite = 0;
bool found = false;
for (int k = startup - 1; k >= 0; k--)
{
if (double.IsFinite(source.Values[k]))
{
lastFinite = source.Values[k];
found = true;
break;
}
}
if (found) { _snap.LastFiniteInput = lastFinite; _snap.HasFiniteInput = true; _pSnap = _snap; }
}
for (int i = startup; i < source.Count; i++)
@@ -213,7 +221,10 @@ public sealed class Loess : AbstractBase
for (int i = 0; i < period; i++)
{
double dist = Math.Abs(i - halfWindow) / bandwidth;
if (dist >= 1.0) dist = 0.9999;
if (dist >= 1.0)
{
dist = 0.9999;
}
double t = 1.0 - dist * dist * dist;
double w = t * t * t;
@@ -226,14 +237,21 @@ public sealed class Loess : AbstractBase
}
double delta = weightSum * x2Sum - xSum * xSum;
if (Math.Abs(delta) < double.Epsilon) delta = 1.0;
if (Math.Abs(delta) < double.Epsilon)
{
delta = 1.0;
}
double targetX = -halfWindow;
for (int i = 0; i < period; i++)
{
double dist = Math.Abs(i - halfWindow) / bandwidth;
if (dist >= 1.0) dist = 0.9999;
if (dist >= 1.0)
{
dist = 0.9999;
}
double t = 1.0 - dist * dist * dist;
double w = t * t * t;
double xi = i - halfWindow;
@@ -252,7 +270,10 @@ public sealed class Loess : AbstractBase
{
// a and b expected to be same length (slice called in Update ensures this)
int length = a.Length;
if (length == 0) return 0;
if (length == 0)
{
return 0;
}
int i = 0;
double sum = 0;
@@ -296,10 +317,14 @@ public sealed class Loess : AbstractBase
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period)
{
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output spans must be of equal length.", nameof(output));
}
if (period < 3)
{
throw new ArgumentOutOfRangeException(nameof(period), "Period must be at least 3.");
}
int adjPeriod = (period & 1) == 0 ? period + 1 : period;
+1 -1
View File
@@ -291,4 +291,4 @@ public class NotchIndicatorTests
$"Value at index {i} should be finite");
}
}
}
}
+3 -3
View File
@@ -74,7 +74,7 @@ public class NotchTests
double output = 0;
// Warmup to stabilize (IIR transient)
for(int i=0; i<100; i++)
for (int i = 0; i < 100; i++)
{
output = notch.Update(new TValue(DateTime.UtcNow, input)).Value;
}
@@ -101,7 +101,7 @@ public class NotchTests
if (i > 50) // ignore transient
{
maxAmp = Math.Max(maxAmp, Math.Abs(outVal));
maxAmp = Math.Max(maxAmp, Math.Abs(outVal));
}
}
@@ -109,4 +109,4 @@ public class NotchTests
// With Q=5, it should be very small.
Assert.True(maxAmp < 0.1, $"Amplitude {maxAmp} should be attenuated ( < 0.1 )");
}
}
}
+2 -2
View File
@@ -40,10 +40,10 @@ public class NotchValidationTests
double[] expected = { 0.5, 0.0, 0.0, 0.0, 0.0, 0.0 };
for(int i=0; i<input.Length; i++)
for (int i = 0; i < input.Length; i++)
{
double val = notch.Update(input[i]).Value;
Assert.Equal(expected[i], val, precision: 9);
}
}
}
}
+1 -1
View File
@@ -160,4 +160,4 @@ public class SgfIndicatorTests
Assert.Equal(10, indicator.Period);
Assert.Equal(4, indicator.PolyOrder);
}
}
}
+1 -1
View File
@@ -93,4 +93,4 @@ public class SgfTests
Assert.True(sgf.IsHot);
}
}
}
+11 -1
View File
@@ -24,7 +24,11 @@ public class SgfValidationTests : IDisposable
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (_disposed)
{
return;
}
if (disposing)
{
_testData?.Dispose();
@@ -104,11 +108,17 @@ public class SgfValidationTests : IDisposable
}
if (wSum > double.Epsilon)
{
result[i] = val / wSum;
}
else if (wSum <= double.Epsilon && wSum > -double.Epsilon)
{
result[i] = double.NaN;
}
else // Negative or small sum fallback usually just NaN or raw
{
result[i] = ((i + 1) < adjPeriod) ? source[i] : double.NaN; // Match Sgf.cs partial window fallback logic roughly
}
if (wSum <= double.Epsilon)
{
+8 -1
View File
@@ -129,9 +129,13 @@ public sealed class Sgf : AbstractBase
}
if (wSum > double.Epsilon)
{
result /= wSum;
}
else
{
result = input.Value;
}
}
else
{
@@ -166,7 +170,10 @@ public sealed class Sgf : AbstractBase
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
var resultValues = new double[source.Count];
Calculate(source.Values, resultValues, _period, _polyOrder);
+37 -4
View File
@@ -40,7 +40,9 @@ public sealed class Ssf : AbstractBase
public Ssf(int period)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
// Use high precision constants
// Note: Some implementations (like Ooples/PineScript) use 1.414 * 3.14159 which causes divergence
@@ -88,7 +90,10 @@ public sealed class Ssf : AbstractBase
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
if (source.Length == 0) return;
if (source.Length == 0)
{
return;
}
Reset();
@@ -126,9 +131,13 @@ public sealed class Ssf : AbstractBase
{
double val = source[i];
if (double.IsFinite(val))
{
_state.LastValidValue = val;
}
else
{
val = _state.LastValidValue;
}
double ssf = (_state.Count < 4)
? val
@@ -142,7 +151,9 @@ public sealed class Ssf : AbstractBase
}
if (_state.Count >= WarmupPeriod)
{
_state.IsHot = true;
}
Last = new TValue(DateTime.MinValue, _state.Ssf1);
@@ -190,9 +201,15 @@ public sealed class Ssf : AbstractBase
_state.Ssf1 = ssf;
_state.PrevInput = val;
if (isNew) _state.Count++;
if (isNew)
{
_state.Count++;
}
if (!_state.IsHot && _state.Count >= WarmupPeriod)
{
_state.IsHot = true;
}
Last = new TValue(input.Time, ssf);
PubEvent(Last, isNew);
@@ -201,7 +218,10 @@ public sealed class Ssf : AbstractBase
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
int len = source.Count;
var t = new List<long>(len);
@@ -269,9 +289,13 @@ public sealed class Ssf : AbstractBase
{
double val = source[i];
if (double.IsFinite(val))
{
state.LastValidValue = val;
}
else
{
val = state.LastValidValue;
}
double ssf = (state.Count < 4)
? val
@@ -286,7 +310,9 @@ public sealed class Ssf : AbstractBase
}
if (!state.IsHot && state.Count >= warmupPeriod)
{
state.IsHot = true;
}
}
public static (TSeries Results, Ssf Indicator) Calculate(TSeries source, int period)
@@ -300,7 +326,9 @@ public sealed class Ssf : AbstractBase
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
double sqrt2_pi = Math.Sqrt(2) * Math.PI;
double arg = sqrt2_pi / period;
@@ -311,9 +339,14 @@ public sealed class Ssf : AbstractBase
double c1 = 1.0 - c2 - c3;
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output must have the same length", nameof(output));
}
if (source.Length == 0) return;
if (source.Length == 0)
{
return;
}
var state = State.New();
+2
View File
@@ -48,7 +48,9 @@ public sealed class UsfIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
if (args.Reason != UpdateReason.NewBar && args.Reason != UpdateReason.HistoricalBar)
{
return;
}
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
TValue result = _ma.Update(new TValue(item.TimeLeft.Ticks, _priceSelector(item)), args.IsNewBar());
+4
View File
@@ -413,7 +413,9 @@ public class UsfTests
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
for (int i = 0; i < source.Length; i++)
{
source[i] = gbm.Next().Close;
}
// Warm up
Usf.Calculate(source.AsSpan(), output.AsSpan(), 100);
@@ -509,7 +511,9 @@ public class UsfTests
{
var series = new TSeries();
for (int i = 1; i <= 10; i++)
{
series.Add(DateTime.UtcNow, i * 10);
}
var (results, indicator) = Usf.Calculate(series, 5);
+5 -1
View File
@@ -209,7 +209,11 @@ public sealed class UsfValidationTests : IDisposable
private static double CalculateVariance(List<double> values)
{
if (values.Count == 0) return 0;
if (values.Count == 0)
{
return 0;
}
double mean = values.Average();
return values.Sum(v => (v - mean) * (v - mean)) / values.Count;
}
+37 -4
View File
@@ -42,7 +42,9 @@ public sealed class Usf : AbstractBase
public Usf(int period)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
double sqrt2_pi = Math.Sqrt(2) * Math.PI;
double arg = sqrt2_pi / period;
@@ -90,7 +92,10 @@ public sealed class Usf : AbstractBase
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
if (source.Length == 0) return;
if (source.Length == 0)
{
return;
}
Reset();
@@ -117,9 +122,13 @@ public sealed class Usf : AbstractBase
{
double val = source[i];
if (double.IsFinite(val))
{
_state.LastValidValue = val;
}
else
{
val = _state.LastValidValue;
}
double usf = (_state.Count < 4)
? val
@@ -136,7 +145,9 @@ public sealed class Usf : AbstractBase
}
if (_state.Count >= WarmupPeriod)
{
_state.IsHot = true;
}
Last = new TValue(DateTime.MinValue, _state.Usf1);
@@ -191,9 +202,15 @@ public sealed class Usf : AbstractBase
_state.PrevInput2 = _state.PrevInput1;
_state.PrevInput1 = val;
if (isNew && !initialized) _state.Count++;
if (isNew && !initialized)
{
_state.Count++;
}
if (!_state.IsHot && _state.Count >= WarmupPeriod)
{
_state.IsHot = true;
}
Last = new TValue(input.Time, usf);
PubEvent(Last, isNew);
@@ -202,7 +219,10 @@ public sealed class Usf : AbstractBase
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
int len = source.Count;
var t = new List<long>(len);
@@ -266,9 +286,13 @@ public sealed class Usf : AbstractBase
{
double val = source[i];
if (double.IsFinite(val))
{
state.LastValidValue = val;
}
else
{
val = state.LastValidValue;
}
double usf = (state.Count < 4)
? val
@@ -286,7 +310,9 @@ public sealed class Usf : AbstractBase
}
if (!state.IsHot && state.Count >= warmupPeriod)
{
state.IsHot = true;
}
}
public static (TSeries Results, Usf Indicator) Calculate(TSeries source, int period)
@@ -300,7 +326,9 @@ public sealed class Usf : AbstractBase
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
double sqrt2_pi = Math.Sqrt(2) * Math.PI;
double arg = sqrt2_pi / period;
@@ -311,9 +339,14 @@ public sealed class Usf : AbstractBase
double c1 = (1.0 + c2 - c3) / 4.0;
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output must have the same length", nameof(output));
}
if (source.Length == 0) return;
if (source.Length == 0)
{
return;
}
var state = State.New();
+1 -1
View File
@@ -137,4 +137,4 @@ public class WienerIndicatorTests
Assert.Equal(20, indicator.Period);
Assert.Equal(5, indicator.SmoothPeriod);
}
}
}
@@ -24,7 +24,11 @@ public class WienerValidationTests : IDisposable
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (_disposed)
{
return;
}
if (disposing)
{
_testData?.Dispose();
@@ -182,4 +186,4 @@ public class WienerValidationTests : IDisposable
}
_output.WriteLine("Span mode successfully validated against reference implementation");
}
}
}