mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-17 10:08:05 +00:00
code reviews
This commit is contained in:
+133
-39
@@ -39,7 +39,7 @@ public static class ErrorHelpers
|
||||
// Try SIMD path - NaN detection is integrated into the SIMD loop
|
||||
if (Avx2.IsSupported && len >= Vector256<double>.Count)
|
||||
{
|
||||
int processedCount = ComputeSignedErrorsSimdWithNaNDetection(actual, predicted, output, lastValidActual, lastValidPredicted);
|
||||
int processedCount = ComputeSignedErrorsSimdWithNaNDetection(actual, predicted, output, ref lastValidActual, ref lastValidPredicted);
|
||||
if (processedCount == len)
|
||||
return; // All processed via SIMD
|
||||
// Continue with scalar for remaining elements (NaN was detected)
|
||||
@@ -74,7 +74,7 @@ public static class ErrorHelpers
|
||||
// Try SIMD path - NaN detection is integrated into the SIMD loop (avoids double-pass)
|
||||
if (Avx2.IsSupported && len >= Vector256<double>.Count)
|
||||
{
|
||||
int processedCount = ComputeAbsoluteErrorsSimdWithNaNDetection(actual, predicted, output, lastValidActual, lastValidPredicted);
|
||||
int processedCount = ComputeAbsoluteErrorsSimdWithNaNDetection(actual, predicted, output, ref lastValidActual, ref lastValidPredicted);
|
||||
if (processedCount == len)
|
||||
return; // All processed via SIMD
|
||||
// Continue with scalar for remaining elements (NaN was detected)
|
||||
@@ -88,7 +88,7 @@ public static class ErrorHelpers
|
||||
|
||||
/// <summary>
|
||||
/// Computes squared errors: (actual - predicted)²
|
||||
/// Uses AVX2 SIMD when available for clean data, with scalar fallback for NaN handling.
|
||||
/// Uses AVX2 SIMD when available with integrated NaN detection, with scalar fallback for NaN handling.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ComputeSquaredErrors(
|
||||
@@ -106,10 +106,14 @@ public static class ErrorHelpers
|
||||
double lastValidActual = FindFirstValidValue(actual);
|
||||
double lastValidPredicted = FindFirstValidValue(predicted);
|
||||
|
||||
// Try SIMD path for clean data (no NaN/Inf)
|
||||
if (Avx2.IsSupported && len >= Vector256<double>.Count && IsDataClean(actual, predicted))
|
||||
// Try SIMD path - NaN detection is integrated into the SIMD loop (avoids double-pass)
|
||||
if (Avx2.IsSupported && len >= Vector256<double>.Count)
|
||||
{
|
||||
ComputeSquaredErrorsSimd(actual, predicted, output);
|
||||
int processedCount = ComputeSquaredErrorsSimdWithNaNDetection(actual, predicted, output, ref lastValidActual, ref lastValidPredicted);
|
||||
if (processedCount == len)
|
||||
return; // All processed via SIMD
|
||||
// Continue with scalar for remaining elements (NaN was detected)
|
||||
ComputeSquaredErrorsScalar(actual.Slice(processedCount), predicted.Slice(processedCount), output.Slice(processedCount), lastValidActual, lastValidPredicted);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -187,19 +191,15 @@ public static class ErrorHelpers
|
||||
double act = actual[i];
|
||||
double pred = predicted[i];
|
||||
|
||||
if (double.IsFinite(act)) currentValidActual = act; else act = currentValidActual;
|
||||
if (double.IsFinite(pred)) currentValidPredicted = pred; else pred = currentValidPredicted;
|
||||
#pragma warning disable S1121 // Assignments should not be made from within sub-expressions
|
||||
act = double.IsFinite(act) ? (currentValidActual = act) : currentValidActual;
|
||||
pred = double.IsFinite(pred) ? (currentValidPredicted = pred) : currentValidPredicted;
|
||||
#pragma warning restore S1121
|
||||
|
||||
double absActual = Math.Abs(act);
|
||||
if (absActual < epsilon)
|
||||
{
|
||||
// Avoid division by zero - use absolute error as fallback
|
||||
output[i] = Math.Abs(act - pred);
|
||||
}
|
||||
else
|
||||
{
|
||||
output[i] = Math.Abs(act - pred) / absActual * 100.0;
|
||||
}
|
||||
output[i] = absActual < epsilon
|
||||
? Math.Abs(act - pred)
|
||||
: Math.Abs(act - pred) / absActual * 100.0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,14 +236,9 @@ public static class ErrorHelpers
|
||||
if (double.IsFinite(pred)) currentValidPredicted = pred; else pred = currentValidPredicted;
|
||||
|
||||
double denominator = (Math.Abs(act) + Math.Abs(pred)) / 2.0;
|
||||
if (denominator < epsilon)
|
||||
{
|
||||
output[i] = 0.0; // Both values near zero
|
||||
}
|
||||
else
|
||||
{
|
||||
output[i] = Math.Abs(act - pred) / denominator * 100.0;
|
||||
}
|
||||
output[i] = denominator < epsilon
|
||||
? 0.0 // Both values near zero
|
||||
: Math.Abs(act - pred) / denominator * 100.0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -413,14 +408,9 @@ public static class ErrorHelpers
|
||||
double diff = act - pred;
|
||||
double absDiff = Math.Abs(diff);
|
||||
|
||||
if (absDiff <= delta)
|
||||
{
|
||||
output[i] = 0.5 * diff * diff;
|
||||
}
|
||||
else
|
||||
{
|
||||
output[i] = delta * (absDiff - halfDelta);
|
||||
}
|
||||
output[i] = absDiff <= delta
|
||||
? 0.5 * diff * diff
|
||||
: delta * (absDiff - halfDelta);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -735,14 +725,15 @@ public static class ErrorHelpers
|
||||
/// <summary>
|
||||
/// SIMD path with integrated NaN detection. Returns the number of elements processed.
|
||||
/// If NaN is detected, returns the index where NaN was found so caller can continue with scalar.
|
||||
/// Updates lastValidActual/lastValidPredicted to track last seen finite values for scalar continuation.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int ComputeSignedErrorsSimdWithNaNDetection(
|
||||
ReadOnlySpan<double> actual,
|
||||
ReadOnlySpan<double> predicted,
|
||||
Span<double> output,
|
||||
double lastValidActual,
|
||||
double lastValidPredicted)
|
||||
ref double lastValidActual,
|
||||
ref double lastValidPredicted)
|
||||
{
|
||||
int len = actual.Length;
|
||||
int vectorSize = Vector256<double>.Count;
|
||||
@@ -762,7 +753,12 @@ public static class ErrorHelpers
|
||||
int mask = Avx.MoveMask(combined);
|
||||
if (mask != 0b1111)
|
||||
{
|
||||
// NaN detected - return current position for scalar fallback
|
||||
// NaN detected - update lastValid from previously processed elements before returning
|
||||
if (i > 0)
|
||||
{
|
||||
lastValidActual = actual[i - 1];
|
||||
lastValidPredicted = predicted[i - 1];
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
@@ -771,6 +767,13 @@ public static class ErrorHelpers
|
||||
errorVec.StoreUnsafe(ref MemoryMarshal.GetReference(output.Slice(i)));
|
||||
}
|
||||
|
||||
// Update lastValid from end of SIMD-processed section
|
||||
if (i > 0)
|
||||
{
|
||||
lastValidActual = actual[i - 1];
|
||||
lastValidPredicted = predicted[i - 1];
|
||||
}
|
||||
|
||||
// Handle scalar remainder
|
||||
for (; i < len; i++)
|
||||
{
|
||||
@@ -783,6 +786,8 @@ public static class ErrorHelpers
|
||||
return i;
|
||||
}
|
||||
|
||||
lastValidActual = act;
|
||||
lastValidPredicted = pred;
|
||||
output[i] = act - pred;
|
||||
}
|
||||
|
||||
@@ -845,14 +850,15 @@ public static class ErrorHelpers
|
||||
/// <summary>
|
||||
/// SIMD path with integrated NaN detection for absolute errors. Returns the number of elements processed.
|
||||
/// If NaN is detected, returns the index where NaN was found so caller can continue with scalar.
|
||||
/// Updates lastValidActual/lastValidPredicted to track last seen finite values for scalar continuation.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int ComputeAbsoluteErrorsSimdWithNaNDetection(
|
||||
ReadOnlySpan<double> actual,
|
||||
ReadOnlySpan<double> predicted,
|
||||
Span<double> output,
|
||||
double lastValidActual,
|
||||
double lastValidPredicted)
|
||||
ref double lastValidActual,
|
||||
ref double lastValidPredicted)
|
||||
{
|
||||
int len = actual.Length;
|
||||
int vectorSize = Vector256<double>.Count;
|
||||
@@ -875,7 +881,12 @@ public static class ErrorHelpers
|
||||
int mask = Avx.MoveMask(combined);
|
||||
if (mask != 0b1111)
|
||||
{
|
||||
// NaN detected - return current position for scalar fallback
|
||||
// NaN detected - update lastValid from previously processed elements before returning
|
||||
if (i > 0)
|
||||
{
|
||||
lastValidActual = actual[i - 1];
|
||||
lastValidPredicted = predicted[i - 1];
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
@@ -885,6 +896,13 @@ public static class ErrorHelpers
|
||||
absErrorVec.StoreUnsafe(ref MemoryMarshal.GetReference(output.Slice(i)));
|
||||
}
|
||||
|
||||
// Update lastValid from end of SIMD-processed section
|
||||
if (i > 0)
|
||||
{
|
||||
lastValidActual = actual[i - 1];
|
||||
lastValidPredicted = predicted[i - 1];
|
||||
}
|
||||
|
||||
// Handle scalar remainder
|
||||
for (; i < len; i++)
|
||||
{
|
||||
@@ -897,12 +915,88 @@ public static class ErrorHelpers
|
||||
return i;
|
||||
}
|
||||
|
||||
lastValidActual = act;
|
||||
lastValidPredicted = pred;
|
||||
output[i] = Math.Abs(act - pred);
|
||||
}
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SIMD path with integrated NaN detection for squared errors. Returns the number of elements processed.
|
||||
/// If NaN is detected, returns the index where NaN was found so caller can continue with scalar.
|
||||
/// Updates lastValidActual/lastValidPredicted to track last seen finite values for scalar continuation.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int ComputeSquaredErrorsSimdWithNaNDetection(
|
||||
ReadOnlySpan<double> actual,
|
||||
ReadOnlySpan<double> predicted,
|
||||
Span<double> output,
|
||||
ref double lastValidActual,
|
||||
ref double lastValidPredicted)
|
||||
{
|
||||
int len = actual.Length;
|
||||
int vectorSize = Vector256<double>.Count;
|
||||
int vectorEnd = len - (len % vectorSize);
|
||||
|
||||
int i = 0;
|
||||
for (; i < vectorEnd; i += vectorSize)
|
||||
{
|
||||
Vector256<double> actVec = Vector256.LoadUnsafe(ref MemoryMarshal.GetReference(actual.Slice(i)));
|
||||
Vector256<double> predVec = Vector256.LoadUnsafe(ref MemoryMarshal.GetReference(predicted.Slice(i)));
|
||||
|
||||
// Check for NaN/Inf: x == x is false for NaN
|
||||
Vector256<double> actCmp = Avx.Compare(actVec, actVec, FloatComparisonMode.OrderedNonSignaling);
|
||||
Vector256<double> predCmp = Avx.Compare(predVec, predVec, FloatComparisonMode.OrderedNonSignaling);
|
||||
Vector256<double> combined = Avx.And(actCmp, predCmp);
|
||||
|
||||
int mask = Avx.MoveMask(combined);
|
||||
if (mask != 0b1111)
|
||||
{
|
||||
// NaN detected - update lastValid from previously processed elements before returning
|
||||
if (i > 0)
|
||||
{
|
||||
lastValidActual = actual[i - 1];
|
||||
lastValidPredicted = predicted[i - 1];
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
// No NaN - compute squared error: (actual - predicted)²
|
||||
Vector256<double> errorVec = Avx.Subtract(actVec, predVec);
|
||||
Vector256<double> sqErrorVec = Avx.Multiply(errorVec, errorVec);
|
||||
sqErrorVec.StoreUnsafe(ref MemoryMarshal.GetReference(output.Slice(i)));
|
||||
}
|
||||
|
||||
// Update lastValid from end of SIMD-processed section
|
||||
if (i > 0)
|
||||
{
|
||||
lastValidActual = actual[i - 1];
|
||||
lastValidPredicted = predicted[i - 1];
|
||||
}
|
||||
|
||||
// Handle scalar remainder
|
||||
for (; i < len; i++)
|
||||
{
|
||||
double act = actual[i];
|
||||
double pred = predicted[i];
|
||||
|
||||
if (!double.IsFinite(act) || !double.IsFinite(pred))
|
||||
{
|
||||
// Return current position - caller will handle with scalar fallback
|
||||
return i;
|
||||
}
|
||||
|
||||
lastValidActual = act;
|
||||
lastValidPredicted = pred;
|
||||
double diff = act - pred;
|
||||
output[i] = diff * diff;
|
||||
}
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void ComputeAbsoluteErrorsSimd(
|
||||
ReadOnlySpan<double> actual,
|
||||
|
||||
@@ -289,18 +289,18 @@ public class SimdExtensionsTests
|
||||
|
||||
// VarianceSIMD tests
|
||||
[Fact]
|
||||
public void VarianceSIMD_LessThanTwoElements_ReturnsNaN()
|
||||
public void VarianceSIMD_LessThanTwoElements_ReturnsZero()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.True(double.IsNaN(span.VarianceSIMD()));
|
||||
Assert.Equal(0.0, span.VarianceSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceSIMD_EmptySpan_ReturnsNaN()
|
||||
public void VarianceSIMD_EmptySpan_ReturnsZero()
|
||||
{
|
||||
var span = ReadOnlySpan<double>.Empty;
|
||||
Assert.True(double.IsNaN(span.VarianceSIMD()));
|
||||
Assert.Equal(0.0, span.VarianceSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -631,8 +631,8 @@ public class SimdExtensionsTests
|
||||
Assert.True(variance > 0);
|
||||
Assert.True(stdDev > 0);
|
||||
|
||||
Assert.True(sw.ElapsedMilliseconds < 50,
|
||||
$"SIMD operations took {sw.ElapsedMilliseconds}ms, expected < 50ms");
|
||||
Assert.True(sw.ElapsedMilliseconds < 100,
|
||||
$"SIMD operations took {sw.ElapsedMilliseconds}ms, expected < 100ms");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -870,26 +870,26 @@ public class SimdScalarFallbackTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceSIMD_SingleElement_ReturnsNaN()
|
||||
public void VarianceSIMD_SingleElement_ReturnsZero()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.True(double.IsNaN(span.VarianceSIMD()));
|
||||
Assert.Equal(0.0, span.VarianceSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StdDevSIMD_SingleElement_ReturnsNaN()
|
||||
public void StdDevSIMD_SingleElement_ReturnsZero()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.True(double.IsNaN(span.StdDevSIMD()));
|
||||
Assert.Equal(0.0, span.StdDevSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StdDevSIMD_EmptySpan_ReturnsNaN()
|
||||
public void StdDevSIMD_EmptySpan_ReturnsZero()
|
||||
{
|
||||
var span = ReadOnlySpan<double>.Empty;
|
||||
Assert.True(double.IsNaN(span.StdDevSIMD()));
|
||||
Assert.Equal(0.0, span.StdDevSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -992,4 +992,4 @@ public class SimdScalarFallbackTests
|
||||
|
||||
Assert.Throws<ArgumentException>(() => SimdExtensions.Subtract(left, right, result));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -299,7 +299,8 @@ public static class SimdExtensions
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double VarianceSIMD(this ReadOnlySpan<double> span, double? mean = null)
|
||||
{
|
||||
if (span.Length < 2) return double.NaN;
|
||||
// Match VarianceScalar behavior: return 0.0 for length <= 1 to avoid inconsistency
|
||||
if (span.Length <= 1) return 0.0;
|
||||
|
||||
double m;
|
||||
if (mean.HasValue)
|
||||
@@ -627,32 +628,44 @@ public static class SimdExtensions
|
||||
ref double aRef = ref MemoryMarshal.GetReference(a);
|
||||
ref double bRef = ref MemoryMarshal.GetReference(b);
|
||||
|
||||
// Hoist FMA check outside loop for branch prediction optimization
|
||||
bool useFma = Fma.IsSupported;
|
||||
|
||||
// Unroll loop: Process 16 doubles (4 vectors) at a time
|
||||
if (len >= 16)
|
||||
{
|
||||
for (; i <= len - 16; i += 16)
|
||||
if (useFma)
|
||||
{
|
||||
var va1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i));
|
||||
var vb1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i));
|
||||
|
||||
var va2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 4));
|
||||
var vb2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 4));
|
||||
|
||||
var va3 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 8));
|
||||
var vb3 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 8));
|
||||
|
||||
var va4 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 12));
|
||||
var vb4 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 12));
|
||||
|
||||
if (Fma.IsSupported)
|
||||
for (; i <= len - 16; i += 16)
|
||||
{
|
||||
var va1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i));
|
||||
var vb1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i));
|
||||
var va2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 4));
|
||||
var vb2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 4));
|
||||
var va3 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 8));
|
||||
var vb3 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 8));
|
||||
var va4 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 12));
|
||||
var vb4 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 12));
|
||||
|
||||
vSum = Fma.MultiplyAdd(va1, vb1, vSum);
|
||||
vSum2 = Fma.MultiplyAdd(va2, vb2, vSum2);
|
||||
vSum3 = Fma.MultiplyAdd(va3, vb3, vSum3);
|
||||
vSum4 = Fma.MultiplyAdd(va4, vb4, vSum4);
|
||||
}
|
||||
else
|
||||
}
|
||||
else
|
||||
{
|
||||
for (; i <= len - 16; i += 16)
|
||||
{
|
||||
var va1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i));
|
||||
var vb1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i));
|
||||
var va2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 4));
|
||||
var vb2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 4));
|
||||
var va3 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 8));
|
||||
var vb3 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 8));
|
||||
var va4 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 12));
|
||||
var vb4 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 12));
|
||||
|
||||
vSum = Avx.Add(vSum, Avx.Multiply(va1, vb1));
|
||||
vSum2 = Avx.Add(vSum2, Avx.Multiply(va2, vb2));
|
||||
vSum3 = Avx.Add(vSum3, Avx.Multiply(va3, vb3));
|
||||
@@ -661,15 +674,24 @@ public static class SimdExtensions
|
||||
}
|
||||
}
|
||||
|
||||
// Process remaining vectors (4 doubles at a time)
|
||||
for (; i <= len - 4; i += 4)
|
||||
// Process remaining vectors (4 doubles at a time) with hoisted branch
|
||||
if (useFma)
|
||||
{
|
||||
var va = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i));
|
||||
var vb = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i));
|
||||
|
||||
vSum = Fma.IsSupported
|
||||
? Fma.MultiplyAdd(va, vb, vSum)
|
||||
: Avx.Add(vSum, Avx.Multiply(va, vb));
|
||||
for (; i <= len - 4; i += 4)
|
||||
{
|
||||
var va = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i));
|
||||
var vb = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i));
|
||||
vSum = Fma.MultiplyAdd(va, vb, vSum);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (; i <= len - 4; i += 4)
|
||||
{
|
||||
var va = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i));
|
||||
var vb = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i));
|
||||
vSum = Avx.Add(vSum, Avx.Multiply(va, vb));
|
||||
}
|
||||
}
|
||||
|
||||
// Combine accumulators
|
||||
|
||||
@@ -395,7 +395,7 @@ public class TBarTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TBar_WithInfinity_HandlesGracefully()
|
||||
public void TBar_WithPositiveInfinity_HandlesGracefully()
|
||||
{
|
||||
var bar = new TBar(12345, 100, double.PositiveInfinity, 90, 105, 1000);
|
||||
|
||||
@@ -404,6 +404,17 @@ public class TBarTests
|
||||
Assert.True(double.IsPositiveInfinity(bar.HL2)); // Uses High
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TBar_WithNegativeInfinity_HandlesGracefully()
|
||||
{
|
||||
var bar = new TBar(12345, 100, 110, double.NegativeInfinity, 105, 1000);
|
||||
|
||||
Assert.True(double.IsNegativeInfinity(bar.Low));
|
||||
Assert.True(double.IsNegativeInfinity(bar.L.Value));
|
||||
Assert.True(double.IsNegativeInfinity(bar.HL2)); // Uses Low
|
||||
Assert.True(double.IsNegativeInfinity(bar.HLC3)); // Uses Low
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TBar_WithMaxValue_HandlesGracefully()
|
||||
{
|
||||
@@ -412,8 +423,8 @@ public class TBarTests
|
||||
Assert.Equal(double.MaxValue, bar.Open);
|
||||
Assert.Equal(double.MaxValue, bar.High);
|
||||
Assert.Equal(double.MinValue, bar.Low);
|
||||
// HL2 calculation with extreme values
|
||||
Assert.True(double.IsFinite(bar.HL2) || double.IsInfinity(bar.HL2));
|
||||
// HL2 = (MaxValue + MinValue) * 0.5 = 0 (symmetric around zero)
|
||||
Assert.Equal(0.0, bar.HL2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -497,4 +508,4 @@ public class TBarTests
|
||||
// (90 + 120 + 60) / 3 = 270 / 3 = 90
|
||||
Assert.Equal(90.0, bar.OHL3);
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-2
@@ -23,11 +23,19 @@ public readonly record struct TBar(long Time, double Open, double High, double L
|
||||
// Computed properties (calculated on demand, no storage overhead)
|
||||
public double HL2 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low) * 0.5; }
|
||||
public double OC2 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (Open + Close) * 0.5; }
|
||||
public double OHL3 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (Open + High + Low) / 3.0; }
|
||||
public double HLC3 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low + Close) / 3.0; }
|
||||
public double OHL3 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (Open + High + Low) * (1.0 / 3.0); }
|
||||
public double HLC3 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low + Close) * (1.0 / 3.0); }
|
||||
public double OHLC4 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (Open + High + Low + Close) * 0.25; }
|
||||
public double HLCC4 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low + Close + Close) * 0.25; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a TBar from DateTime and OHLCV values.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Performance warning:</b> If <paramref name="time"/>.Kind is not <see cref="DateTimeKind.Utc"/>,
|
||||
/// <see cref="DateTime.ToUniversalTime"/> is called, which allocates. For hot paths, prefer the
|
||||
/// primary constructor with pre-computed UTC ticks.
|
||||
/// </remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TBar(DateTime time, double open, double high, double low, double close, double volume)
|
||||
: this(time.Kind == DateTimeKind.Utc ? time.Ticks : time.ToUniversalTime().Ticks, open, high, low, close, volume)
|
||||
|
||||
@@ -281,6 +281,11 @@ public class TBarSeries : IReadOnlyList<TBar>
|
||||
public void Add(DateTime time, double open, double high, double low, double close, double volume, bool isNew = true) =>
|
||||
Add(new TBar(time.Ticks, open, high, low, close, volume), isNew);
|
||||
|
||||
/// <summary>
|
||||
/// Bulk add from IEnumerable sources. Fires Pub event for each bar.
|
||||
/// WARNING: This method may allocate if inputs are not already arrays.
|
||||
/// For zero-allocation bulk loading, prefer AddRange with ReadOnlySpan parameters.
|
||||
/// </summary>
|
||||
public void Add(IEnumerable<long> t, IEnumerable<double> o, IEnumerable<double> h, IEnumerable<double> l, IEnumerable<double> c, IEnumerable<double> v)
|
||||
{
|
||||
var tArr = t as long[] ?? t.ToArray();
|
||||
@@ -320,28 +325,37 @@ public class TBarSeries : IReadOnlyList<TBar>
|
||||
if (o.Length != len || h.Length != len || l.Length != len || c.Length != len || v.Length != len)
|
||||
throw new ArgumentException("All spans must have the same length", nameof(t));
|
||||
|
||||
if (len == 0) return;
|
||||
|
||||
int oldCount = _c.Count;
|
||||
int newCount = oldCount + len;
|
||||
|
||||
// Pre-allocate capacity to avoid repeated resizing
|
||||
int newCapacity = _c.Count + len;
|
||||
if (_t.Capacity < newCapacity)
|
||||
if (_t.Capacity < newCount)
|
||||
{
|
||||
_t.Capacity = newCapacity;
|
||||
_o.Capacity = newCapacity;
|
||||
_h.Capacity = newCapacity;
|
||||
_l.Capacity = newCapacity;
|
||||
_c.Capacity = newCapacity;
|
||||
_v.Capacity = newCapacity;
|
||||
_t.Capacity = newCount;
|
||||
_o.Capacity = newCount;
|
||||
_h.Capacity = newCount;
|
||||
_l.Capacity = newCount;
|
||||
_c.Capacity = newCount;
|
||||
_v.Capacity = newCount;
|
||||
}
|
||||
|
||||
// Bulk add without event firing (for initial data loading)
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
_t.Add(t[i]);
|
||||
_o.Add(o[i]);
|
||||
_h.Add(h[i]);
|
||||
_l.Add(l[i]);
|
||||
_c.Add(c[i]);
|
||||
_v.Add(v[i]);
|
||||
}
|
||||
// Use SetCount to resize lists without zeroing, then copy via span
|
||||
CollectionsMarshal.SetCount(_t, newCount);
|
||||
CollectionsMarshal.SetCount(_o, newCount);
|
||||
CollectionsMarshal.SetCount(_h, newCount);
|
||||
CollectionsMarshal.SetCount(_l, newCount);
|
||||
CollectionsMarshal.SetCount(_c, newCount);
|
||||
CollectionsMarshal.SetCount(_v, newCount);
|
||||
|
||||
// Direct span copy - zero allocation bulk add
|
||||
t.CopyTo(CollectionsMarshal.AsSpan(_t).Slice(oldCount));
|
||||
o.CopyTo(CollectionsMarshal.AsSpan(_o).Slice(oldCount));
|
||||
h.CopyTo(CollectionsMarshal.AsSpan(_h).Slice(oldCount));
|
||||
l.CopyTo(CollectionsMarshal.AsSpan(_l).Slice(oldCount));
|
||||
c.CopyTo(CollectionsMarshal.AsSpan(_c).Slice(oldCount));
|
||||
v.CopyTo(CollectionsMarshal.AsSpan(_v).Slice(oldCount));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -354,28 +368,46 @@ public class TBarSeries : IReadOnlyList<TBar>
|
||||
int len = bars.Length;
|
||||
if (len == 0) return;
|
||||
|
||||
int oldCount = _c.Count;
|
||||
int newCount = oldCount + len;
|
||||
|
||||
// Pre-allocate capacity to avoid repeated resizing
|
||||
int newCapacity = _c.Count + len;
|
||||
if (_t.Capacity < newCapacity)
|
||||
if (_t.Capacity < newCount)
|
||||
{
|
||||
_t.Capacity = newCapacity;
|
||||
_o.Capacity = newCapacity;
|
||||
_h.Capacity = newCapacity;
|
||||
_l.Capacity = newCapacity;
|
||||
_c.Capacity = newCapacity;
|
||||
_v.Capacity = newCapacity;
|
||||
_t.Capacity = newCount;
|
||||
_o.Capacity = newCount;
|
||||
_h.Capacity = newCount;
|
||||
_l.Capacity = newCount;
|
||||
_c.Capacity = newCount;
|
||||
_v.Capacity = newCount;
|
||||
}
|
||||
|
||||
// Bulk add without event firing (for initial data loading)
|
||||
// Use SetCount to resize lists without zeroing
|
||||
CollectionsMarshal.SetCount(_t, newCount);
|
||||
CollectionsMarshal.SetCount(_o, newCount);
|
||||
CollectionsMarshal.SetCount(_h, newCount);
|
||||
CollectionsMarshal.SetCount(_l, newCount);
|
||||
CollectionsMarshal.SetCount(_c, newCount);
|
||||
CollectionsMarshal.SetCount(_v, newCount);
|
||||
|
||||
// Get mutable spans for direct write
|
||||
Span<long> tSpan = CollectionsMarshal.AsSpan(_t).Slice(oldCount);
|
||||
Span<double> oSpan = CollectionsMarshal.AsSpan(_o).Slice(oldCount);
|
||||
Span<double> hSpan = CollectionsMarshal.AsSpan(_h).Slice(oldCount);
|
||||
Span<double> lSpan = CollectionsMarshal.AsSpan(_l).Slice(oldCount);
|
||||
Span<double> cSpan = CollectionsMarshal.AsSpan(_c).Slice(oldCount);
|
||||
Span<double> vSpan = CollectionsMarshal.AsSpan(_v).Slice(oldCount);
|
||||
|
||||
// Copy from TBar structs to SoA layout
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
ref readonly TBar bar = ref bars[i];
|
||||
_t.Add(bar.Time);
|
||||
_o.Add(bar.Open);
|
||||
_h.Add(bar.High);
|
||||
_l.Add(bar.Low);
|
||||
_c.Add(bar.Close);
|
||||
_v.Add(bar.Volume);
|
||||
tSpan[i] = bar.Time;
|
||||
oSpan[i] = bar.Open;
|
||||
hSpan[i] = bar.High;
|
||||
lSpan[i] = bar.Low;
|
||||
cSpan[i] = bar.Close;
|
||||
vSpan[i] = bar.Volume;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class TValueTests
|
||||
@@ -42,7 +44,7 @@ public class TValueTests
|
||||
var dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
var tValue = new TValue(dt.Ticks, 123.456);
|
||||
|
||||
string result = tValue.ToString();
|
||||
string result = tValue.ToString(null, CultureInfo.InvariantCulture);
|
||||
|
||||
Assert.Contains("2023-01-01", result, StringComparison.Ordinal);
|
||||
Assert.Contains("12:00:00", result, StringComparison.Ordinal);
|
||||
@@ -288,7 +290,7 @@ public class TValueTests
|
||||
var dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
var tValue = new TValue(dt.Ticks, double.NaN);
|
||||
|
||||
string result = tValue.ToString();
|
||||
string result = tValue.ToString(null, CultureInfo.InvariantCulture);
|
||||
|
||||
Assert.Contains("NaN", result, StringComparison.Ordinal);
|
||||
}
|
||||
@@ -299,7 +301,7 @@ public class TValueTests
|
||||
var dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
var tValue = new TValue(dt.Ticks, double.PositiveInfinity);
|
||||
|
||||
string result = tValue.ToString();
|
||||
string result = tValue.ToString(null, CultureInfo.InvariantCulture);
|
||||
|
||||
Assert.Contains("∞", result, StringComparison.Ordinal);
|
||||
}
|
||||
@@ -310,7 +312,7 @@ public class TValueTests
|
||||
var dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
var tValue = new TValue(dt.Ticks, -123.456);
|
||||
|
||||
string result = tValue.ToString();
|
||||
string result = tValue.ToString(null, CultureInfo.InvariantCulture);
|
||||
|
||||
Assert.Contains("-123.46", result, StringComparison.Ordinal);
|
||||
}
|
||||
@@ -340,9 +342,11 @@ public class TValueTests
|
||||
{
|
||||
var tv = new TValue(12345, double.NaN);
|
||||
|
||||
var hash = tv.GetHashCode();
|
||||
// Should not throw - the record struct implementation handles NaN correctly
|
||||
int hash = tv.GetHashCode();
|
||||
|
||||
Assert.True(hash != 0 || hash == 0); // Just verify it doesn't throw
|
||||
// Hash should be consistent for same NaN value
|
||||
Assert.Equal(hash, tv.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -6,10 +6,11 @@ namespace QuanTAlib;
|
||||
/// <summary>
|
||||
/// A lightweight struct representing a time-value pair.
|
||||
/// Pure data type: 16 bytes (long + double).
|
||||
/// Implements ISpanFormattable for allocation-free formatting.
|
||||
/// </summary>
|
||||
[SkipLocalsInit]
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
public readonly record struct TValue(long Time, double Value)
|
||||
public readonly record struct TValue(long Time, double Value) : ISpanFormattable
|
||||
{
|
||||
public DateTime AsDateTime => new(Time, DateTimeKind.Utc);
|
||||
|
||||
@@ -37,4 +38,90 @@ public readonly record struct TValue(long Time, double Value)
|
||||
};
|
||||
return $"[{AsDateTime:yyyy-MM-dd HH:mm:ss}, {valueStr}]";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats the TValue using the specified format string.
|
||||
/// Note: Custom format and formatProvider are not supported by TValue.
|
||||
/// If a non-null/non-empty format is provided, a NotSupportedException is thrown.
|
||||
/// </summary>
|
||||
/// <param name="format">Must be null or empty; custom formats are not supported.</param>
|
||||
/// <param name="formatProvider">Ignored; TValue uses its own fixed format.</param>
|
||||
/// <returns>The string representation of this TValue.</returns>
|
||||
/// <exception cref="NotSupportedException">Thrown when a non-null/non-empty format is provided.</exception>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public string ToString(string? format, IFormatProvider? formatProvider)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(format))
|
||||
throw new NotSupportedException($"Custom format '{format}' is not supported by TValue. Use ToString() for the default format.");
|
||||
|
||||
return ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats the TValue into the provided span without heap allocation.
|
||||
/// Format: "[yyyy-MM-dd HH:mm:ss, value]"
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider)
|
||||
{
|
||||
charsWritten = 0;
|
||||
|
||||
// Early reject for buffers too small to hold even the timestamp portion.
|
||||
// This is a heuristic check; actual buffer-overflow protection is performed
|
||||
// by the explicit length checks that guard each write operation below.
|
||||
if (destination.Length < 24)
|
||||
return false;
|
||||
|
||||
// Write opening bracket
|
||||
destination[0] = '[';
|
||||
int pos = 1;
|
||||
|
||||
// Format datetime: yyyy-MM-dd HH:mm:ss (19 chars)
|
||||
if (!AsDateTime.TryFormat(destination.Slice(pos), out int dtChars, "yyyy-MM-dd HH:mm:ss", provider))
|
||||
return false;
|
||||
pos += dtChars;
|
||||
|
||||
// Write separator
|
||||
if (pos + 2 > destination.Length)
|
||||
return false;
|
||||
destination[pos++] = ',';
|
||||
destination[pos++] = ' ';
|
||||
|
||||
// Format value
|
||||
if (double.IsPositiveInfinity(Value))
|
||||
{
|
||||
if (pos + 1 > destination.Length)
|
||||
return false;
|
||||
destination[pos++] = (char)0x221E; //
|
||||
}
|
||||
else if (double.IsNegativeInfinity(Value))
|
||||
{
|
||||
if (pos + 2 > destination.Length)
|
||||
return false;
|
||||
destination[pos++] = '-';
|
||||
destination[pos++] = (char)0x221E; // -
|
||||
}
|
||||
else if (double.IsNaN(Value))
|
||||
{
|
||||
if (pos + 3 > destination.Length)
|
||||
return false;
|
||||
destination[pos++] = 'N';
|
||||
destination[pos++] = 'a';
|
||||
destination[pos++] = 'N';
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Value.TryFormat(destination.Slice(pos), out int valueChars, "F2", provider))
|
||||
return false;
|
||||
pos += valueChars;
|
||||
}
|
||||
|
||||
// Write closing bracket
|
||||
if (pos + 1 > destination.Length)
|
||||
return false;
|
||||
destination[pos++] = ']';
|
||||
|
||||
charsWritten = pos;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user