test: setup common stability and robustness properties tracking

This commit is contained in:
Miha Kralj
2026-02-27 12:50:05 -08:00
parent 4ab3a7fb53
commit 769a923a24
287 changed files with 1314 additions and 867 deletions
+12 -6
View File
@@ -19,15 +19,21 @@ public delegate void BiInputBatchDelegate(
int period);
/// <summary>
/// Abstract base class for bi-input indicators (indicators that require two inputs like error metrics).
/// Abstract base class for error-metric indicators that compare two input series (actual vs predicted).
/// Provides common infrastructure for RingBuffer-based sliding window calculations with O(1) updates.
/// </summary>
/// <remarks>
/// This base class eliminates code duplication across error indicators (MAE, MSE, RMSE, MAPE, etc.)
/// by providing:
/// - Common state management with bar correction (isNew semantics)
/// - RingBuffer-based sliding window with running sum
/// - Periodic resync for floating-point drift correction
/// This base class is designed specifically for error metrics (MAE, MSE, RMSE, MAPE, SMAPE, etc.)
/// where each bar contributes a single scalar error value to a running mean.
///
/// It is NOT intended for statistical bi-input indicators (Correlation, Cointegration) which
/// maintain multiple running sums (Σx, Σy, Σx², Σy², Σxy) and have different state-restoration
/// semantics — those indicators manage their own state directly.
///
/// Infrastructure provided:
/// - _p_state / _buffer.Snapshot() / _buffer.Restore() for bar correction (isNew semantics)
/// - RingBuffer-based sliding window with a single running sum
/// - Periodic resync every 1000 updates for floating-point drift correction
/// - NaN/Infinity handling with last-valid-value substitution
/// - Template Method pattern: subclasses only implement ComputeError and optionally PostProcess
/// </remarks>
@@ -49,7 +49,7 @@ public sealed class AvgpriceValidationTests : IDisposable
var taOut = new double[open.Length];
var retCode = Functions.AvgPrice(open.AsSpan(), high.AsSpan(), low.AsSpan(), close.AsSpan(),
0..^0, taOut, out var outRange);
Assert.Equal(Core.RetCode.Success, retCode);
Assert.Equal(TALib.Core.RetCode.Success, retCode);
var (offset, length) = outRange.GetOffsetAndLength(taOut.Length);
// QuanTAlib batch span
@@ -46,7 +46,7 @@ public sealed class MedpriceValidationTests : IDisposable
// TA-Lib MedPrice
var taOut = new double[high.Length];
var retCode = Functions.MedPrice(high.AsSpan(), low.AsSpan(), 0..^0, taOut, out var outRange);
Assert.Equal(Core.RetCode.Success, retCode);
Assert.Equal(TALib.Core.RetCode.Success, retCode);
var (offset, length) = outRange.GetOffsetAndLength(taOut.Length);
// QuanTAlib batch via TBarSeries
@@ -51,7 +51,7 @@ public sealed class MidpointValidationTests : IDisposable
// Calculate TA-Lib MIDPOINT
var retCode = TALib.Functions.MidPoint<double>(tData, 0..^0, output, out var outRange, period);
Assert.Equal(Core.RetCode.Success, retCode);
Assert.Equal(TALib.Core.RetCode.Success, retCode);
int lookback = TALib.Functions.MidPointLookback(period);
@@ -81,7 +81,7 @@ public sealed class MidpointValidationTests : IDisposable
// Calculate TA-Lib MIDPOINT
var retCode = TALib.Functions.MidPoint<double>(tData, 0..^0, output, out var outRange, period);
Assert.Equal(Core.RetCode.Success, retCode);
Assert.Equal(TALib.Core.RetCode.Success, retCode);
int lookback = TALib.Functions.MidPointLookback(period);
@@ -107,7 +107,7 @@ public sealed class MidpointValidationTests : IDisposable
// Calculate TA-Lib MIDPOINT
var retCode = TALib.Functions.MidPoint<double>(sourceData, 0..^0, talibOutput, out var outRange, period);
Assert.Equal(Core.RetCode.Success, retCode);
Assert.Equal(TALib.Core.RetCode.Success, retCode);
int lookback = TALib.Functions.MidPointLookback(period);
@@ -47,7 +47,7 @@ public sealed class MidpriceValidationTests : IDisposable
// TA-Lib MidPrice
var taOut = new double[high.Length];
var retCode = Functions.MidPrice(high.AsSpan(), low.AsSpan(), 0..^0, taOut, out var outRange, period);
Assert.Equal(Core.RetCode.Success, retCode);
Assert.Equal(TALib.Core.RetCode.Success, retCode);
var (offset, length) = outRange.GetOffsetAndLength(taOut.Length);
// QuanTAlib batch span
@@ -76,7 +76,7 @@ public sealed class MidpriceValidationTests : IDisposable
var taOut = new double[high.Length];
var retCode = Functions.MidPrice(high.AsSpan(), low.AsSpan(), 0..^0, taOut, out var outRange, period);
Assert.Equal(Core.RetCode.Success, retCode);
Assert.Equal(TALib.Core.RetCode.Success, retCode);
var (offset, length) = outRange.GetOffsetAndLength(taOut.Length);
var qlOut = new double[high.Length];
+33 -7
View File
@@ -91,25 +91,51 @@ public sealed class RingBuffer : IEnumerable<double>
}
/// <summary>
/// Recalculates the sum by iterating over all elements.
/// Recalculates the sum by iterating over all elements using SIMD acceleration.
/// Useful for correcting floating-point drift after many updates.
/// Uses GetSequencedSpans to avoid allocation when buffer wraps.
/// </summary>
public double RecalculateSum()
{
double sum = 0;
GetSequencedSpans(out var first, out var second);
_sum = SumSpanSimd(first) + SumSpanSimd(second);
return _sum;
}
for (int i = 0; i < first.Length; i++)
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static double SumSpanSimd(ReadOnlySpan<double> span)
{
if (span.IsEmpty)
{
sum += first[i];
return 0.0;
}
for (int i = 0; i < second.Length; i++)
int vectorSize = Vector<double>.Count;
var acc = Vector<double>.Zero;
int i = 0;
if (span.Length >= vectorSize)
{
sum += second[i];
ref double spanRef = ref MemoryMarshal.GetReference(span);
for (; i <= span.Length - vectorSize; i += vectorSize)
{
acc += Unsafe.As<double, Vector<double>>(ref Unsafe.Add(ref spanRef, i));
}
}
// Horizontal sum of SIMD accumulator
double sum = 0.0;
for (int j = 0; j < vectorSize; j++)
{
sum += acc[j];
}
// Scalar tail
for (; i < span.Length; i++)
{
sum += span[i];
}
_sum = sum;
return sum;
}
@@ -0,0 +1,81 @@
using System;
using System.Linq;
using Xunit;
using QuanTAlib;
namespace QuanTAlib.Tests.Core;
/// <summary>
/// Tests verifying the structural stability of indicators across the repository.
/// </summary>
public class IndicatorPropertiesTests
{
[Fact]
public void Sma_ShouldNotProduceNaN_WithValidInputs()
{
var sma = new Sma(period: 10);
var random = new Random(42);
for (int i = 0; i < 100; i++)
{
double price = (random.Next(1, 1000000) / 10.0);
sma.Update(new TValue(DateTime.Today.AddDays(i), price));
// Only check for NaN after warmup
if (i >= sma.WarmupPeriod && double.IsNaN(sma.Last.Value))
{
Assert.Fail($"Produced NaN at index {i}");
}
}
}
[Fact]
public void Ema_ShouldNotProduceNaN_WithValidInputs()
{
var ema = new Ema(period: 10);
var random = new Random(42);
for (int i = 0; i < 100; i++)
{
double price = (random.Next(1, 1000000) / 10.0);
ema.Update(new TValue(DateTime.Today.AddDays(i), price));
// Only check for NaN after warmup
if (i >= ema.WarmupPeriod && double.IsNaN(ema.Last.Value))
{
Assert.Fail($"Produced NaN at index {i}");
}
}
}
[Fact]
public void Indicator_ShouldRecoverFromNaN_WhenReset()
{
var ema = new Ema(period: 10);
// Feed valid value
ema.Update(new TValue(DateTime.Today.AddDays(1), 100));
// Feed NaN, which should corrupt state
ema.Update(new TValue(DateTime.Today.AddDays(2), double.NaN));
// Let's actually ensure it is corrupted depending on implementation
// Some robust implementations might discard NaN internally, so we don't assert it strictly
// We just ensure it recovers properly.
// Reset should clear the corrupted state
ema.Reset();
// Feed valid value again
ema.Update(new TValue(DateTime.Today.AddDays(3), 100));
// Wait for Warmup
for (int i = 4; i < 3 + ema.WarmupPeriod; i++) {
ema.Update(new TValue(DateTime.Today.AddDays(i), 100));
}
// Verify recovery after warmup
Assert.False(double.IsNaN(ema.Last.Value));
Assert.Equal(100, Math.Round(ema.Last.Value, 5));
}
}
@@ -48,7 +48,7 @@ public sealed class TyppriceValidationTests : IDisposable
var taOut = new double[high.Length];
var retCode = Functions.TypPrice(high.AsSpan(), low.AsSpan(), close.AsSpan(),
0..^0, taOut, out var outRange);
Assert.Equal(Core.RetCode.Success, retCode);
Assert.Equal(TALib.Core.RetCode.Success, retCode);
var (offset, length) = outRange.GetOffsetAndLength(taOut.Length);
// QuanTAlib batch span
@@ -48,7 +48,7 @@ public sealed class WclpriceValidationTests : IDisposable
var taOut = new double[high.Length];
var retCode = Functions.WclPrice(high.AsSpan(), low.AsSpan(), close.AsSpan(),
0..^0, taOut, out var outRange);
Assert.Equal(Core.RetCode.Success, retCode);
Assert.Equal(TALib.Core.RetCode.Success, retCode);
var (offset, length) = outRange.GetOffsetAndLength(taOut.Length);
// QuanTAlib batch span