mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-19 02:58:05 +00:00
test: setup common stability and robustness properties tracking
This commit is contained in:
@@ -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];
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user