normalization of methods

This commit is contained in:
Miha Kralj
2026-02-10 21:33:16 -08:00
parent 915d7a007b
commit 6d6259a47d
527 changed files with 10525 additions and 2123 deletions
+5 -5
View File
@@ -28,11 +28,11 @@ public class BesselTests
double[] output = new double[5];
var exLength = Assert.Throws<ArgumentException>(() =>
Bessel.Calculate(source.AsSpan(), output.AsSpan(), 1));
Bessel.Batch(source.AsSpan(), output.AsSpan(), 1));
Assert.Equal("length", exLength.ParamName);
var exLengthZero = Assert.Throws<ArgumentException>(() =>
Bessel.Calculate(source.AsSpan(), output.AsSpan(), 0));
Bessel.Batch(source.AsSpan(), output.AsSpan(), 0));
Assert.Equal("length", exLengthZero.ParamName);
}
@@ -43,7 +43,7 @@ public class BesselTests
double[] wrongSizeOutput = new double[3];
var ex = Assert.Throws<ArgumentException>(() =>
Bessel.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), 14));
Bessel.Batch(source.AsSpan(), wrongSizeOutput.AsSpan(), 14));
Assert.Equal("output", ex.ParamName);
}
@@ -252,7 +252,7 @@ public class BesselTests
var tseriesResult = Bessel.Calculate(series, 14).Results;
Bessel.Calculate(source.AsSpan(), output.AsSpan(), 14);
Bessel.Batch(source.AsSpan(), output.AsSpan(), 14);
for (int i = 0; i < 100; i++)
{
@@ -276,7 +276,7 @@ public class BesselTests
var tValues = series.Values.ToArray();
var spanInput = new ReadOnlySpan<double>(tValues);
var spanOutput = new double[tValues.Length];
Bessel.Calculate(spanInput, spanOutput, length);
Bessel.Batch(spanInput, spanOutput, length);
double spanResult = spanOutput[^1];
// 3. Streaming Mode
@@ -47,7 +47,7 @@ public sealed class BesselValidationTests : IDisposable
// Same data via Span API
var src = _testData.Data.Values.ToArray();
var outSpan = new double[src.Length];
Bessel.Calculate(src.AsSpan(), outSpan.AsSpan(), length);
Bessel.Batch(src.AsSpan(), outSpan.AsSpan(), length);
// Verify last window for convergence and consistency
ValidationHelper.VerifyData(qResult, outSpan, lookback: 0, skip: length, tolerance: ValidationHelper.DefaultTolerance);
+8 -8
View File
@@ -481,12 +481,6 @@ public sealed class Bessel : AbstractBase
/// <para><b>Complexity:</b> O(n) where n = source.Count</para>
/// <para>The returned indicator maintains state and can continue processing new values.</para>
/// </remarks>
public static (TSeries Results, Bessel Indicator) Calculate(TSeries source, int length)
{
var bessel = new Bessel(length);
TSeries results = bessel.Update(source);
return (results, bessel);
}
/// <summary>
/// Calculates filtered values for a span of doubles (stateless batch processing).
@@ -504,7 +498,7 @@ public sealed class Bessel : AbstractBase
/// <para>SIMD optimization is not applicable due to IIR recursive data dependency.</para>
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int length)
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int length)
{
if (length < 2)
{
@@ -531,6 +525,12 @@ public sealed class Bessel : AbstractBase
CalculateCore(source, output, c1, c2, c3, length, ref state);
}
public static (TSeries Results, Bessel Indicator) Calculate(TSeries source, int length)
{
var bessel = new Bessel(length);
TSeries results = bessel.Update(source);
return (results, bessel);
}
/// <summary>
/// Event handler for reactive updates from subscribed publishers.
@@ -567,4 +567,4 @@ public sealed class Bessel : AbstractBase
}
base.Dispose(disposing);
}
}
}
+1 -1
View File
@@ -151,7 +151,7 @@ public class BilateralTests
var tValues = series.Values.ToArray();
var spanInput = new ReadOnlySpan<double>(tValues);
var spanOutput = new double[tValues.Length];
Bilateral.Calculate(spanInput, spanOutput, period);
Bilateral.Batch(spanInput, spanOutput, period);
double spanResult = spanOutput[^1];
// 3. Streaming Mode
@@ -96,7 +96,7 @@ public sealed class BilateralValidationTests : IDisposable
{
// Calculate QuanTAlib Bilateral (Span API)
double[] qOutput = new double[sourceData.Length];
global::QuanTAlib.Bilateral.Calculate(sourceData.AsSpan(), qOutput.AsSpan(), period, sigmaSRatio, sigmaRMult);
global::QuanTAlib.Bilateral.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period, sigmaSRatio, sigmaRMult);
// Calculate Reference Bilateral
var refResult = GetReferenceData(period, sigmaSRatio, sigmaRMult);
+9 -9
View File
@@ -332,17 +332,11 @@ public sealed class Bilateral : AbstractBase
/// <summary>
/// Calculates bilateral filter values for a TSeries and returns both results and a primed indicator.
/// </summary>
public static (TSeries Results, Bilateral Indicator) Calculate(TSeries source, int period, double sigmaSRatio = 0.5, double sigmaRMult = 1.0)
{
var indicator = new Bilateral(period, sigmaSRatio, sigmaRMult);
var results = indicator.Update(source);
return (results, indicator);
}
/// <summary>
/// Calculates bilateral filter values using spans (zero allocation in hot path).
/// </summary>
public static void Calculate(ReadOnlySpan<double> source, Span<double> destination, int period, double sigmaSRatio = 0.5, double sigmaRMult = 1.0)
private static void BatchCore(ReadOnlySpan<double> source, Span<double> destination, int period, double sigmaSRatio = 0.5, double sigmaRMult = 1.0)
{
if (period <= 0)
{
@@ -520,7 +514,13 @@ public sealed class Bilateral : AbstractBase
/// </summary>
public static void Batch(ReadOnlySpan<double> source, Span<double> destination, int period, double sigmaSRatio = 0.5, double sigmaRMult = 1.0)
{
Calculate(source, destination, period, sigmaSRatio, sigmaRMult);
BatchCore(source, destination, period, sigmaSRatio, sigmaRMult);
}
public static (TSeries Results, Bilateral Indicator) Calculate(TSeries source, int period, double sigmaSRatio = 0.5, double sigmaRMult = 1.0)
{
var indicator = new Bilateral(period, sigmaSRatio, sigmaRMult);
var results = indicator.Update(source);
return (results, indicator);
}
/// <summary>
@@ -534,4 +534,4 @@ public sealed class Bilateral : AbstractBase
}
base.Dispose(disposing);
}
}
}
+2 -2
View File
@@ -39,7 +39,7 @@ public class BpfTests
// 1. Span Mode
double[] spanOutput = new double[series.Count];
Bpf.Calculate(series.Values.ToArray(), spanOutput, lowerPeriod, upperPeriod);
Bpf.Batch(series.Values.ToArray(), spanOutput, lowerPeriod, upperPeriod);
// 2. TSeries Batch Mode
var bpfBatch = new Bpf(lowerPeriod, upperPeriod);
@@ -111,7 +111,7 @@ public class BpfTests
double[] input = Enumerable.Repeat(100.0, 500).ToArray();
double[] output = new double[500];
Bpf.Calculate(input, output, 10, 20);
Bpf.Batch(input, output, 10, 20);
// Last value should be close to 0
Assert.True(Math.Abs(output[^1]) < 1e-6);
+3 -3
View File
@@ -32,9 +32,9 @@ public class BpfValidationTests
double[] out100 = new double[T];
// Instantiate BPF with LowerPeriod=40 (HP cutoff), UpperPeriod=10 (LP cutoff)
Bpf.Calculate(sine5, out5, 40, 10);
Bpf.Calculate(sine15, out15, 40, 10);
Bpf.Calculate(sine100, out100, 40, 10);
Bpf.Batch(sine5, out5, 40, 10);
Bpf.Batch(sine15, out15, 40, 10);
Bpf.Batch(sine100, out100, 40, 10);
// Analysis of results (last 100 samples to avoid warmup)
double amp5 = GetAmplitude(out5);
+28 -14
View File
@@ -115,7 +115,7 @@ public sealed class Bpf : AbstractBase
double[] values = source.Values.ToArray();
double[] results = new double[values.Length];
Calculate(values, results, LowerPeriod, UpperPeriod);
Batch(values, results, LowerPeriod, UpperPeriod);
TSeries output = [];
for (int i = 0; i < values.Length; i++)
@@ -181,24 +181,15 @@ public sealed class Bpf : AbstractBase
return Last;
}
public override void Reset()
public static TSeries Batch(TSeries source, int lowerPeriod, int upperPeriod)
{
_state = default;
_state.LastValid = double.NaN;
_p_state = default;
Last = default;
var indicator = new Bpf(lowerPeriod, upperPeriod);
return indicator.Update(source);
}
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
foreach (double val in source)
{
Update(new TValue(DateTime.UtcNow, val), isNew: true);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int lowerPeriod, int upperPeriod)
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int lowerPeriod, int upperPeriod)
{
if (source.Length != output.Length)
{
@@ -267,6 +258,29 @@ public sealed class Bpf : AbstractBase
}
}
public override void Reset()
{
_state = default;
_state.LastValid = double.NaN;
_p_state = default;
Last = default;
}
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
foreach (double val in source)
{
Update(new TValue(DateTime.UtcNow, val), isNew: true);
}
}
public static (TSeries Results, Bpf Indicator) Calculate(TSeries source, int lowerPeriod, int upperPeriod)
{
var indicator = new Bpf(lowerPeriod, upperPeriod);
TSeries results = indicator.Update(source);
return (results, indicator);
}
/// <summary>
/// Unsubscribes from the source publisher if one was provided during construction.
/// </summary>
+2 -2
View File
@@ -21,7 +21,7 @@ public class ButterTests
{
var source = new double[10];
var destination = new double[5];
Assert.Throws<ArgumentOutOfRangeException>(() => Butter.Calculate(source, destination, 5, double.NaN));
Assert.Throws<ArgumentOutOfRangeException>(() => Butter.Batch(source, destination, 5, double.NaN));
}
[Fact]
@@ -79,7 +79,7 @@ public class ButterTests
var tValues = series.Values.ToArray();
var spanInput = new ReadOnlySpan<double>(tValues);
var spanOutput = new double[tValues.Length];
Butter.Calculate(spanInput, spanOutput, period, double.NaN);
Butter.Batch(spanInput, spanOutput, period, double.NaN);
double spanResult = spanOutput[^1];
// 3. Streaming Mode
+16 -3
View File
@@ -3,6 +3,7 @@ using System.Runtime.InteropServices;
namespace QuanTAlib;
[SkipLocalsInit]
public sealed class Butter : AbstractBase
{
private readonly int _period;
@@ -146,7 +147,7 @@ public sealed class Butter : AbstractBase
{
var result = new TSeries();
Span<double> output = new double[source.Count];
Calculate(source.Values, output, _period, double.NaN);
Batch(source.Values, output, _period, double.NaN);
for (int i = 0; i < source.Count; i++)
{
@@ -168,7 +169,13 @@ public sealed class Butter : AbstractBase
return result;
}
public static void Calculate(ReadOnlySpan<double> source, Span<double> destination, int period, double initialLast)
public static TSeries Batch(TSeries source, int period)
{
var indicator = new Butter(period);
return indicator.Update(source);
}
public static void Batch(ReadOnlySpan<double> source, Span<double> destination, int period, double initialLast)
{
if (period < 2)
{
@@ -216,6 +223,12 @@ public sealed class Butter : AbstractBase
destination[i] = y;
}
}
public static (TSeries Results, Butter Indicator) Calculate(TSeries source, int period)
{
var indicator = new Butter(period);
TSeries results = indicator.Update(source);
return (results, indicator);
}
/// <summary>
/// Unsubscribes from the source publisher if one was provided during construction.
@@ -228,4 +241,4 @@ public sealed class Butter : AbstractBase
}
base.Dispose(disposing);
}
}
}
+1 -1
View File
@@ -103,7 +103,7 @@ public class Cheby1Tests
// Span
var spanResults = new double[count];
Cheby1.Calculate(values, spanResults, period, 1.0);
Cheby1.Batch(values, spanResults, period, 1.0);
// Compare
for (int i = 0; i < count; i++)
+20 -6
View File
@@ -118,7 +118,7 @@ public sealed class Cheby1 : AbstractBase
double[] values = source.Values.ToArray();
double[] results = new double[values.Length];
Calculate(values, results, Period, Ripple);
Batch(values, results, Period, Ripple);
TSeries output = [];
for (int i = 0; i < values.Length; i++)
@@ -196,15 +196,15 @@ public sealed class Cheby1 : AbstractBase
return Last;
}
public override void Reset()
public static TSeries Batch(TSeries source, int period, double ripple = 1.0)
{
_state = default;
_p_state = default;
Last = default;
var indicator = new Cheby1(period, ripple);
return indicator.Update(source);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period, double ripple = 1.0)
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period, double ripple = 1.0)
{
if (source.Length != output.Length)
{
@@ -294,6 +294,20 @@ public sealed class Cheby1 : AbstractBase
}
}
public override void Reset()
{
_state = default;
_p_state = default;
Last = default;
}
public static (TSeries Results, Cheby1 Indicator) Calculate(TSeries source, int period, double ripple = 1.0)
{
var indicator = new Cheby1(period, ripple);
TSeries results = indicator.Update(source);
return (results, indicator);
}
/// <summary>
/// Unsubscribes from the source publisher if one was provided during construction.
/// </summary>
+1 -1
View File
@@ -101,7 +101,7 @@ public class Cheby2Tests
// Span
var spanResults = new double[count];
Cheby2.Calculate(values, spanResults, period, 5.0);
Cheby2.Batch(values, spanResults, period, 5.0);
// Compare
for (int i = 0; i < count; i++)
+15 -3
View File
@@ -136,7 +136,7 @@ public sealed class Cheby2 : AbstractBase
double[] values = source.Values.ToArray();
double[] results = new double[values.Length];
Calculate(values, results, Period, Attenuation);
Batch(values, results, Period, Attenuation);
TSeries output = [];
for (int i = 0; i < values.Length; i++)
@@ -217,8 +217,14 @@ public sealed class Cheby2 : AbstractBase
Last = default;
}
public static TSeries Batch(TSeries source, int period, double attenuation = 5.0)
{
var indicator = new Cheby2(period, attenuation);
return indicator.Update(source);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period, double attenuation = 5.0)
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period, double attenuation = 5.0)
{
if (source.Length != output.Length)
{
@@ -322,6 +328,12 @@ public sealed class Cheby2 : AbstractBase
filt1 = filt;
}
}
public static (TSeries Results, Cheby2 Indicator) Calculate(TSeries source, int period, double attenuation = 5.0)
{
var indicator = new Cheby2(period, attenuation);
TSeries results = indicator.Update(source);
return (results, indicator);
}
/// <summary>
/// Unsubscribes from the source publisher if one was provided during construction.
@@ -334,4 +346,4 @@ public sealed class Cheby2 : AbstractBase
}
base.Dispose(disposing);
}
}
}
+1 -1
View File
@@ -41,7 +41,7 @@ public class EllipticTests
// 3. Span
double[] spanInput = series.Values.ToArray();
double[] spanOutput = new double[spanInput.Length];
Elliptic.Calculate(spanInput, spanOutput, period);
Elliptic.Batch(spanInput, spanOutput, period);
// Assert
for (int i = 0; i < series.Count; i++)
+17 -3
View File
@@ -201,6 +201,19 @@ public sealed class Elliptic : AbstractBase
return Last;
}
public static TSeries Batch(TSeries source, int period)
{
var indicator = new Elliptic(period);
return indicator.Update(source);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period)
{
CalculateWithState(source, output, period, out _);
}
public override void Reset()
{
_state = default;
@@ -209,10 +222,11 @@ public sealed class Elliptic : AbstractBase
Last = default;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period)
public static (TSeries Results, Elliptic Indicator) Calculate(TSeries source, int period)
{
CalculateWithState(source, output, period, out _);
var indicator = new Elliptic(period);
TSeries results = indicator.Update(source);
return (results, indicator);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
+1 -1
View File
@@ -61,7 +61,7 @@ public class GaussTests
// Static calculate comparison
double[] output = new double[source.Count];
Gauss.Calculate(source.Values, output, 1.0);
Gauss.Batch(source.Values, output, 1.0);
for (int i = 0; i < source.Count; i++)
{
Assert.Equal(seriesResult[i].Value, output[i], 1e-9);
+1 -1
View File
@@ -163,7 +163,7 @@ public class GaussValidationTests : IDisposable
foreach (var sigma in sigmas)
{
double[] output = new double[source.Length];
Gauss.Calculate(source.AsSpan(), output.AsSpan(), sigma);
Gauss.Batch(source.AsSpan(), output.AsSpan(), sigma);
var expected = CalculateExpectedGauss(source, sigma);
+20 -7
View File
@@ -199,7 +199,7 @@ public sealed class Gauss : AbstractBase
// Calculate using static method for performance
var resultValues = new double[source.Count];
Calculate(source.Values, resultValues, _sigma);
Batch(source.Values, resultValues, _sigma);
// Convert to TSeries
var result = new TSeries();
@@ -219,12 +219,10 @@ public sealed class Gauss : AbstractBase
return result;
}
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
public static TSeries Batch(TSeries source, double sigma = 1.0)
{
foreach (double value in source)
{
Update(new TValue(DateTime.MinValue, value), isNew: true);
}
var indicator = new Gauss(sigma);
return indicator.Update(source);
}
/// <summary>
@@ -234,7 +232,7 @@ public sealed class Gauss : AbstractBase
/// <param name="output">Output buffer (must be same length as source)</param>
/// <param name="sigma">Standard deviation</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, double sigma)
public static void Batch(ReadOnlySpan<double> source, Span<double> output, double sigma)
{
const int StackallocThreshold = 256;
@@ -337,6 +335,21 @@ public sealed class Gauss : AbstractBase
}
}
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
foreach (double value in source)
{
Update(new TValue(DateTime.MinValue, value), isNew: true);
}
}
public static (TSeries Results, Gauss Indicator) Calculate(TSeries source, double sigma = 1.0)
{
var indicator = new Gauss(sigma);
TSeries results = indicator.Update(source);
return (results, indicator);
}
/// <summary>
/// Unsubscribes from the source publisher if one was provided during construction.
/// </summary>
+1 -1
View File
@@ -125,7 +125,7 @@ public class HannTests
}
var tseriesResult = new Hann(length).Update(series);
Hann.Calculate(input.AsSpan(), output.AsSpan(), length);
Hann.Batch(input.AsSpan(), output.AsSpan(), length);
for (int i = 0; i < 100; i++)
{
+1 -1
View File
@@ -136,7 +136,7 @@ public class HannValidationTests : IDisposable
foreach (var len in lengths)
{
double[] output = new double[source.Length];
Hann.Calculate(source.AsSpan(), output.AsSpan(), len);
Hann.Batch(source.AsSpan(), output.AsSpan(), len);
var expected = CalculateExpectedHann(source, len);
+21 -7
View File
@@ -9,6 +9,7 @@ namespace QuanTAlib;
/// This filter provides strong smoothing properties but introduces lag, as the weights
/// typically start and end at zero.
/// </summary>
[SkipLocalsInit]
public sealed class Hann : AbstractBase
{
private readonly double[] _weights;
@@ -156,7 +157,7 @@ public sealed class Hann : AbstractBase
}
var resultValues = new double[source.Count];
Calculate(source.Values, resultValues, Length);
Batch(source.Values, resultValues, Length);
// Convert to TSeries
var result = new TSeries();
@@ -176,12 +177,10 @@ public sealed class Hann : AbstractBase
return result;
}
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
public static TSeries Batch(TSeries source, int length)
{
foreach (double value in source)
{
Update(new TValue(DateTime.MinValue, value), isNew: true);
}
var indicator = new Hann(length);
return indicator.Update(source);
}
/// <summary>
@@ -191,7 +190,7 @@ public sealed class Hann : AbstractBase
/// <param name="output">Output buffer (must be same length as source)</param>
/// <param name="length">Lookback length</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int length)
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int length)
{
if (length <= 1)
{
@@ -249,6 +248,21 @@ public sealed class Hann : AbstractBase
}
}
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
foreach (double value in source)
{
Update(new TValue(DateTime.MinValue, value), isNew: true);
}
}
public static (TSeries Results, Hann Indicator) Calculate(TSeries source, int length)
{
var indicator = new Hann(length);
TSeries results = indicator.Update(source);
return (results, indicator);
}
/// <summary>
/// Unsubscribes from the source publisher if one was provided during construction.
/// </summary>
+2 -2
View File
@@ -68,7 +68,7 @@ public class HpTests
var tseriesResult = hp.Update(source);
var spanOutput = new double[source.Count];
Hp.Calculate(source.Values, spanOutput, 1600);
Hp.Batch(source.Values, spanOutput, 1600);
for (int i = 0; i < source.Count; i++)
{
@@ -116,6 +116,6 @@ public class HpTests
{
double[] src = new double[10];
double[] dst = new double[5];
Assert.Throws<ArgumentException>(() => Hp.Calculate(src, dst, 1600));
Assert.Throws<ArgumentException>(() => Hp.Batch(src, dst, 1600));
}
}
+15 -3
View File
@@ -165,7 +165,7 @@ public sealed class Hp : AbstractBase
}
var resultValues = new double[source.Count];
Calculate(source.Values, resultValues, Lambda);
Batch(source.Values, resultValues, Lambda);
var result = new TSeries();
var times = source.Times;
@@ -196,10 +196,16 @@ public sealed class Hp : AbstractBase
return result;
}
public static TSeries Batch(TSeries source, double lambda = 1600.0)
{
var indicator = new Hp(lambda);
return indicator.Update(source);
}
/// <summary>
/// Static calculation of HP Filter on a span.
/// </summary>
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, double lambda)
public static void Batch(ReadOnlySpan<double> source, Span<double> output, double lambda)
{
if (source.Length != output.Length)
{
@@ -236,6 +242,12 @@ public sealed class Hp : AbstractBase
prevTrend = val;
}
}
public static (TSeries Results, Hp Indicator) Calculate(TSeries source, double lambda = 1600.0)
{
var indicator = new Hp(lambda);
TSeries results = indicator.Update(source);
return (results, indicator);
}
/// <summary>
/// Unsubscribes from the source publisher if one was provided during construction.
@@ -248,4 +260,4 @@ public sealed class Hp : AbstractBase
}
base.Dispose(disposing);
}
}
}
+2 -2
View File
@@ -70,7 +70,7 @@ public class HpfTests
var tseriesResult = hpf.Update(source);
var spanOutput = new double[source.Count];
Hpf.Calculate(source.Values, spanOutput, 40, out _);
Hpf.Batch(source.Values, spanOutput, 40, out _);
for (int i = 0; i < source.Count; i++)
{
@@ -118,6 +118,6 @@ public class HpfTests
{
double[] src = new double[10];
double[] dst = new double[5];
Assert.Throws<ArgumentException>(() => Hpf.Calculate(src, dst, 40, out _));
Assert.Throws<ArgumentException>(() => Hpf.Batch(src, dst, 40, out _));
}
}
+17 -4
View File
@@ -181,7 +181,7 @@ public sealed class Hpf : AbstractBase
var output = new double[source.Count];
Calculate(source.Values, output, Length, out var endState);
Batch(source.Values, output, Length, out var endState);
_state = new State
{
@@ -206,17 +206,22 @@ public sealed class Hpf : AbstractBase
return result;
}
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int length)
public static TSeries Batch(TSeries source, int length = 40)
{
Calculate(source, output, length, out _);
var indicator = new Hpf(length);
return indicator.Update(source);
}
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int length)
{
Batch(source, output, length, out _);
}
/// <summary>
/// Batch HPF. Returns end state so callers can restore streaming state without replay.
/// NaN/Inf => carry-forward last finite source.
/// Outputs 0 for the first two finite samples, then runs the 2-pole recursion.
/// </summary>
public static void Calculate(
public static void Batch(
ReadOnlySpan<double> source,
Span<double> output,
int length,
@@ -304,6 +309,14 @@ public sealed class Hpf : AbstractBase
state = (hp1, hp2, src1, src2, samples, hasSrc);
}
public static (TSeries Results, Hpf Indicator) Calculate(TSeries source, int length = 40)
{
var indicator = new Hpf(length);
TSeries results = indicator.Update(source);
return (results, indicator);
}
/// <summary>
/// Unsubscribes from the source publisher if one was provided during construction.
/// </summary>
+1 -1
View File
@@ -120,7 +120,7 @@ public class KalmanTests
// 2. Span Calculate
var spanOutput = new double[series.Count];
Kalman.Calculate(series.Values.ToArray(), spanOutput, 0.01, 0.1);
Kalman.Batch(series.Values.ToArray(), spanOutput, 0.01, 0.1);
// 3. Streaming Update
filter.Reset();
+23 -10
View File
@@ -186,7 +186,7 @@ public sealed class Kalman : AbstractBase
var output = new double[source.Count];
Calculate(source.Values, output, ProcessNoise, MeasurementNoise,
Batch(source.Values, output, ProcessNoise, MeasurementNoise,
out double endX, out double endP, out int endSamples);
var result = new TSeries();
@@ -203,19 +203,17 @@ public sealed class Kalman : AbstractBase
return result;
}
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
public static TSeries Batch(TSeries source, double q = 0.01, double r = 0.1)
{
foreach (double v in source)
{
Update(new TValue(DateTime.MinValue, v), isNew: true);
}
var indicator = new Kalman(q, r);
return indicator.Update(source);
}
/// <summary>
/// Batch KF. Returns final state so instance can restore without replay.
/// NaN/Inf => prediction-only (hold x, p += q) once initialized.
/// </summary>
public static void Calculate(
public static void Batch(
ReadOnlySpan<double> source,
Span<double> output,
double q,
@@ -278,15 +276,30 @@ public sealed class Kalman : AbstractBase
// Overload for Calculate without out params to maintain API compatibility if needed,
// although the original Calculate signature was different anyway (returned void).
// The previous implementation had: public static void Calculate(ReadOnlySpan<double> source, Span<double> output, double q, double r)
// The previous implementation had: public static void Batch(ReadOnlySpan<double> source, Span<double> output, double q, double r)
// We should keep this signature valid.
/// <summary>
/// Static calculation of Kalman Filter on a span.
/// </summary>
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, double q, double r)
public static void Batch(ReadOnlySpan<double> source, Span<double> output, double q, double r)
{
Calculate(source, output, q, r, out _, out _, out _);
Batch(source, output, q, r, out _, out _, out _);
}
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
foreach (double v in source)
{
Update(new TValue(DateTime.MinValue, v), isNew: true);
}
}
public static (TSeries Results, Kalman Indicator) Calculate(TSeries source, double q = 0.01, double r = 0.1)
{
var indicator = new Kalman(q, r);
TSeries results = indicator.Update(source);
return (results, indicator);
}
/// <summary>
+1 -1
View File
@@ -106,7 +106,7 @@ public sealed class LoessTests
// 2. Span Batch
var resSpan = new double[data.Length];
Loess.Calculate(data.AsSpan(), resSpan.AsSpan(), period);
Loess.Batch(data.AsSpan(), resSpan.AsSpan(), period);
// 3. Streaming
var loessStream = new Loess(period);
+47 -34
View File
@@ -163,7 +163,7 @@ public sealed class Loess : AbstractBase
// Use static Calculate for performance on the whole series
var resultValues = new double[source.Count];
Calculate(source.Values, resultValues, Period);
Batch(source.Values, resultValues, Period);
var result = new TSeries();
var times = source.Times;
@@ -200,6 +200,48 @@ public sealed class Loess : AbstractBase
return result;
}
public static TSeries Batch(TSeries source, int period)
{
var indicator = new Loess(period);
return indicator.Update(source);
}
/// <summary>
/// Static stateless calculation optimized for SIMD.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Batch(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;
double[] kernel = new double[adjPeriod];
GenerateKernelOldestFirst(adjPeriod, kernel);
ReadOnlySpan<double> kSpan = new ReadOnlySpan<double>(kernel);
for (int i = 0; i < source.Length; i++)
{
if (i < adjPeriod - 1)
{
output[i] = source[i];
continue;
}
var window = source.Slice(i - adjPeriod + 1, adjPeriod);
output[i] = DotProduct(window, kSpan);
}
}
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
foreach (double value in source)
@@ -310,40 +352,11 @@ public sealed class Loess : AbstractBase
return sum;
}
/// <summary>
/// Static stateless calculation optimized for SIMD.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period)
public static (TSeries Results, Loess Indicator) Calculate(TSeries source, 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;
double[] kernel = new double[adjPeriod];
GenerateKernelOldestFirst(adjPeriod, kernel);
ReadOnlySpan<double> kSpan = new ReadOnlySpan<double>(kernel);
for (int i = 0; i < source.Length; i++)
{
if (i < adjPeriod - 1)
{
output[i] = source[i];
continue;
}
var window = source.Slice(i - adjPeriod + 1, adjPeriod);
output[i] = DotProduct(window, kSpan);
}
var indicator = new Loess(period);
TSeries results = indicator.Update(source);
return (results, indicator);
}
/// <summary>
+4 -4
View File
@@ -37,12 +37,12 @@ public class NotchTests
var data = _gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = data.Close;
// 1. Static Calculate (TSeries)
var staticResult = Notch.Calculate(series, period, q);
// 1. Static Batch(TSeries)
var staticResult = Notch.Batch(series, period, q);
// 2. Static Calculate (Span)
// 2. Static Batch(Span)
double[] spanResult = new double[series.Count];
Notch.Calculate(series.Values, spanResult.AsSpan(), period, q);
Notch.Batch(series.Values, spanResult.AsSpan(), period, q);
// 3. Instance Update (TSeries)
var instance = new Notch(period, q);
+11 -4
View File
@@ -138,7 +138,7 @@ public sealed class Notch : AbstractBase
ReadOnlySpan<double> srcSpan = source.Values;
double[] outArray = new double[srcSpan.Length];
Calculate(srcSpan, outArray.AsSpan(), NotchFreq, Bandwidth);
Batch(srcSpan, outArray.AsSpan(), NotchFreq, Bandwidth);
for (int i = 0; i < outArray.Length; i++)
{
@@ -159,7 +159,7 @@ public sealed class Notch : AbstractBase
return result;
}
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period, double q = 1.0)
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period, double q = 1.0)
{
if (source.Length != output.Length)
{
@@ -210,13 +210,13 @@ public sealed class Notch : AbstractBase
}
}
public static TSeries Calculate(TSeries source, int period, double q = 1.0)
public static TSeries Batch(TSeries source, int period, double q = 1.0)
{
var result = new TSeries();
ReadOnlySpan<double> srcSpan = source.Values;
double[] outArray = new double[srcSpan.Length];
Calculate(srcSpan, outArray.AsSpan(), period, q);
Batch(srcSpan, outArray.AsSpan(), period, q);
for (int i = 0; i < outArray.Length; i++)
{
@@ -226,6 +226,13 @@ public sealed class Notch : AbstractBase
return result;
}
public static (TSeries Results, Notch Indicator) Calculate(TSeries source, int period, double q = 1.0)
{
var indicator = new Notch(period, q);
TSeries results = indicator.Update(source);
return (results, indicator);
}
/// <summary>
/// Unsubscribes from the source publisher if one was provided during construction.
/// </summary>
+1 -1
View File
@@ -44,7 +44,7 @@ public class SgfTests
// 3. Span Mode
double[] spanInput = series.Values.ToArray();
double[] spanOutput = new double[spanInput.Length];
Sgf.Calculate(spanInput, spanOutput, period, polyOrder);
Sgf.Batch(spanInput, spanOutput, period, polyOrder);
// Assert
for (int i = 0; i < series.Count; i++)
+1 -1
View File
@@ -201,7 +201,7 @@ public class SgfValidationTests : IDisposable
foreach (var (period, order) in scenarios)
{
double[] output = new double[source.Length];
Sgf.Calculate(source.AsSpan(), output.AsSpan(), period, order);
Sgf.Batch(source.AsSpan(), output.AsSpan(), period, order);
var expected = CalculateExpectedSgf(source, period, order);
+20 -7
View File
@@ -177,7 +177,7 @@ public sealed class Sgf : AbstractBase
}
var resultValues = new double[source.Count];
Calculate(source.Values, resultValues, _period, _polyOrder);
Batch(source.Values, resultValues, _period, _polyOrder);
var result = new TSeries();
var times = source.Times;
@@ -197,16 +197,14 @@ public sealed class Sgf : AbstractBase
return result;
}
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
public static TSeries Batch(TSeries source, int period, int polyOrder = 2)
{
foreach (double value in source)
{
Update(new TValue(DateTime.MinValue, value), isNew: true);
}
var indicator = new Sgf(period, polyOrder);
return indicator.Update(source);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period, int polyOrder = 2)
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period, int polyOrder = 2)
{
if (source.Length != output.Length)
{
@@ -262,6 +260,21 @@ public sealed class Sgf : AbstractBase
}
}
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
foreach (double value in source)
{
Update(new TValue(DateTime.MinValue, value), isNew: true);
}
}
public static (TSeries Results, Sgf Indicator) Calculate(TSeries source, int period, int polyOrder = 2)
{
var indicator = new Sgf(period, polyOrder);
TSeries results = indicator.Update(source);
return (results, indicator);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void CalculateWeightsSpan(Span<double> weights, int period, int polyOrder)
{
+2 -2
View File
@@ -231,7 +231,7 @@ public class SsfTests
var tseriesResult = Ssf.Calculate(series, 10).Results;
// Calculate with Span API
Ssf.Calculate(source.AsSpan(), output.AsSpan(), 10);
Ssf.Batch(source.AsSpan(), output.AsSpan(), 10);
// Compare results
for (int i = 0; i < 100; i++)
@@ -257,7 +257,7 @@ public class SsfTests
var tValues = series.Values.ToArray();
var spanInput = new ReadOnlySpan<double>(tValues);
var spanOutput = new double[tValues.Length];
Ssf.Calculate(spanInput, spanOutput, period);
Ssf.Batch(spanInput, spanOutput, period);
double spanResult = spanOutput[^1];
// 3. Streaming Mode
+9 -9
View File
@@ -315,15 +315,8 @@ public sealed class Ssf : AbstractBase
}
}
public static (TSeries Results, Ssf Indicator) Calculate(TSeries source, int period)
{
var ssf = new Ssf(period);
TSeries results = ssf.Update(source);
return (results, ssf);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period)
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period)
{
if (period <= 0)
{
@@ -353,6 +346,13 @@ public sealed class Ssf : AbstractBase
CalculateCore(source, output, c1, c2, c3, period, ref state);
}
public static (TSeries Results, Ssf Indicator) Calculate(TSeries source, int period)
{
var ssf = new Ssf(period);
TSeries results = ssf.Update(source);
return (results, ssf);
}
public override void Reset()
{
_state = State.New();
@@ -371,4 +371,4 @@ public sealed class Ssf : AbstractBase
}
base.Dispose(disposing);
}
}
}
+7 -7
View File
@@ -317,7 +317,7 @@ public class UsfTests
var tValues = series.Values.ToArray();
var spanInput = new ReadOnlySpan<double>(tValues);
var spanOutput = new double[tValues.Length];
Usf.Calculate(spanInput, spanOutput, period);
Usf.Batch(spanInput, spanOutput, period);
double spanResult = spanOutput[^1];
// 3. Streaming Mode
@@ -370,11 +370,11 @@ public class UsfTests
double[] wrongSizeOutput = new double[3];
// Period must be > 0
Assert.Throws<ArgumentException>(() => Usf.Calculate(source.AsSpan(), output.AsSpan(), 0));
Assert.Throws<ArgumentException>(() => Usf.Calculate(source.AsSpan(), output.AsSpan(), -1));
Assert.Throws<ArgumentException>(() => Usf.Batch(source.AsSpan(), output.AsSpan(), 0));
Assert.Throws<ArgumentException>(() => Usf.Batch(source.AsSpan(), output.AsSpan(), -1));
// Output must be same length as source
Assert.Throws<ArgumentException>(() => Usf.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), 3));
Assert.Throws<ArgumentException>(() => Usf.Batch(source.AsSpan(), wrongSizeOutput.AsSpan(), 3));
}
[Fact]
@@ -396,7 +396,7 @@ public class UsfTests
var (tseriesResult, _) = Usf.Calculate(series, 10);
// Calculate with Span API
Usf.Calculate(source.AsSpan(), output.AsSpan(), 10);
Usf.Batch(source.AsSpan(), output.AsSpan(), 10);
// Compare results
for (int i = 0; i < 100; i++)
@@ -418,7 +418,7 @@ public class UsfTests
}
// Warm up
Usf.Calculate(source.AsSpan(), output.AsSpan(), 100);
Usf.Batch(source.AsSpan(), output.AsSpan(), 100);
// This test verifies the method runs without throwing
Assert.True(double.IsFinite(output[^1]));
@@ -430,7 +430,7 @@ public class UsfTests
double[] source = [100, 110, double.NaN, 120, 130];
double[] output = new double[5];
Usf.Calculate(source.AsSpan(), output.AsSpan(), 3);
Usf.Batch(source.AsSpan(), output.AsSpan(), 3);
// All outputs should be finite
foreach (var val in output)
+1 -1
View File
@@ -68,7 +68,7 @@ public sealed class UsfValidationTests : IDisposable
// 3. Span Mode
double[] sourceData = _testData.RawData.ToArray();
double[] spanOutput = new double[sourceData.Length];
Usf.Calculate(sourceData.AsSpan(), spanOutput.AsSpan(), period);
Usf.Batch(sourceData.AsSpan(), spanOutput.AsSpan(), period);
// Compare batch vs streaming
Assert.Equal(batchResult.Count, streamingResults.Count);
+9 -9
View File
@@ -315,15 +315,8 @@ public sealed class Usf : AbstractBase
}
}
public static (TSeries Results, Usf Indicator) Calculate(TSeries source, int period)
{
var usf = new Usf(period);
TSeries results = usf.Update(source);
return (results, usf);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period)
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period)
{
if (period <= 0)
{
@@ -353,6 +346,13 @@ public sealed class Usf : AbstractBase
CalculateCore(source, output, c1, c2, c3, period, ref state);
}
public static (TSeries Results, Usf Indicator) Calculate(TSeries source, int period)
{
var usf = new Usf(period);
TSeries results = usf.Update(source);
return (results, usf);
}
public override void Reset()
{
_state = State.New();
@@ -371,4 +371,4 @@ public sealed class Usf : AbstractBase
}
base.Dispose(disposing);
}
}
}
+1 -1
View File
@@ -44,7 +44,7 @@ public class WienerTests
// 3. Span Mode
double[] spanInput = series.Values.ToArray();
double[] spanOutput = new double[spanInput.Length];
Wiener.Calculate(spanInput, spanOutput, period, smoothPeriod);
Wiener.Batch(spanInput, spanOutput, period, smoothPeriod);
// Assert
for (int i = 0; i < series.Count; i++)
@@ -178,7 +178,7 @@ public class WienerValidationTests : IDisposable
foreach (var (period, smooth) in scenarios)
{
double[] output = new double[source.Length];
Wiener.Calculate(source.AsSpan(), output.AsSpan(), period, smooth);
Wiener.Batch(source.AsSpan(), output.AsSpan(), period, smooth);
var expected = CalculateExpectedWiener(source, period, smooth);
+24 -11
View File
@@ -77,6 +77,26 @@ public sealed class Wiener : AbstractBase
return result;
}
public static TSeries Batch(TSeries source, int period, int smoothPeriod = 10)
{
var indicator = new Wiener(period, smoothPeriod);
return indicator.Update(source);
}
public static void Batch(ReadOnlySpan<double> source, Span<double> destination, int period, int smoothPeriod = 10)
{
if (destination.Length < source.Length)
{
throw new ArgumentException("Destination span is shorter than source span.", nameof(destination));
}
var filter = new Wiener(period, smoothPeriod);
for (int i = 0; i < source.Length; i++)
{
destination[i] = filter.Update(new TValue(0, source[i])).Value;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double Calc()
{
@@ -154,17 +174,10 @@ public sealed class Wiener : AbstractBase
}
}
public static void Calculate(ReadOnlySpan<double> source, Span<double> destination, int period, int smoothPeriod = 10)
public static (TSeries Results, Wiener Indicator) Calculate(TSeries source, int period, int smoothPeriod = 10)
{
if (destination.Length < source.Length)
{
throw new ArgumentException("Destination span is shorter than source span.", nameof(destination));
}
var filter = new Wiener(period, smoothPeriod);
for (int i = 0; i < source.Length; i++)
{
destination[i] = filter.Update(new TValue(0, source[i])).Value;
}
var indicator = new Wiener(period, smoothPeriod);
TSeries results = indicator.Update(source);
return (results, indicator);
}
}