From 3b146b68bd515a1b98d8b508e10cf1d5ca1c2d98 Mon Sep 17 00:00:00 2001 From: Miha Kralj Date: Fri, 5 Dec 2025 10:33:27 -0800 Subject: [PATCH] Refactor tests and improve random number generation handling; update Dema, Ema, Sma, Tema, Wma, and GBM classes for consistency and clarity --- lib/averages/dema/Dema.Tests.cs | 13 +- lib/averages/ema/Ema.Tests.cs | 3 +- lib/averages/sma/Sma.Tests.cs | 5 +- lib/averages/sma/Sma.cs | 117 ++++++------ lib/averages/tema/Tema.Tests.cs | 3 +- lib/averages/tema/Tema.cs | 6 +- lib/averages/wma/Wma.Tests.cs | 5 +- lib/averages/wma/Wma.cs | 309 ++++++++++++++++---------------- lib/feeds/gbm/Gbm.Tests.cs | 12 +- lib/feeds/gbm/gbm.cs | 9 +- 10 files changed, 240 insertions(+), 242 deletions(-) diff --git a/lib/averages/dema/Dema.Tests.cs b/lib/averages/dema/Dema.Tests.cs index 735b3193..24661044 100644 --- a/lib/averages/dema/Dema.Tests.cs +++ b/lib/averages/dema/Dema.Tests.cs @@ -2,6 +2,7 @@ using Xunit; namespace QuanTAlib.Tests; +#pragma warning disable S2245 // Random is acceptable for simulation/testing purposes public class DemaTests { [Fact] @@ -12,7 +13,7 @@ public class DemaTests var dema = new Dema(period); var ema1 = new Ema(period); var ema2 = new Ema(period); - var r = new Random(123); + var r = new Random(123); // nosemgrep // Act & Assert for (int i = 0; i < 100; i++) @@ -36,7 +37,7 @@ public class DemaTests // Arrange int period = 10; var source = new TSeries(); - var r = new Random(123); + var r = new Random(123); // nosemgrep for (int i = 0; i < 100; i++) { source.Add(new TValue(DateTime.Now.AddMinutes(i), r.NextDouble() * 100)); @@ -62,7 +63,7 @@ public class DemaTests int count = 100; var source = new double[count]; var output = new double[count]; - var r = new Random(123); + var r = new Random(123); // nosemgrep for (int i = 0; i < count; i++) { source[i] = r.NextDouble() * 100; @@ -88,7 +89,7 @@ public class DemaTests double alpha = 2.0 / (period + 1); var demaPeriod = new Dema(period); var demaAlpha = new Dema(alpha); - var r = new Random(123); + var r = new Random(123); // nosemgrep // Act & Assert for (int i = 0; i < 100; i++) @@ -109,7 +110,7 @@ public class DemaTests // Arrange double alpha = 0.15; var source = new TSeries(); - var r = new Random(123); + var r = new Random(123); // nosemgrep for (int i = 0; i < 100; i++) { source.Add(new TValue(DateTime.Now.AddMinutes(i), r.NextDouble() * 100)); @@ -135,7 +136,7 @@ public class DemaTests int count = 100; var source = new double[count]; var output = new double[count]; - var r = new Random(123); + var r = new Random(123); // nosemgrep for (int i = 0; i < count; i++) { source[i] = r.NextDouble() * 100; diff --git a/lib/averages/ema/Ema.Tests.cs b/lib/averages/ema/Ema.Tests.cs index c1c1b41f..af5fe9da 100644 --- a/lib/averages/ema/Ema.Tests.cs +++ b/lib/averages/ema/Ema.Tests.cs @@ -1,5 +1,6 @@ namespace QuanTAlib.Tests; +#pragma warning disable S2245 // Random is acceptable for simulation/testing purposes public class EmaTests { [Fact] @@ -441,7 +442,7 @@ public class EmaTests { double[] source = new double[10000]; double[] output = new double[10000]; - var rng = new Random(42); + var rng = new Random(42); // nosemgrep for (int i = 0; i < source.Length; i++) source[i] = rng.NextDouble() * 100; diff --git a/lib/averages/sma/Sma.Tests.cs b/lib/averages/sma/Sma.Tests.cs index 12334267..d65e0948 100644 --- a/lib/averages/sma/Sma.Tests.cs +++ b/lib/averages/sma/Sma.Tests.cs @@ -1,5 +1,6 @@ namespace QuanTAlib.Tests; +#pragma warning disable S2245 // Random is acceptable for simulation/testing purposes public class SmaTests { [Fact] @@ -422,9 +423,9 @@ public class SmaTests { double[] source = new double[10000]; double[] output = new double[10000]; - var rng = new Random(42); + var rng = new Random(42); // nosemgrep for (int i = 0; i < source.Length; i++) - source[i] = rng.NextDouble() * 100; + source[i] = rng.NextDouble() * 100; // nosemgrep // Warm up Sma.Calculate(source.AsSpan(), output.AsSpan(), 100); diff --git a/lib/averages/sma/Sma.cs b/lib/averages/sma/Sma.cs index 3f2b5c15..c6ad658a 100644 --- a/lib/averages/sma/Sma.cs +++ b/lib/averages/sma/Sma.cs @@ -292,78 +292,75 @@ public sealed class Sma } [MethodImpl(MethodImplOptions.AggressiveOptimization)] -#pragma warning disable S6640 // Unsafe code is required for high-performance SIMD operations - private static unsafe void CalculateSimdCore(ReadOnlySpan source, Span output, int period) + private static void CalculateSimdCore(ReadOnlySpan source, Span output, int period) { int len = source.Length; const int VectorWidth = 4; - fixed (double* srcPtr = source) - fixed (double* outPtr = output) + ref double srcRef = ref MemoryMarshal.GetReference(source); + ref double outRef = ref MemoryMarshal.GetReference(output); + + double invPeriod = 1.0 / period; + + int warmupEnd = Math.Min(period, len); + double sum = 0; + for (int i = 0; i < warmupEnd; i++) { - double invPeriod = 1.0 / period; + sum += Unsafe.Add(ref srcRef, i); + Unsafe.Add(ref outRef, i) = sum / (i + 1); + } - int warmupEnd = Math.Min(period, len); - double sum = 0; - for (int i = 0; i < warmupEnd; i++) + if (len <= period) + return; + + var vInvPeriod = Vector256.Create(invPeriod); + var vZero = Vector256.Zero; + int simdEnd = period + ((len - period) / VectorWidth) * VectorWidth; + int tickCount = 0; + + for (int i = period; i < simdEnd; i += VectorWidth) + { + var vNew = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, i)); + var vOld = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, i - period)); + + var vDelta = Avx.Subtract(vNew, vOld); + + var vShift1 = Avx2.Permute4x64(vDelta.AsUInt64(), 0b_10_01_00_00).AsDouble(); + vShift1 = Avx.Blend(vZero, vShift1, 0b_1110); + var vP1 = Avx.Add(vDelta, vShift1); + + var vShift2 = Avx2.Permute4x64(vP1.AsUInt64(), 0b_01_00_00_00).AsDouble(); + vShift2 = Avx.Blend(vZero, vShift2, 0b_1100); + var vP2 = Avx.Add(vP1, vShift2); + + var vSumPrev = Vector256.Create(sum); + var vSums = Avx.Add(vSumPrev, vP2); + + var vResult = Avx.Multiply(vSums, vInvPeriod); + Vector256.StoreUnsafe(vResult, ref Unsafe.Add(ref outRef, i)); + + sum = vSums.GetElement(3); + + tickCount += VectorWidth; + if (tickCount >= ResyncInterval) { - sum += srcPtr[i]; - outPtr[i] = sum / (i + 1); - } - - if (len <= period) - return; - - var vInvPeriod = Vector256.Create(invPeriod); - var vZero = Vector256.Zero; - int simdEnd = period + ((len - period) / VectorWidth) * VectorWidth; - int tickCount = 0; - - for (int i = period; i < simdEnd; i += VectorWidth) - { - var vNew = Avx.LoadVector256(srcPtr + i); - var vOld = Avx.LoadVector256(srcPtr + i - period); - - var vDelta = Avx.Subtract(vNew, vOld); - - var vShift1 = Avx2.Permute4x64(vDelta.AsUInt64(), 0b_10_01_00_00).AsDouble(); - vShift1 = Avx.Blend(vZero, vShift1, 0b_1110); - var vP1 = Avx.Add(vDelta, vShift1); - - var vShift2 = Avx2.Permute4x64(vP1.AsUInt64(), 0b_01_00_00_00).AsDouble(); - vShift2 = Avx.Blend(vZero, vShift2, 0b_1100); - var vP2 = Avx.Add(vP1, vShift2); - - var vSumPrev = Vector256.Create(sum); - var vSums = Avx.Add(vSumPrev, vP2); - - var vResult = Avx.Multiply(vSums, vInvPeriod); - Avx.Store(outPtr + i, vResult); - - sum = vSums.GetElement(3); - - tickCount += VectorWidth; - if (tickCount >= ResyncInterval) + tickCount = 0; + int lastIdx = i + VectorWidth - 1; + double recalcSum = 0; + for (int k = 0; k < period; k++) { - tickCount = 0; - int lastIdx = i + VectorWidth - 1; - double recalcSum = 0; - for (int k = 0; k < period; k++) - { - recalcSum += srcPtr[lastIdx - k]; - } - sum = recalcSum; + recalcSum += Unsafe.Add(ref srcRef, lastIdx - k); } - } - - for (int i = simdEnd; i < len; i++) - { - sum = sum - srcPtr[i - period] + srcPtr[i]; - outPtr[i] = sum * invPeriod; + sum = recalcSum; } } + + for (int i = simdEnd; i < len; i++) + { + sum = sum - Unsafe.Add(ref srcRef, i - period) + Unsafe.Add(ref srcRef, i); + Unsafe.Add(ref outRef, i) = sum * invPeriod; + } } -#pragma warning restore S6640 /// /// Checks if span contains any non-finite values (NaN or Infinity). diff --git a/lib/averages/tema/Tema.Tests.cs b/lib/averages/tema/Tema.Tests.cs index dd524140..d577de36 100644 --- a/lib/averages/tema/Tema.Tests.cs +++ b/lib/averages/tema/Tema.Tests.cs @@ -1,5 +1,6 @@ namespace QuanTAlib.Tests; +#pragma warning disable S2245 // Random is acceptable for simulation/testing purposes public class TemaTests { [Fact] @@ -253,7 +254,7 @@ public class TemaTests { double[] source = new double[10000]; double[] output = new double[10000]; - var rng = new Random(42); + var rng = new Random(42); // nosemgrep for (int i = 0; i < source.Length; i++) source[i] = rng.NextDouble() * 100; diff --git a/lib/averages/tema/Tema.cs b/lib/averages/tema/Tema.cs index c1d9e376..550a7ec9 100644 --- a/lib/averages/tema/Tema.cs +++ b/lib/averages/tema/Tema.cs @@ -19,7 +19,9 @@ namespace QuanTAlib; /// Uses three EMA instances, each with O(1) update complexity. /// /// IsHot: -/// Becomes true when the third EMA converges (approx. 3x EMA convergence time). +/// Becomes true when the TEMA step response converges to within 5% error. +/// This happens when the third EMA's error factor drops below ~9% (approx 2.43/alpha steps), +/// which is faster than the standard EMA convergence (3/alpha steps). /// [SkipLocalsInit] public sealed class Tema @@ -48,7 +50,7 @@ public sealed class Tema public string Name { get; } public TValue Value { get; private set; } - public bool IsHot => _state3.IsHot; + public bool IsHot => _state3.E <= 0.09; public Tema(int period) { diff --git a/lib/averages/wma/Wma.Tests.cs b/lib/averages/wma/Wma.Tests.cs index 414334e3..4f334073 100644 --- a/lib/averages/wma/Wma.Tests.cs +++ b/lib/averages/wma/Wma.Tests.cs @@ -1,5 +1,6 @@ namespace QuanTAlib.Tests; +#pragma warning disable S2245 // Random is acceptable for simulation/testing purposes public class WmaTests { [Fact] @@ -468,7 +469,7 @@ public class WmaTests { double[] source = new double[10000]; double[] output = new double[10000]; - var rng = new Random(42); + var rng = new Random(42); // nosemgrep for (int i = 0; i < source.Length; i++) source[i] = rng.NextDouble() * 100; @@ -513,7 +514,7 @@ public class WmaTests { double[] source = new double[1000]; double[] output = new double[1000]; - var rng = new Random(42); + var rng = new Random(42); // nosemgrep for (int i = 0; i < source.Length; i++) source[i] = rng.NextDouble() * 100; diff --git a/lib/averages/wma/Wma.cs b/lib/averages/wma/Wma.cs index e242f1df..720edbae 100644 --- a/lib/averages/wma/Wma.cs +++ b/lib/averages/wma/Wma.cs @@ -289,189 +289,186 @@ public sealed class Wma } [MethodImpl(MethodImplOptions.AggressiveOptimization)] -#pragma warning disable S6640 // Unsafe code is required for high-performance SIMD operations - private static unsafe void CalculateSimdCore(ReadOnlySpan source, Span output, int period) + private static void CalculateSimdCore(ReadOnlySpan source, Span output, int period) { int len = source.Length; const int VectorWidth = 4; - fixed (double* srcPtr = source) - fixed (double* outPtr = output) + ref double srcRef = ref MemoryMarshal.GetReference(source); + ref double outRef = ref MemoryMarshal.GetReference(output); + + double divisor = period * (period + 1) * 0.5; + double invDivisor = 1.0 / divisor; + + int warmupEnd = Math.Min(period, len); + double sum = 0; + double wsum = 0; + for (int i = 0; i < warmupEnd; i++) { - double divisor = period * (period + 1) * 0.5; - double invDivisor = 1.0 / divisor; + double val = Unsafe.Add(ref srcRef, i); + sum += val; + wsum += (i + 1) * val; + double currentDivisor = (i + 1) * (i + 2) * 0.5; + Unsafe.Add(ref outRef, i) = wsum / currentDivisor; + } - int warmupEnd = Math.Min(period, len); - double sum = 0; - double wsum = 0; - for (int i = 0; i < warmupEnd; i++) + if (len <= period) + return; + + var vInvDivisor = Vector256.Create(invDivisor); + var vPeriod = Vector256.Create((double)period); + var vZero = Vector256.Zero; + int simdEnd = period + ((len - period) / VectorWidth) * VectorWidth; + + var vSumState = Vector256.Create(sum); + var vWsumState = Vector256.Create(wsum); + + int idx = period; + while (idx < simdEnd) + { + int nextSync = Math.Min(simdEnd, idx + ResyncInterval); + + int unrolledSync = nextSync - (2 * VectorWidth); + for (; idx <= unrolledSync; idx += 2 * VectorWidth) { - double val = srcPtr[i]; - sum += val; - wsum += (i + 1) * val; - double currentDivisor = (i + 1) * (i + 2) * 0.5; - outPtr[i] = wsum / currentDivisor; + var vNew1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, idx)); + var vOld1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, idx - period)); + var vNew2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, idx + VectorWidth)); + var vOld2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, idx + VectorWidth - period)); + + var vDeltaS1 = Avx.Subtract(vNew1, vOld1); + var vDeltaS2 = Avx.Subtract(vNew2, vOld2); + + var vShiftS1_1 = Avx2.Permute4x64(vDeltaS1.AsUInt64(), 0b_10_01_00_00).AsDouble(); + vShiftS1_1 = Avx.Blend(vZero, vShiftS1_1, 0b_1110); + var vPS_DeltaS1 = Avx.Add(vDeltaS1, vShiftS1_1); + var vShiftS2_1 = Avx2.Permute4x64(vPS_DeltaS1.AsUInt64(), 0b_01_00_00_00).AsDouble(); + vShiftS2_1 = Avx.Blend(vZero, vShiftS2_1, 0b_1100); + vPS_DeltaS1 = Avx.Add(vPS_DeltaS1, vShiftS2_1); + + var vShiftS1_2 = Avx2.Permute4x64(vDeltaS2.AsUInt64(), 0b_10_01_00_00).AsDouble(); + vShiftS1_2 = Avx.Blend(vZero, vShiftS1_2, 0b_1110); + var vPS_DeltaS2 = Avx.Add(vDeltaS2, vShiftS1_2); + var vShiftS2_2 = Avx2.Permute4x64(vPS_DeltaS2.AsUInt64(), 0b_01_00_00_00).AsDouble(); + vShiftS2_2 = Avx.Blend(vZero, vShiftS2_2, 0b_1100); + vPS_DeltaS2 = Avx.Add(vPS_DeltaS2, vShiftS2_2); + + var vSums1 = Avx.Add(vSumState, vPS_DeltaS1); + var vLastS1 = Avx2.Permute4x64(vSums1.AsUInt64(), 0b_11_11_11_11).AsDouble(); + var vSums2 = Avx.Add(vLastS1, vPS_DeltaS2); + + var vSumsShifted1 = Avx.Subtract(vSums1, vDeltaS1); + var vSumsShifted2 = Avx.Subtract(vSums2, vDeltaS2); + + Vector256 vU1, vU2; + if (Fma.IsSupported) + { + vU1 = Fma.MultiplySubtract(vPeriod, vNew1, vSumsShifted1); + vU2 = Fma.MultiplySubtract(vPeriod, vNew2, vSumsShifted2); + } + else + { + vU1 = Avx.Subtract(Avx.Multiply(vPeriod, vNew1), vSumsShifted1); + vU2 = Avx.Subtract(Avx.Multiply(vPeriod, vNew2), vSumsShifted2); + } + + var vShiftW1_1 = Avx2.Permute4x64(vU1.AsUInt64(), 0b_10_01_00_00).AsDouble(); + vShiftW1_1 = Avx.Blend(vZero, vShiftW1_1, 0b_1110); + var vPW1_1 = Avx.Add(vU1, vShiftW1_1); + var vShiftW2_1 = Avx2.Permute4x64(vPW1_1.AsUInt64(), 0b_01_00_00_00).AsDouble(); + vShiftW2_1 = Avx.Blend(vZero, vShiftW2_1, 0b_1100); + var vPW2_1 = Avx.Add(vPW1_1, vShiftW2_1); + + var vShiftW1_2 = Avx2.Permute4x64(vU2.AsUInt64(), 0b_10_01_00_00).AsDouble(); + vShiftW1_2 = Avx.Blend(vZero, vShiftW1_2, 0b_1110); + var vPW1_2 = Avx.Add(vU2, vShiftW1_2); + var vShiftW2_2 = Avx2.Permute4x64(vPW1_2.AsUInt64(), 0b_01_00_00_00).AsDouble(); + vShiftW2_2 = Avx.Blend(vZero, vShiftW2_2, 0b_1100); + var vPW2_2 = Avx.Add(vPW1_2, vShiftW2_2); + + var vWsums1 = Avx.Add(vWsumState, vPW2_1); + var vLastW1 = Avx2.Permute4x64(vWsums1.AsUInt64(), 0b_11_11_11_11).AsDouble(); + var vWsums2 = Avx.Add(vLastW1, vPW2_2); + + Vector256.StoreUnsafe(Avx.Multiply(vWsums1, vInvDivisor), ref Unsafe.Add(ref outRef, idx)); + Vector256.StoreUnsafe(Avx.Multiply(vWsums2, vInvDivisor), ref Unsafe.Add(ref outRef, idx + VectorWidth)); + + vSumState = Avx2.Permute4x64(vSums2.AsUInt64(), 0b_11_11_11_11).AsDouble(); + vWsumState = Avx2.Permute4x64(vWsums2.AsUInt64(), 0b_11_11_11_11).AsDouble(); } - if (len <= period) - return; - - var vInvDivisor = Vector256.Create(invDivisor); - var vPeriod = Vector256.Create((double)period); - var vZero = Vector256.Zero; - int simdEnd = period + ((len - period) / VectorWidth) * VectorWidth; - - var vSumState = Vector256.Create(sum); - var vWsumState = Vector256.Create(wsum); - - int idx = period; - while (idx < simdEnd) + for (; idx < nextSync; idx += VectorWidth) { - int nextSync = Math.Min(simdEnd, idx + ResyncInterval); - - int unrolledSync = nextSync - (2 * VectorWidth); - for (; idx <= unrolledSync; idx += 2 * VectorWidth) - { - var vNew1 = Avx.LoadVector256(srcPtr + idx); - var vOld1 = Avx.LoadVector256(srcPtr + idx - period); - var vNew2 = Avx.LoadVector256(srcPtr + idx + VectorWidth); - var vOld2 = Avx.LoadVector256(srcPtr + idx + VectorWidth - period); + var vNew = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, idx)); + var vOld = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, idx - period)); - var vDeltaS1 = Avx.Subtract(vNew1, vOld1); - var vDeltaS2 = Avx.Subtract(vNew2, vOld2); + var vDeltaS = Avx.Subtract(vNew, vOld); - var vShiftS1_1 = Avx2.Permute4x64(vDeltaS1.AsUInt64(), 0b_10_01_00_00).AsDouble(); - vShiftS1_1 = Avx.Blend(vZero, vShiftS1_1, 0b_1110); - var vPS_DeltaS1 = Avx.Add(vDeltaS1, vShiftS1_1); - var vShiftS2_1 = Avx2.Permute4x64(vPS_DeltaS1.AsUInt64(), 0b_01_00_00_00).AsDouble(); - vShiftS2_1 = Avx.Blend(vZero, vShiftS2_1, 0b_1100); - vPS_DeltaS1 = Avx.Add(vPS_DeltaS1, vShiftS2_1); + var vShiftS1 = Avx2.Permute4x64(vDeltaS.AsUInt64(), 0b_10_01_00_00).AsDouble(); + vShiftS1 = Avx.Blend(vZero, vShiftS1, 0b_1110); + var vPS1 = Avx.Add(vDeltaS, vShiftS1); - var vShiftS1_2 = Avx2.Permute4x64(vDeltaS2.AsUInt64(), 0b_10_01_00_00).AsDouble(); - vShiftS1_2 = Avx.Blend(vZero, vShiftS1_2, 0b_1110); - var vPS_DeltaS2 = Avx.Add(vDeltaS2, vShiftS1_2); - var vShiftS2_2 = Avx2.Permute4x64(vPS_DeltaS2.AsUInt64(), 0b_01_00_00_00).AsDouble(); - vShiftS2_2 = Avx.Blend(vZero, vShiftS2_2, 0b_1100); - vPS_DeltaS2 = Avx.Add(vPS_DeltaS2, vShiftS2_2); + var vShiftS2 = Avx2.Permute4x64(vPS1.AsUInt64(), 0b_01_00_00_00).AsDouble(); + vShiftS2 = Avx.Blend(vZero, vShiftS2, 0b_1100); + var vPS2 = Avx.Add(vPS1, vShiftS2); - var vSums1 = Avx.Add(vSumState, vPS_DeltaS1); - var vLastS1 = Avx2.Permute4x64(vSums1.AsUInt64(), 0b_11_11_11_11).AsDouble(); - var vSums2 = Avx.Add(vLastS1, vPS_DeltaS2); + var vSums = Avx.Add(vSumState, vPS2); - var vSumsShifted1 = Avx.Subtract(vSums1, vDeltaS1); - var vSumsShifted2 = Avx.Subtract(vSums2, vDeltaS2); + var vSumsShifted = Avx2.Permute4x64(vSums.AsUInt64(), 0b_10_01_00_00).AsDouble(); + vSumsShifted = Avx.Blend(vSumState, vSumsShifted, 0b_1110); - Vector256 vU1, vU2; - if (Fma.IsSupported) - { - vU1 = Fma.MultiplySubtract(vPeriod, vNew1, vSumsShifted1); - vU2 = Fma.MultiplySubtract(vPeriod, vNew2, vSumsShifted2); - } - else - { - vU1 = Avx.Subtract(Avx.Multiply(vPeriod, vNew1), vSumsShifted1); - vU2 = Avx.Subtract(Avx.Multiply(vPeriod, vNew2), vSumsShifted2); - } + var vTerm1 = Avx.Multiply(vPeriod, vNew); + var vU = Avx.Subtract(vTerm1, vSumsShifted); - var vShiftW1_1 = Avx2.Permute4x64(vU1.AsUInt64(), 0b_10_01_00_00).AsDouble(); - vShiftW1_1 = Avx.Blend(vZero, vShiftW1_1, 0b_1110); - var vPW1_1 = Avx.Add(vU1, vShiftW1_1); - var vShiftW2_1 = Avx2.Permute4x64(vPW1_1.AsUInt64(), 0b_01_00_00_00).AsDouble(); - vShiftW2_1 = Avx.Blend(vZero, vShiftW2_1, 0b_1100); - var vPW2_1 = Avx.Add(vPW1_1, vShiftW2_1); + var vShiftW1 = Avx2.Permute4x64(vU.AsUInt64(), 0b_10_01_00_00).AsDouble(); + vShiftW1 = Avx.Blend(vZero, vShiftW1, 0b_1110); + var vPW1 = Avx.Add(vU, vShiftW1); - var vShiftW1_2 = Avx2.Permute4x64(vU2.AsUInt64(), 0b_10_01_00_00).AsDouble(); - vShiftW1_2 = Avx.Blend(vZero, vShiftW1_2, 0b_1110); - var vPW1_2 = Avx.Add(vU2, vShiftW1_2); - var vShiftW2_2 = Avx2.Permute4x64(vPW1_2.AsUInt64(), 0b_01_00_00_00).AsDouble(); - vShiftW2_2 = Avx.Blend(vZero, vShiftW2_2, 0b_1100); - var vPW2_2 = Avx.Add(vPW1_2, vShiftW2_2); + var vShiftW2 = Avx2.Permute4x64(vPW1.AsUInt64(), 0b_01_00_00_00).AsDouble(); + vShiftW2 = Avx.Blend(vZero, vShiftW2, 0b_1100); + var vPW2 = Avx.Add(vPW1, vShiftW2); - var vWsums1 = Avx.Add(vWsumState, vPW2_1); - var vLastW1 = Avx2.Permute4x64(vWsums1.AsUInt64(), 0b_11_11_11_11).AsDouble(); - var vWsums2 = Avx.Add(vLastW1, vPW2_2); + var vWsums = Avx.Add(vWsumState, vPW2); - Avx.Store(outPtr + idx, Avx.Multiply(vWsums1, vInvDivisor)); - Avx.Store(outPtr + idx + VectorWidth, Avx.Multiply(vWsums2, vInvDivisor)); + var vResult = Avx.Multiply(vWsums, vInvDivisor); + Vector256.StoreUnsafe(vResult, ref Unsafe.Add(ref outRef, idx)); - vSumState = Avx2.Permute4x64(vSums2.AsUInt64(), 0b_11_11_11_11).AsDouble(); - vWsumState = Avx2.Permute4x64(vWsums2.AsUInt64(), 0b_11_11_11_11).AsDouble(); - } - - for (; idx < nextSync; idx += VectorWidth) - { - var vNew = Avx.LoadVector256(srcPtr + idx); - var vOld = Avx.LoadVector256(srcPtr + idx - period); - - var vDeltaS = Avx.Subtract(vNew, vOld); - - var vShiftS1 = Avx2.Permute4x64(vDeltaS.AsUInt64(), 0b_10_01_00_00).AsDouble(); - vShiftS1 = Avx.Blend(vZero, vShiftS1, 0b_1110); - var vPS1 = Avx.Add(vDeltaS, vShiftS1); - - var vShiftS2 = Avx2.Permute4x64(vPS1.AsUInt64(), 0b_01_00_00_00).AsDouble(); - vShiftS2 = Avx.Blend(vZero, vShiftS2, 0b_1100); - var vPS2 = Avx.Add(vPS1, vShiftS2); - - var vSums = Avx.Add(vSumState, vPS2); - - var vSumsShifted = Avx2.Permute4x64(vSums.AsUInt64(), 0b_10_01_00_00).AsDouble(); - vSumsShifted = Avx.Blend(vSumState, vSumsShifted, 0b_1110); - - var vTerm1 = Avx.Multiply(vPeriod, vNew); - var vU = Avx.Subtract(vTerm1, vSumsShifted); - - var vShiftW1 = Avx2.Permute4x64(vU.AsUInt64(), 0b_10_01_00_00).AsDouble(); - vShiftW1 = Avx.Blend(vZero, vShiftW1, 0b_1110); - var vPW1 = Avx.Add(vU, vShiftW1); - - var vShiftW2 = Avx2.Permute4x64(vPW1.AsUInt64(), 0b_01_00_00_00).AsDouble(); - vShiftW2 = Avx.Blend(vZero, vShiftW2, 0b_1100); - var vPW2 = Avx.Add(vPW1, vShiftW2); - - var vWsums = Avx.Add(vWsumState, vPW2); - - var vResult = Avx.Multiply(vWsums, vInvDivisor); - Avx.Store(outPtr + idx, vResult); - - vSumState = Avx2.Permute4x64(vSums.AsUInt64(), 0b_11_11_11_11).AsDouble(); - vWsumState = Avx2.Permute4x64(vWsums.AsUInt64(), 0b_11_11_11_11).AsDouble(); - } - - if (idx < len) - { - int lastIdx = idx - 1; - double recalcSum = 0; - double recalcWsum = 0; - for (int k = 0; k < period; k++) - { - double val = srcPtr[lastIdx - k]; - recalcSum += val; - recalcWsum += (period - k) * val; - } - sum = recalcSum; - wsum = recalcWsum; - - vSumState = Vector256.Create(sum); - vWsumState = Vector256.Create(wsum); - } + vSumState = Avx2.Permute4x64(vSums.AsUInt64(), 0b_11_11_11_11).AsDouble(); + vWsumState = Avx2.Permute4x64(vWsums.AsUInt64(), 0b_11_11_11_11).AsDouble(); } - - sum = vSumState.GetElement(0); - wsum = vWsumState.GetElement(0); - for (; idx < len; idx++) + if (idx < len) { - double val = srcPtr[idx]; - double oldSum = sum; - double oldest = srcPtr[idx - period]; - sum = sum - oldest + val; - wsum = wsum - oldSum + (period * val); - outPtr[idx] = wsum * invDivisor; + int lastIdx = idx - 1; + double recalcSum = 0; + double recalcWsum = 0; + for (int k = 0; k < period; k++) + { + double val = Unsafe.Add(ref srcRef, lastIdx - k); + recalcSum += val; + recalcWsum += (period - k) * val; + } + sum = recalcSum; + wsum = recalcWsum; + + vSumState = Vector256.Create(sum); + vWsumState = Vector256.Create(wsum); } } + + sum = vSumState.GetElement(0); + wsum = vWsumState.GetElement(0); + + for (; idx < len; idx++) + { + double val = Unsafe.Add(ref srcRef, idx); + double oldSum = sum; + double oldest = Unsafe.Add(ref srcRef, idx - period); + sum = sum - oldest + val; + wsum = wsum - oldSum + (period * val); + Unsafe.Add(ref outRef, idx) = wsum * invDivisor; + } } -#pragma warning restore S6640 [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool HasNonFiniteValues(ReadOnlySpan span) diff --git a/lib/feeds/gbm/Gbm.Tests.cs b/lib/feeds/gbm/Gbm.Tests.cs index 8837e93c..d9197729 100644 --- a/lib/feeds/gbm/Gbm.Tests.cs +++ b/lib/feeds/gbm/Gbm.Tests.cs @@ -212,7 +212,7 @@ public class GBMTests var gbm = new GBM(startPrice: 100.0); // Start with streaming - var bar1 = gbm.Next(); + _ = gbm.Next(); var bar2 = gbm.Next(); // Batch generation with explicit time @@ -253,11 +253,11 @@ public class GBMTests { var gbm = new GBM(startPrice: 100.0); - var bar1 = gbm.Next(); - var bar2 = gbm.Next(); + var previousBar = gbm.Next(); + var currentBar = gbm.Next(); - // bar2.Open should equal bar1.Close (continuity) - Assert.Equal(bar1.Close, bar2.Open); + // currentBar.Open should equal previousBar.Close (continuity) + Assert.Equal(previousBar.Close, currentBar.Open); } [Fact] @@ -268,7 +268,7 @@ public class GBMTests // Generate multiple bars for (int i = 0; i < 100; i++) { - gbm.Next(); + _ = gbm.Next(); } // GBM should not expose any history storage diff --git a/lib/feeds/gbm/gbm.cs b/lib/feeds/gbm/gbm.cs index 8f084e54..6b23b067 100644 --- a/lib/feeds/gbm/gbm.cs +++ b/lib/feeds/gbm/gbm.cs @@ -9,6 +9,7 @@ namespace QuanTAlib; /// [SkipLocalsInit] #pragma warning disable S101 // Rename class 'GBM' to match pascal case naming rules +#pragma warning disable S2245 // Random is acceptable for simulation/testing purposes public class GBM : IFeed #pragma warning restore S101 { @@ -82,10 +83,8 @@ public class GBM : IFeed return _cachedZ; } -#pragma warning disable S2245 // Random is acceptable for simulation/testing purposes - double u1 = 1.0 - _rnd.NextDouble(); - double u2 = 1.0 - _rnd.NextDouble(); -#pragma warning restore S2245 + double u1 = 1.0 - _rnd.NextDouble(); // nosemgrep + double u2 = 1.0 - _rnd.NextDouble(); // nosemgrep double mag = Math.Sqrt(-2.0 * Math.Log(u1)); double angle = 2.0 * Math.PI * u2; @@ -190,11 +189,9 @@ public class GBM : IFeed double open = currentPrice; double close = price; -#pragma warning disable S2245 // Random is acceptable for simulation/testing purposes double rnd1 = _rnd.NextDouble(); double rnd2 = _rnd.NextDouble(); double rnd3 = _rnd.NextDouble(); -#pragma warning restore S2245 t[i] = currentTime; o[i] = open;