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
+1 -1
View File
@@ -272,4 +272,4 @@ public sealed class Beta : AbstractBase
_sumRm2 = FusedMultiplyAdd(rm, rm, _sumRm2);
}
}
}
}
+1 -1
View File
@@ -166,4 +166,4 @@ public class CmaIndicatorTests
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
Assert.Equal(20.0, indicator.LinesSeries[0].GetValue(0), 1e-10); // CMA = (10+20+30)/3 = 20
}
}
}
+6 -1
View File
@@ -401,7 +401,9 @@ public class CmaTests
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
Cma.Batch(source.AsSpan(), output.AsSpan());
@@ -520,7 +522,10 @@ public class CmaTests
public void Calculate_ReturnsCorrectResultsAndHotIndicator()
{
var series = new TSeries();
for (int i = 1; i <= 10; i++) series.Add(DateTime.UtcNow, i * 10);
for (int i = 1; i <= 10; i++)
{
series.Add(DateTime.UtcNow, i * 10);
}
// 10, 20, 30, 40, 50, 60, 70, 80, 90, 100
var (results, indicator) = Cma.Calculate(series);
+19 -4
View File
@@ -96,7 +96,10 @@ public sealed class Cma : AbstractBase
/// <param name="step">Time interval between values (not used for CMA)</param>
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
if (source.Length == 0) return;
if (source.Length == 0)
{
return;
}
// Reset state
_state = default;
@@ -162,7 +165,10 @@ public sealed class Cma : 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);
@@ -207,10 +213,15 @@ public sealed class Cma : AbstractBase
public static void Batch(ReadOnlySpan<double> source, Span<double> output)
{
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output must have the same length", nameof(output));
}
int len = source.Length;
if (len == 0) return;
if (len == 0)
{
return;
}
double mean = 0;
double lastValid = double.NaN;
@@ -230,9 +241,13 @@ public sealed class Cma : AbstractBase
{
double val = source[i];
if (double.IsFinite(val))
{
lastValid = val;
}
else
{
val = lastValid;
}
// M_n = M_(n-1) + alpha * delta using FMA for single-rounding precision
double alpha = 1.0 / (i + 1);
@@ -264,4 +279,4 @@ public sealed class Cma : AbstractBase
_p_state = default;
Last = default;
}
}
}
@@ -82,8 +82,15 @@ public class CovarianceSimdTests
{
double x = dataX[i];
double y = dataY[i];
if (!double.IsFinite(x)) x = 0;
if (!double.IsFinite(y)) y = 0;
if (!double.IsFinite(x))
{
x = 0;
}
if (!double.IsFinite(y))
{
y = 0;
}
var res = scalarCov.Update(x, y);
Assert.Equal(res.Value, result.Values[i], precision: 9);
+56 -12
View File
@@ -182,7 +182,9 @@ public sealed class Covariance : AbstractBase
public static TSeries Calculate(TSeries sourceX, TSeries sourceY, int period, bool isPopulation = false)
{
if (sourceX.Count != sourceY.Count)
{
throw new ArgumentException("Source series must have the same length", nameof(sourceY));
}
int len = sourceX.Count;
var t = new List<long>(len);
@@ -203,12 +205,20 @@ public sealed class Covariance : AbstractBase
public static void Batch(ReadOnlySpan<double> sourceX, ReadOnlySpan<double> sourceY, Span<double> output, int period, bool isPopulation = false)
{
if (sourceX.Length != sourceY.Length || sourceX.Length != output.Length)
{
throw new ArgumentException("All spans must have the same length", nameof(output));
}
if (period < 2)
{
throw new ArgumentException("Period must be greater than or equal to 2", nameof(period));
}
int len = sourceX.Length;
if (len == 0) return;
if (len == 0)
{
return;
}
// SIMD overhead amortizes well for datasets >= 256 elements
const int SimdThreshold = 256;
@@ -242,8 +252,15 @@ public sealed class Covariance : AbstractBase
{
double x = sourceX[i];
double y = sourceY[i];
if (!double.IsFinite(x)) x = 0;
if (!double.IsFinite(y)) y = 0;
if (!double.IsFinite(x))
{
x = 0;
}
if (!double.IsFinite(y))
{
y = 0;
}
sumX += x;
sumY += y;
@@ -270,8 +287,15 @@ public sealed class Covariance : AbstractBase
{
double x = sourceX[i];
double y = sourceY[i];
if (!double.IsFinite(x)) x = 0;
if (!double.IsFinite(y)) y = 0;
if (!double.IsFinite(x))
{
x = 0;
}
if (!double.IsFinite(y))
{
y = 0;
}
double oldX = bufferX[bufferIndex];
double oldY = bufferY[bufferIndex];
@@ -283,7 +307,10 @@ public sealed class Covariance : AbstractBase
bufferX[bufferIndex] = x;
bufferY[bufferIndex] = y;
bufferIndex++;
if (bufferIndex >= period) bufferIndex = 0;
if (bufferIndex >= period)
{
bufferIndex = 0;
}
double n = period;
double numerator = sumXY - (sumX * sumY) / n;
@@ -357,7 +384,10 @@ public sealed class Covariance : AbstractBase
(double sumX, double sumY, double sumXY) = WarmupCovariance(period, len, isPopulation, ref srcXRef, ref srcYRef, ref outRef);
if (len <= period) return;
if (len <= period)
{
return;
}
var vInvN = Vector256.Create(invN);
var vInvDenom = Vector256.Create(invDenom);
@@ -452,13 +482,27 @@ public sealed class Covariance : AbstractBase
{
double x = Unsafe.Add(ref srcXRef, i);
double y = Unsafe.Add(ref srcYRef, i);
if (!double.IsFinite(x)) x = 0;
if (!double.IsFinite(y)) y = 0;
if (!double.IsFinite(x))
{
x = 0;
}
if (!double.IsFinite(y))
{
y = 0;
}
double oldX = Unsafe.Add(ref srcXRef, i - period);
double oldY = Unsafe.Add(ref srcYRef, i - period);
if (!double.IsFinite(oldX)) oldX = 0;
if (!double.IsFinite(oldY)) oldY = 0;
if (!double.IsFinite(oldX))
{
oldX = 0;
}
if (!double.IsFinite(oldY))
{
oldY = 0;
}
sumX = sumX - oldX + x;
sumY = sumY - oldY + y;
@@ -468,4 +512,4 @@ public sealed class Covariance : AbstractBase
Unsafe.Add(ref outRef, i) = numerator * invDenom;
}
}
}
}
+83 -62
View File
@@ -76,7 +76,9 @@ public sealed class LinReg : AbstractBase
public LinReg(int period, int offset = 0)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
_period = period;
_offset = offset;
@@ -262,7 +264,10 @@ public sealed class LinReg : AbstractBase
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return new TSeries([], []);
if (source.Count == 0)
{
return new TSeries([], []);
}
int len = source.Count;
var t = new List<long>(len);
@@ -332,12 +337,20 @@ public sealed class LinReg : AbstractBase
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period, int offset = 0, double initialLastValid = 0)
{
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output must have the same length", nameof(output));
}
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
int len = source.Length;
if (len == 0) return;
if (len == 0)
{
return;
}
// Stack allocate for typical periods (most < 100)
// ArrayPool for large periods to avoid stack overflow
@@ -353,87 +366,95 @@ public sealed class LinReg : AbstractBase
try
{
double sum_y = 0;
double sum_xy = 0;
double lastValid = initialLastValid;
int bufferIndex = 0;
int count = 0;
double sum_y = 0;
double sum_xy = 0;
double lastValid = initialLastValid;
int bufferIndex = 0;
int count = 0;
double full_sum_x = 0.5 * period * (period - 1);
double full_sum_x2 = (period - 1.0) * period * (2.0 * period - 1.0) / 6.0;
double full_denom = period * full_sum_x2 - full_sum_x * full_sum_x;
double full_sum_x = 0.5 * period * (period - 1);
double full_sum_x2 = (period - 1.0) * period * (2.0 * period - 1.0) / 6.0;
double full_denom = period * full_sum_x2 - full_sum_x * full_sum_x;
for (int i = 0; i < len; i++)
{
double val = source[i];
if (double.IsFinite(val))
lastValid = val;
else
val = lastValid;
if (count < period)
for (int i = 0; i < len; i++)
{
buffer[count] = val;
sum_y += val;
count++;
sum_xy = 0;
for (int j = 0; j < count; j++)
double val = source[i];
if (double.IsFinite(val))
{
sum_xy = Math.FusedMultiplyAdd(count - 1 - j, buffer[j], sum_xy);
}
if (count <= 1)
{
output[i] = val;
lastValid = val;
}
else
{
double n = count;
double sx = 0.5 * n * (n - 1);
double sx2 = (n - 1.0) * n * (2.0 * n - 1.0) / 6.0;
double denom = n * sx2 - sx * sx;
val = lastValid;
}
if (Math.Abs(denom) < MinDenominator)
if (count < period)
{
buffer[count] = val;
sum_y += val;
count++;
sum_xy = 0;
for (int j = 0; j < count; j++)
{
sum_xy = Math.FusedMultiplyAdd(count - 1 - j, buffer[j], sum_xy);
}
if (count <= 1)
{
output[i] = val;
}
else
{
double m = Math.FusedMultiplyAdd(n, sum_xy, -sx * sum_y) / denom;
double b = Math.FusedMultiplyAdd(-m, sx, sum_y) / n;
output[i] = Math.FusedMultiplyAdd(-m, offset, b);
double n = count;
double sx = 0.5 * n * (n - 1);
double sx2 = (n - 1.0) * n * (2.0 * n - 1.0) / 6.0;
double denom = n * sx2 - sx * sx;
if (Math.Abs(denom) < MinDenominator)
{
output[i] = val;
}
else
{
double m = Math.FusedMultiplyAdd(n, sum_xy, -sx * sum_y) / denom;
double b = Math.FusedMultiplyAdd(-m, sx, sum_y) / n;
output[i] = Math.FusedMultiplyAdd(-m, offset, b);
}
}
if (count == period)
{
bufferIndex = 0;
}
}
else
{
double oldest = buffer[bufferIndex];
double prev_sum_y = sum_y;
if (count == period)
{
bufferIndex = 0;
sum_xy = sum_xy + prev_sum_y - period * oldest;
sum_y = sum_y - oldest + val;
buffer[bufferIndex] = val;
bufferIndex++;
if (bufferIndex >= period)
{
bufferIndex = 0;
}
double m = Math.FusedMultiplyAdd(period, sum_xy, -full_sum_x * sum_y) / full_denom;
double b = Math.FusedMultiplyAdd(-m, full_sum_x, sum_y) / period;
output[i] = Math.FusedMultiplyAdd(-m, offset, b);
}
}
}
else
{
double oldest = buffer[bufferIndex];
double prev_sum_y = sum_y;
sum_xy = sum_xy + prev_sum_y - period * oldest;
sum_y = sum_y - oldest + val;
buffer[bufferIndex] = val;
bufferIndex++;
if (bufferIndex >= period)
bufferIndex = 0;
double m = Math.FusedMultiplyAdd(period, sum_xy, -full_sum_x * sum_y) / full_denom;
double b = Math.FusedMultiplyAdd(-m, full_sum_x, sum_y) / period;
output[i] = Math.FusedMultiplyAdd(-m, offset, b);
}
}
}
finally
{
if (rentedBuffer != null)
{
ArrayPool<double>.Shared.Return(rentedBuffer);
}
}
}
@@ -448,4 +469,4 @@ public sealed class LinReg : AbstractBase
Intercept = 0;
RSquared = 0;
}
}
}
+9 -2
View File
@@ -264,14 +264,21 @@ public class MedianTests
// Arrange
int period = 5;
double[] data = new double[20];
for (int i = 0; i < data.Length; i++) data[i] = i;
for (int i = 0; i < data.Length; i++)
{
data[i] = i;
}
// Act
double[] output = new double[data.Length];
Median.Batch(data, output, period);
var series = new TSeries();
for (int i = 0; i < data.Length; i++) series.Add(new TValue(DateTime.MinValue, data[i]));
for (int i = 0; i < data.Length; i++)
{
series.Add(new TValue(DateTime.MinValue, data[i]));
}
var batchSeries = Median.Batch(series, period);
// Assert
@@ -86,7 +86,10 @@ public sealed class MedianValidationTests : IDisposable
private static double CalculateMedian(List<double> sortedWindow)
{
int count = sortedWindow.Count;
if (count == 0) return 0; // Or NaN
if (count == 0)
{
return 0; // Or NaN
}
int mid = count / 2;
if (count % 2 != 0)
+39 -7
View File
@@ -38,7 +38,9 @@ public sealed class Median : AbstractBase, IDisposable
public Median(int period)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
_period = period;
_buffer = new RingBuffer(period);
@@ -76,7 +78,10 @@ public sealed class Median : AbstractBase, IDisposable
/// </summary>
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
if (source.Length == 0) return;
if (source.Length == 0)
{
return;
}
_buffer.Clear();
int warmupLength = Math.Min(source.Length, WarmupPeriod);
@@ -151,7 +156,10 @@ public sealed class Median : AbstractBase, IDisposable
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);
@@ -178,7 +186,10 @@ public sealed class Median : AbstractBase, IDisposable
// validCount = elements in sortedBuffer BEFORE insertion
int validCount = _buffer.Count - 1;
int index = Array.BinarySearch(_sortedBuffer, 0, validCount, value);
if (index < 0) index = ~index;
if (index < 0)
{
index = ~index;
}
if (index < validCount)
{
@@ -224,12 +235,20 @@ public sealed class Median : AbstractBase, IDisposable
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period)
{
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output must have the same length", nameof(output));
}
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
int len = source.Length;
if (len == 0) return;
if (len == 0)
{
return;
}
// Always use ArrayPool to avoid CS8353 stackalloc escape issues
double[] rentedSorted = ArrayPool<double>.Shared.Rent(period);
@@ -266,7 +285,10 @@ public sealed class Median : AbstractBase, IDisposable
windowIdx = (windowIdx + 1) % period;
int newIndex = BinarySearchSpan(sortedBuffer, count, val);
if (newIndex < 0) newIndex = ~newIndex;
if (newIndex < 0)
{
newIndex = ~newIndex;
}
if (newIndex < count)
{
@@ -300,11 +322,18 @@ public sealed class Median : AbstractBase, IDisposable
int mid = lo + ((hi - lo) >> 1);
int cmp = span[mid].CompareTo(value);
if (cmp == 0)
{
return mid;
}
if (cmp < 0)
{
lo = mid + 1;
}
else
{
hi = mid - 1;
}
}
return ~lo;
}
@@ -325,7 +354,10 @@ public sealed class Median : AbstractBase, IDisposable
/// </summary>
public new void Dispose()
{
if (_disposed) return;
if (_disposed)
{
return;
}
if (_source != null)
{
@@ -335,4 +367,4 @@ public sealed class Median : AbstractBase, IDisposable
_disposed = true;
}
}
}
+8 -2
View File
@@ -284,7 +284,10 @@ public class SkewTests
public void Reset_ClearsState()
{
var skew = new Skew(5);
for (int i = 0; i < 5; i++) skew.Update(new TValue(DateTime.UtcNow, i));
for (int i = 0; i < 5; i++)
{
skew.Update(new TValue(DateTime.UtcNow, i));
}
skew.Reset();
Assert.False(skew.IsHot);
@@ -380,7 +383,10 @@ public class SkewTests
// Create large dataset to trigger SIMD path (>= 256)
int count = 1000;
var data = new double[count];
for (int i = 0; i < count; i++) data[i] = (double)i;
for (int i = 0; i < count; i++)
{
data[i] = (double)i;
}
var series = new TSeries(new System.Collections.Generic.List<long>(new long[count]), new System.Collections.Generic.List<double>(data));
+49 -11
View File
@@ -104,7 +104,11 @@ public sealed class Skew : AbstractBase
// Calculate 2nd moment (Variance)
// m2 = Sum((x-mean)^2) / n = (SumSq - Sum^2/n) / n
double m2Numerator = _sumSq - (_sum * _sum) / n;
if (m2Numerator < Epsilon) m2Numerator = 0;
if (m2Numerator < Epsilon)
{
m2Numerator = 0;
}
double m2 = m2Numerator / n;
// Calculate 3rd moment
@@ -143,7 +147,10 @@ public sealed class Skew : 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);
@@ -225,12 +232,20 @@ public sealed class Skew : AbstractBase
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period, bool isPopulation = false)
{
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output must have the same length", nameof(output));
}
if (period < 3)
{
throw new ArgumentException("Period must be greater than or equal to 3", nameof(period));
}
int len = source.Length;
if (len == 0) return;
if (len == 0)
{
return;
}
// Try SIMD path for large, clean datasets
// SIMD overhead amortizes well for datasets >= 256 elements
@@ -260,7 +275,10 @@ public sealed class Skew : AbstractBase
for (; i < warmupEnd; i++)
{
double val = source[i];
if (!double.IsFinite(val)) val = 0;
if (!double.IsFinite(val))
{
val = 0;
}
sum += val;
sumSq += val * val;
@@ -275,10 +293,16 @@ public sealed class Skew : AbstractBase
for (; i < len; i++)
{
double val = source[i];
if (!double.IsFinite(val)) val = 0;
if (!double.IsFinite(val))
{
val = 0;
}
double oldVal = source[i - period];
if (!double.IsFinite(oldVal)) oldVal = 0;
if (!double.IsFinite(oldVal))
{
oldVal = 0;
}
sum = sum - oldVal + val;
sumSq = sumSq - (oldVal * oldVal) + (val * val);
@@ -297,7 +321,11 @@ public sealed class Skew : AbstractBase
for (int k = 0; k < period; k++)
{
double v = source[startIdx + k];
if (!double.IsFinite(v)) v = 0;
if (!double.IsFinite(v))
{
v = 0;
}
recalcSum += v;
recalcSumSq += v * v;
recalcSumCu += v * v * v;
@@ -315,13 +343,20 @@ public sealed class Skew : AbstractBase
double mean = sum / n;
double m2Numerator = sumSq - (sum * sum) / n;
if (m2Numerator < Epsilon) return 0;
if (m2Numerator < Epsilon)
{
return 0;
}
double m2 = m2Numerator / n;
double m3Numerator = sumCu - 3 * mean * sumSq + 2 * n * mean * mean * mean;
double m3 = m3Numerator / n;
if (m2 <= Epsilon) return 0;
if (m2 <= Epsilon)
{
return 0;
}
double g1 = m3 / (m2 * Math.Sqrt(m2));
@@ -367,7 +402,10 @@ public sealed class Skew : AbstractBase
WarmupSkew(period, isPopulation, ref srcRef, ref outRef, out double sum, out double sumSq, out double sumCu);
if (len <= period) return;
if (len <= period)
{
return;
}
var vInvN = Vector256.Create(invN);
var vN = Vector256.Create(n);
@@ -509,4 +547,4 @@ public sealed class Skew : AbstractBase
Unsafe.Add(ref outRef, i) = CalculateSkewFromSums(sum, sumSq, sumCu, n, isPopulation);
}
}
}
}
@@ -19,9 +19,16 @@ public sealed class StdDevValidationTests : IDisposable
private void Dispose(bool disposing)
{
if (_disposed) return;
if (_disposed)
{
return;
}
_disposed = true;
if (disposing) _testData?.Dispose();
if (disposing)
{
_testData?.Dispose();
}
}
#region Skender Validation
+5 -2
View File
@@ -57,7 +57,10 @@ public sealed class StdDev : 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);
@@ -189,4 +192,4 @@ public sealed class StdDev : AbstractBase
data[i] = (val > 0) ? Math.Sqrt(val) : 0.0;
}
}
}
}
+4
View File
@@ -392,7 +392,9 @@ public class SumTests
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;
}
Sum.Batch(source.AsSpan(), output.AsSpan(), 100);
@@ -520,7 +522,9 @@ public class SumTests
{
var series = new TSeries();
for (int i = 1; i <= 10; i++)
{
series.Add(DateTime.UtcNow, i * 10);
}
// 10, 20, 30, 40, 50, 60, 70, 80, 90, 100
var (results, indicator) = Sum.Calculate(series, 5);
+9 -2
View File
@@ -26,9 +26,16 @@ public sealed class SumValidationTests : IDisposable
private void Dispose(bool disposing)
{
if (_disposed) return;
if (_disposed)
{
return;
}
_disposed = true;
if (disposing) _testData?.Dispose();
if (disposing)
{
_testData?.Dispose();
}
}
[Fact]
+88 -59
View File
@@ -67,7 +67,9 @@ public sealed class Sum : AbstractBase
public Sum(int period)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
_period = period;
_buffer = new RingBuffer(period);
@@ -163,7 +165,10 @@ public sealed class Sum : AbstractBase
/// </summary>
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
if (source.Length == 0) return;
if (source.Length == 0)
{
return;
}
// Reset state
_buffer.Clear();
@@ -284,7 +289,10 @@ public sealed class Sum : 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);
@@ -325,12 +333,20 @@ public sealed class Sum : AbstractBase
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period)
{
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output must have the same length", nameof(output));
}
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
int len = source.Length;
if (len == 0) return;
if (len == 0)
{
return;
}
CalculateScalarCore(source, output, period);
}
@@ -378,72 +394,82 @@ public sealed class Sum : AbstractBase
int tickCount = 0;
// Warmup phase
int warmupEnd = Math.Min(period, len);
for (int i = 0; i < warmupEnd; i++)
{
double val = source[i];
if (double.IsFinite(val))
lastValid = val;
else
val = lastValid;
int warmupEnd = Math.Min(period, len);
for (int i = 0; i < warmupEnd; i++)
{
double val = source[i];
if (double.IsFinite(val))
{
lastValid = val;
}
else
{
val = lastValid;
}
// Kahan-Babuška add
double y = val - c;
double t = sum + y;
c = t - sum - y;
sum = t;
// Kahan-Babuška add
double y = val - c;
double t = sum + y;
c = t - sum - y;
sum = t;
double z = c - cc;
double tt = sum + z;
cc = tt - sum - z;
sum = tt;
double z = c - cc;
double tt = sum + z;
cc = tt - sum - z;
sum = tt;
buffer[i] = val;
output[i] = sum;
}
buffer[i] = val;
output[i] = sum;
}
// Main phase with sliding window
for (int i = period; i < len; i++)
{
double val = source[i];
if (double.IsFinite(val))
lastValid = val;
else
val = lastValid;
// Main phase with sliding window
for (int i = period; i < len; i++)
{
double val = source[i];
if (double.IsFinite(val))
{
lastValid = val;
}
else
{
val = lastValid;
}
double oldVal = buffer[bufferIndex];
double oldVal = buffer[bufferIndex];
// Kahan-Babuška subtract old value
double yS = -oldVal - c;
double tS = sum + yS;
c = tS - sum - yS;
sum = tS;
// Kahan-Babuška subtract old value
double yS = -oldVal - c;
double tS = sum + yS;
c = tS - sum - yS;
sum = tS;
double zS = c - cc;
double ttS = sum + zS;
cc = ttS - sum - zS;
sum = ttS;
double zS = c - cc;
double ttS = sum + zS;
cc = ttS - sum - zS;
sum = ttS;
// Kahan-Babuška add new value
double yA = val - c;
double tA = sum + yA;
c = tA - sum - yA;
sum = tA;
// Kahan-Babuška add new value
double yA = val - c;
double tA = sum + yA;
c = tA - sum - yA;
sum = tA;
double zA = c - cc;
double ttA = sum + zA;
cc = ttA - sum - zA;
sum = ttA;
double zA = c - cc;
double ttA = sum + zA;
cc = ttA - sum - zA;
sum = ttA;
buffer[bufferIndex] = val;
bufferIndex++;
if (bufferIndex >= period)
bufferIndex = 0;
buffer[bufferIndex] = val;
bufferIndex++;
if (bufferIndex >= period)
{
bufferIndex = 0;
}
output[i] = sum;
output[i] = sum;
// Periodic resync for long sequences
tickCount++;
// Periodic resync for long sequences
tickCount++;
if (tickCount >= ResyncInterval)
{
tickCount = 0;
@@ -468,7 +494,10 @@ public sealed class Sum : AbstractBase
}
finally
{
if (bufferArray != null) ArrayPool<double>.Shared.Return(bufferArray);
if (bufferArray != null)
{
ArrayPool<double>.Shared.Return(bufferArray);
}
}
}
@@ -482,4 +511,4 @@ public sealed class Sum : AbstractBase
_p_state = default;
Last = default;
}
}
}
@@ -52,7 +52,9 @@ public sealed class VarianceIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
if (args.Reason != UpdateReason.NewBar && args.Reason != UpdateReason.HistoricalBar)
{
return;
}
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
double value = _priceSelector(item);
+10 -7
View File
@@ -46,20 +46,20 @@ public class VarianceTests
{
// Use simple known values for easier debugging
var variance = new Variance(3);
// Add 3 values: 1, 2, 3
variance.Update(new TValue(DateTime.UtcNow, 1), isNew: true);
variance.Update(new TValue(DateTime.UtcNow, 2), isNew: true);
var originalResult = variance.Update(new TValue(DateTime.UtcNow, 3), isNew: true);
double expectedVariance = originalResult.Value; // Variance of [1,2,3]
// Now correct the 3rd value to 10 (isNew=false)
variance.Update(new TValue(DateTime.UtcNow, 10), isNew: false);
// Correct back to original value 3 (isNew=false)
var restoredResult = variance.Update(new TValue(DateTime.UtcNow, 3), isNew: false);
// Should match original variance
Assert.Equal(expectedVariance, restoredResult.Value, 1e-10);
}
@@ -351,7 +351,10 @@ public class VarianceTests
// Create large dataset to trigger SIMD path (>= 256)
const int count = 1000;
var data = new double[count];
for (int i = 0; i < count; i++) data[i] = (double)i;
for (int i = 0; i < count; i++)
{
data[i] = (double)i;
}
var series = new TSeries(new System.Collections.Generic.List<long>(new long[count]), new System.Collections.Generic.List<double>(data));
@@ -714,4 +717,4 @@ public class VarianceTests
Assert.Equal(0, output[0]); // N=1
Assert.Equal(50, output[1]); // Var([10,20]) = 50
}
}
}
+72 -16
View File
@@ -100,7 +100,10 @@ public sealed class Variance : AbstractBase
double numerator = _sumSq - (_buffer.Sum * _buffer.Sum) / n;
// Handle floating point noise
if (numerator < 0) numerator = 0;
if (numerator < 0)
{
numerator = 0;
}
double denominator = _isPopulation ? n : (n - 1);
variance = numerator / denominator;
@@ -113,7 +116,10 @@ public sealed class Variance : 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);
@@ -181,12 +187,20 @@ public sealed class Variance : AbstractBase
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period, bool isPopulation = false)
{
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output must have the same length", nameof(output));
}
if (period < 2)
{
throw new ArgumentException("Period must be greater than or equal to 2", nameof(period));
}
int len = source.Length;
if (len == 0) return;
if (len == 0)
{
return;
}
// Try SIMD path for large, clean datasets
const int SimdThreshold = 256;
@@ -237,7 +251,10 @@ public sealed class Variance : AbstractBase
for (; i < warmupEnd; i++)
{
double val = source[i];
if (!double.IsFinite(val)) val = 0; // Fallback
if (!double.IsFinite(val))
{
val = 0; // Fallback
}
sum += val;
sumSq = Math.FusedMultiplyAdd(val, val, sumSq);
@@ -247,7 +264,11 @@ public sealed class Variance : AbstractBase
if (n > 1)
{
double numerator = sumSq - (sum * sum) / n;
if (numerator < 0) numerator = 0;
if (numerator < 0)
{
numerator = 0;
}
double denominator = isPopulation ? n : (n - 1);
output[i] = numerator / denominator;
}
@@ -262,7 +283,10 @@ public sealed class Variance : AbstractBase
for (; i < len; i++)
{
double val = source[i];
if (!double.IsFinite(val)) val = 0; // Fallback
if (!double.IsFinite(val))
{
val = 0; // Fallback
}
double oldVal = buffer[bufferIndex];
@@ -272,11 +296,18 @@ public sealed class Variance : AbstractBase
buffer[bufferIndex] = val;
bufferIndex++;
if (bufferIndex >= period) bufferIndex = 0;
if (bufferIndex >= period)
{
bufferIndex = 0;
}
double n = period;
double numerator = sumSq - (sum * sum) / n;
if (numerator < 0) numerator = 0;
if (numerator < 0)
{
numerator = 0;
}
double denominator = isPopulation ? n : (n - 1);
output[i] = numerator / denominator;
@@ -305,7 +336,11 @@ public sealed class Variance : AbstractBase
if (n > 1)
{
double num = sumSq - (sum * sum) / n;
if (num < 0) num = 0;
if (num < 0)
{
num = 0;
}
double den = isPopulation ? n : (n - 1);
Unsafe.Add(ref outRef, i) = num / den;
}
@@ -330,7 +365,10 @@ public sealed class Variance : AbstractBase
WarmupVariance(period, isPopulation, ref srcRef, ref outRef, out double sum, out double sumSq);
if (len <= period) return;
if (len <= period)
{
return;
}
var vInvN = Vector512.Create(invN);
var vInvDenom = Vector512.Create(invDenom);
@@ -420,7 +458,11 @@ public sealed class Variance : AbstractBase
sumSq = Math.FusedMultiplyAdd(val, val, sumSq);
double numerator = sumSq - sum * sum * invN;
if (numerator < 0) numerator = 0;
if (numerator < 0)
{
numerator = 0;
}
Unsafe.Add(ref outRef, i) = numerator * invDenom;
}
}
@@ -439,7 +481,10 @@ public sealed class Variance : AbstractBase
WarmupVariance(period, isPopulation, ref srcRef, ref outRef, out double sum, out double sumSq);
if (len <= period) return;
if (len <= period)
{
return;
}
var vInvN = Vector128.Create(invN);
var vInvDenom = Vector128.Create(invDenom);
@@ -517,7 +562,11 @@ public sealed class Variance : AbstractBase
sumSq = Math.FusedMultiplyAdd(val, val, sumSq);
double numerator = sumSq - sum * sum * invN;
if (numerator < 0) numerator = 0;
if (numerator < 0)
{
numerator = 0;
}
Unsafe.Add(ref outRef, i) = numerator * invDenom;
}
}
@@ -536,7 +585,10 @@ public sealed class Variance : AbstractBase
WarmupVariance(period, isPopulation, ref srcRef, ref outRef, out double sum, out double sumSq);
if (len <= period) return;
if (len <= period)
{
return;
}
var vInvN = Vector256.Create(invN);
var vInvDenom = Vector256.Create(invDenom);
@@ -634,8 +686,12 @@ public sealed class Variance : AbstractBase
sumSq = Math.FusedMultiplyAdd(val, val, sumSq);
double numerator = sumSq - sum * sum * invN;
if (numerator < 0) numerator = 0;
if (numerator < 0)
{
numerator = 0;
}
Unsafe.Add(ref outRef, i) = numerator * invDenom;
}
}
}
}