mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-20 03:28:05 +00:00
Add validation tests for various volume and momentum indicators
- Introduced Massi validation tests to ensure mathematical properties hold for the Mass Index indicator. - Added Va validation tests for Volume Accumulation, checking for finite outputs and correct accumulation behavior. - Implemented Vf validation tests for Volume Force, verifying outputs for rising and falling prices, and ensuring batch and streaming results match. - Created Vo validation tests for Volume Oscillator, confirming behavior with constant, increasing, and decreasing volumes. - Developed Vroc validation tests for Volume Rate of Change, validating outputs for constant volume and changes in volume. - Updated project file to include new momentum indicators (MACD and RSI) in the compilation.
This commit is contained in:
@@ -927,60 +927,6 @@ public static class ErrorHelpers
|
||||
|
||||
#region Private Helpers
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool IsDataClean(ReadOnlySpan<double> actual, ReadOnlySpan<double> predicted)
|
||||
{
|
||||
int len = actual.Length;
|
||||
|
||||
// SIMD path for AVX-supported systems
|
||||
if (Avx.IsSupported && len >= Vector256<double>.Count)
|
||||
{
|
||||
int vectorSize = Vector256<double>.Count;
|
||||
int vectorEnd = len - (len % vectorSize);
|
||||
|
||||
for (int i = 0; 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)));
|
||||
|
||||
// NaN check: x == x is false for NaN
|
||||
// Compare each vector with itself - OrderedQ returns all-ones for finite, zero for NaN
|
||||
Vector256<double> actCmp = Avx.Compare(actVec, actVec, FloatComparisonMode.OrderedNonSignaling);
|
||||
Vector256<double> predCmp = Avx.Compare(predVec, predVec, FloatComparisonMode.OrderedNonSignaling);
|
||||
|
||||
// Combine: both must be all-ones (finite)
|
||||
Vector256<double> combined = Avx.And(actCmp, predCmp);
|
||||
|
||||
// MoveMask returns a bitmask; all-ones means all finite (mask == 0b1111 for 4 doubles)
|
||||
int mask = Avx.MoveMask(combined);
|
||||
if (mask != 0b1111)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Scalar tail
|
||||
for (int i = vectorEnd; i < len; i++)
|
||||
{
|
||||
if (!double.IsFinite(actual[i]) || !double.IsFinite(predicted[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Scalar fallback
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
if (!double.IsFinite(actual[i]) || !double.IsFinite(predicted[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <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.
|
||||
@@ -1053,35 +999,6 @@ public static class ErrorHelpers
|
||||
return len;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void ComputeSignedErrorsSimd(
|
||||
ReadOnlySpan<double> actual,
|
||||
ReadOnlySpan<double> predicted,
|
||||
Span<double> output)
|
||||
{
|
||||
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)));
|
||||
|
||||
// error = actual - predicted (preserves sign)
|
||||
Vector256<double> errorVec = Avx.Subtract(actVec, predVec);
|
||||
|
||||
errorVec.StoreUnsafe(ref MemoryMarshal.GetReference(output.Slice(i)));
|
||||
}
|
||||
|
||||
// Handle remainder with scalar
|
||||
for (; i < len; i++)
|
||||
{
|
||||
output[i] = actual[i] - predicted[i];
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void ComputeSignedErrorsScalar(
|
||||
ReadOnlySpan<double> actual,
|
||||
@@ -1271,41 +1188,6 @@ public static class ErrorHelpers
|
||||
return len;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void ComputeAbsoluteErrorsSimd(
|
||||
ReadOnlySpan<double> actual,
|
||||
ReadOnlySpan<double> predicted,
|
||||
Span<double> output)
|
||||
{
|
||||
int len = actual.Length;
|
||||
int vectorSize = Vector256<double>.Count;
|
||||
int vectorEnd = len - (len % vectorSize);
|
||||
|
||||
// Create mask for absolute value (clear sign bit)
|
||||
Vector256<double> absMask = Vector256.Create(~(1L << 63)).AsDouble();
|
||||
|
||||
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)));
|
||||
|
||||
// error = actual - predicted
|
||||
Vector256<double> errorVec = Avx.Subtract(actVec, predVec);
|
||||
|
||||
// absError = |error| (clear sign bit)
|
||||
Vector256<double> absErrorVec = Avx.And(errorVec, absMask);
|
||||
|
||||
absErrorVec.StoreUnsafe(ref MemoryMarshal.GetReference(output.Slice(i)));
|
||||
}
|
||||
|
||||
// Handle remainder with scalar
|
||||
for (; i < len; i++)
|
||||
{
|
||||
output[i] = Math.Abs(actual[i] - predicted[i]);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void ComputeAbsoluteErrorsScalar(
|
||||
ReadOnlySpan<double> actual,
|
||||
@@ -1345,39 +1227,6 @@ public static class ErrorHelpers
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void ComputeSquaredErrorsSimd(
|
||||
ReadOnlySpan<double> actual,
|
||||
ReadOnlySpan<double> predicted,
|
||||
Span<double> output)
|
||||
{
|
||||
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)));
|
||||
|
||||
// error = actual - predicted
|
||||
Vector256<double> errorVec = Avx.Subtract(actVec, predVec);
|
||||
|
||||
// sqError = error * error
|
||||
Vector256<double> sqErrorVec = Avx.Multiply(errorVec, errorVec);
|
||||
|
||||
sqErrorVec.StoreUnsafe(ref MemoryMarshal.GetReference(output.Slice(i)));
|
||||
}
|
||||
|
||||
// Handle remainder with scalar
|
||||
for (; i < len; i++)
|
||||
{
|
||||
double diff = actual[i] - predicted[i];
|
||||
output[i] = diff * diff;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void ComputeSquaredErrorsScalar(
|
||||
ReadOnlySpan<double> actual,
|
||||
|
||||
@@ -871,4 +871,143 @@ public class TValueTests
|
||||
Assert.False(result);
|
||||
Assert.Equal(0, charsWritten);
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
// COVERAGE TESTS: TryFormat boundary branches (lines 85–144)
|
||||
//
|
||||
// Buffer layout: [yyyy-MM-dd HH:mm:ss, VALUE]
|
||||
// pos 0: '[' (1 char)
|
||||
// pos 1–19: datetime (19 chars)
|
||||
// pos 20: ',' (1 char)
|
||||
// pos 21: ' ' (1 char)
|
||||
// pos 22+: value (variable)
|
||||
// final: ']' (1 char)
|
||||
//
|
||||
// The initial guard rejects buffers < 24. These tests use buffers
|
||||
// that pass the guard but are too small for specific value formats.
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void TryFormat_Buffer24_NaN_ReturnsFalse_NaNNeedsThreeChars()
|
||||
{
|
||||
// Buffer 24 passes initial guard (>= 24).
|
||||
// After '[' + datetime + ', ' → pos = 22.
|
||||
// NaN needs 3 chars: pos + 3 = 25 > 24 → return false (line 123–124).
|
||||
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
var tValue = new TValue(dt.Ticks, double.NaN);
|
||||
|
||||
Span<char> buffer = stackalloc char[24];
|
||||
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, null);
|
||||
|
||||
Assert.False(result);
|
||||
Assert.Equal(0, charsWritten);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryFormat_Buffer24_NormalValue_ReturnsFalse_ValueTryFormatFails()
|
||||
{
|
||||
// Buffer 24, normal value "42.00" needs 5 chars at pos 22 → 27 > 24.
|
||||
// Value.TryFormat gets a 2-char slice → fails (line 134–135).
|
||||
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
var tValue = new TValue(dt.Ticks, 42.0);
|
||||
|
||||
Span<char> buffer = stackalloc char[24];
|
||||
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, CultureInfo.InvariantCulture);
|
||||
|
||||
Assert.False(result);
|
||||
Assert.Equal(0, charsWritten);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryFormat_Buffer24_NegativeInfinity_ReturnsFalse_NoRoomForClosingBracket()
|
||||
{
|
||||
// Buffer 24, "-∞" needs 2 chars at pos 22 → pos becomes 24.
|
||||
// Closing ']' needs pos + 1 = 25 > 24 → return false (line 143–144).
|
||||
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
var tValue = new TValue(dt.Ticks, double.NegativeInfinity);
|
||||
|
||||
Span<char> buffer = stackalloc char[24];
|
||||
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, null);
|
||||
|
||||
Assert.False(result);
|
||||
Assert.Equal(0, charsWritten);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryFormat_Buffer25_NaN_ReturnsFalse_NoRoomForClosingBracket()
|
||||
{
|
||||
// Buffer 25, NaN needs 3 chars at pos 22 → pos becomes 25.
|
||||
// Closing ']' needs pos + 1 = 26 > 25 → return false (line 143–144).
|
||||
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
var tValue = new TValue(dt.Ticks, double.NaN);
|
||||
|
||||
Span<char> buffer = stackalloc char[25];
|
||||
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, null);
|
||||
|
||||
Assert.False(result);
|
||||
Assert.Equal(0, charsWritten);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryFormat_Buffer25_NormalValue_ReturnsFalse_ValueTryFormatStillFails()
|
||||
{
|
||||
// Buffer 25, "0.00" needs 4 chars at pos 22 → only 3 chars available.
|
||||
// Value.TryFormat gets 3-char slice → fails (line 134–135).
|
||||
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
var tValue = new TValue(dt.Ticks, 0.0);
|
||||
|
||||
Span<char> buffer = stackalloc char[25];
|
||||
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, CultureInfo.InvariantCulture);
|
||||
|
||||
Assert.False(result);
|
||||
Assert.Equal(0, charsWritten);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryFormat_Buffer27_NormalValue_ReturnsFalse_FitsValueButNoClosingBracket()
|
||||
{
|
||||
// Buffer 27, "0.00" (4 chars) at pos 22 → pos becomes 26.
|
||||
// Closing ']' needs pos + 1 = 27 ≤ 27 → succeeds.
|
||||
// Actually "0.00" = 4 chars, pos=22+4=26, 26+1=27 ≤ 27 → fits.
|
||||
// Use a value that needs more chars: 10000.00 = 8 chars → 22+8=30 > 27.
|
||||
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
var tValue = new TValue(dt.Ticks, 10000.0);
|
||||
|
||||
Span<char> buffer = stackalloc char[27];
|
||||
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, CultureInfo.InvariantCulture);
|
||||
|
||||
Assert.False(result);
|
||||
Assert.Equal(0, charsWritten);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryFormat_Buffer27_ZeroValue_Succeeds_ExactFit()
|
||||
{
|
||||
// Buffer 27: '[' + datetime(19) + ', '(2) + '0.00'(4) + ']'(1) = 27.
|
||||
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
var tValue = new TValue(dt.Ticks, 0.0);
|
||||
|
||||
Span<char> buffer = stackalloc char[27];
|
||||
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, CultureInfo.InvariantCulture);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.Equal(27, charsWritten);
|
||||
string formatted = new string(buffer.Slice(0, charsWritten));
|
||||
Assert.Equal("[2023-01-01 00:00:00, 0.00]", formatted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryFormat_Buffer26_ZeroValue_ReturnsFalse_ClosingBracketFails()
|
||||
{
|
||||
// Buffer 26: "0.00" (4 chars) at pos 22 → pos=26. Need 27 for ']'.
|
||||
// 26 + 1 = 27 > 26 → return false for closing bracket (line 143–144).
|
||||
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
var tValue = new TValue(dt.Ticks, 0.0);
|
||||
|
||||
Span<char> buffer = stackalloc char[26];
|
||||
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, CultureInfo.InvariantCulture);
|
||||
|
||||
Assert.False(result);
|
||||
Assert.Equal(0, charsWritten);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user