Refactor tests and implementations for various indicators

- Updated RsiIndicatorTests to ensure proper initialization and state checks.
- Added new tests for Rsx, Vel, and Adosc indicators to validate behavior under iterative corrections and edge cases (NaN, Infinity).
- Enhanced Bessel indicator tests and implementation with consistent formatting.
- Improved Ema and Pwma implementations by ensuring proper handling of values.
- Introduced mock classes for charting to facilitate testing without dependencies.
- Ensured all indicators produce consistent results across different modes of operation.
- Cleaned up code formatting and added missing commas for better readability.
This commit is contained in:
Miha Kralj
2025-12-28 21:07:37 -08:00
parent 52af7057bb
commit 3cc2726654
39 changed files with 7535 additions and 840 deletions
+274 -18
View File
@@ -1,19 +1,75 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Xunit;
namespace QuanTAlib.Tests;
/// <summary>
/// Provides validation utilities for comparing indicator results against external libraries.
/// Contains tolerance constants and verification methods for cross-library validation.
/// </summary>
public static class ValidationHelper
{
/// <summary>
/// Default tolerance for floating-point comparisons (1e-7).
/// Suitable for most indicator comparisons.
/// </summary>
public const double DefaultTolerance = 1e-7;
/// <summary>
/// Tolerance for Ooples Finance library comparisons (1e-7).
/// May need adjustment for specific indicators with different internal precision.
/// </summary>
public const double OoplesTolerance = 1e-7;
/// <summary>
/// Tolerance for Skender.Stock.Indicators library comparisons (1e-7).
/// Skender uses decimal internally, so some precision loss is expected.
/// </summary>
public const double SkenderTolerance = 1e-7;
/// <summary>
/// Tolerance for TA-Lib (TALib.NETCore) library comparisons (1e-7).
/// TA-Lib uses double precision throughout.
/// </summary>
public const double TalibTolerance = 1e-7;
/// <summary>
/// Tolerance for Tulip library comparisons (1e-7).
/// Note: Tulip may have 1-bar shifts due to different initialization strategies.
/// </summary>
public const double TulipTolerance = 1e-7;
/// <summary>
/// Relative tolerance for percentage-based comparisons (0.5%).
/// Use when absolute tolerance is not appropriate.
/// </summary>
public const double RelativeTolerance = 0.005;
public static void VerifyData<TResult>(TSeries qSeries, IReadOnlyList<TResult> sSeries, Func<TResult, double?> selector, int skip = 100, double tolerance = DefaultTolerance)
/// <summary>
/// Default number of bars to verify from the end of the series.
/// Using 100 bars ensures we're comparing converged values.
/// </summary>
public const int DefaultVerificationCount = 100;
/// <summary>
/// Verifies TSeries results against an external library's results.
/// Compares the last 'skip' values by default.
/// </summary>
/// <typeparam name="TResult">The type of results from the external library</typeparam>
/// <param name="qSeries">QuanTAlib TSeries results</param>
/// <param name="sSeries">External library results</param>
/// <param name="selector">Function to extract the comparable value from external results</param>
/// <param name="skip">Number of values to verify from the end (default: 100)</param>
/// <param name="tolerance">Tolerance for floating-point comparison</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void VerifyData<TResult>(
TSeries qSeries,
IReadOnlyList<TResult> sSeries,
Func<TResult, double?> selector,
int skip = DefaultVerificationCount,
double tolerance = DefaultTolerance)
{
Assert.Equal(qSeries.Count, sSeries.Count);
@@ -27,11 +83,22 @@ public static class ValidationHelper
if (!sValue.HasValue) continue;
Assert.Equal(sValue.Value, qValue, tolerance);
Assert.True(
Math.Abs(qValue - sValue.Value) <= tolerance,
$"Mismatch at index {i}: QuanTAlib={qValue:G17}, External={sValue.Value:G17}, Diff={Math.Abs(qValue - sValue.Value):G17}");
}
}
public static void VerifyData<TResult>(IReadOnlyList<double> qResults, IReadOnlyList<TResult> sSeries, Func<TResult, double?> selector, int skip = 100, double tolerance = DefaultTolerance)
/// <summary>
/// Verifies IReadOnlyList results against an external library's results.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void VerifyData<TResult>(
IReadOnlyList<double> qResults,
IReadOnlyList<TResult> sSeries,
Func<TResult, double?> selector,
int skip = DefaultVerificationCount,
double tolerance = DefaultTolerance)
{
Assert.Equal(qResults.Count, sSeries.Count);
@@ -45,11 +112,22 @@ public static class ValidationHelper
if (!sValue.HasValue) continue;
Assert.Equal(sValue.Value, qValue, tolerance);
Assert.True(
Math.Abs(qValue - sValue.Value) <= tolerance,
$"Mismatch at index {i}: QuanTAlib={qValue:G17}, External={sValue.Value:G17}, Diff={Math.Abs(qValue - sValue.Value):G17}");
}
}
public static void VerifyData<TResult>(double[] qOutput, IReadOnlyList<TResult> sSeries, Func<TResult, double?> selector, int skip = 100, double tolerance = DefaultTolerance)
/// <summary>
/// Verifies double array results against an external library's results.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void VerifyData<TResult>(
double[] qOutput,
IReadOnlyList<TResult> sSeries,
Func<TResult, double?> selector,
int skip = DefaultVerificationCount,
double tolerance = DefaultTolerance)
{
Assert.Equal(qOutput.Length, sSeries.Count);
@@ -63,11 +141,27 @@ public static class ValidationHelper
if (!sValue.HasValue) continue;
Assert.Equal(sValue.Value, qValue, tolerance);
Assert.True(
Math.Abs(qValue - sValue.Value) <= tolerance,
$"Mismatch at index {i}: QuanTAlib={qValue:G17}, External={sValue.Value:G17}, Diff={Math.Abs(qValue - sValue.Value):G17}");
}
}
public static void VerifyData(TSeries qSeries, double[] tOutput, int lookback, int skip = 100, double tolerance = DefaultTolerance)
/// <summary>
/// Verifies TSeries results against TA-Lib style output with lookback offset.
/// </summary>
/// <param name="qSeries">QuanTAlib TSeries results</param>
/// <param name="tOutput">TA-Lib output array</param>
/// <param name="lookback">TA-Lib lookback period (output is shifted by this amount)</param>
/// <param name="skip">Number of values to verify from the end</param>
/// <param name="tolerance">Tolerance for floating-point comparison</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void VerifyData(
TSeries qSeries,
double[] tOutput,
int lookback,
int skip = DefaultVerificationCount,
double tolerance = DefaultTolerance)
{
int count = qSeries.Count;
int start = Math.Max(0, count - skip);
@@ -83,11 +177,22 @@ public static class ValidationHelper
double tValue = tOutput[tIndex];
Assert.Equal(tValue, qValue, tolerance);
Assert.True(
Math.Abs(qValue - tValue) <= tolerance,
$"Mismatch at index {i} (TA-Lib index {tIndex}): QuanTAlib={qValue:G17}, TA-Lib={tValue:G17}, Diff={Math.Abs(qValue - tValue):G17}");
}
}
public static void VerifyData(IReadOnlyList<double> qResults, double[] tOutput, int lookback, int skip = 100, double tolerance = DefaultTolerance)
/// <summary>
/// Verifies IReadOnlyList results against TA-Lib style output with lookback offset.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void VerifyData(
IReadOnlyList<double> qResults,
double[] tOutput,
int lookback,
int skip = DefaultVerificationCount,
double tolerance = DefaultTolerance)
{
int count = qResults.Count;
int start = Math.Max(0, count - skip);
@@ -103,11 +208,22 @@ public static class ValidationHelper
double tValue = tOutput[tIndex];
Assert.Equal(tValue, qValue, tolerance);
Assert.True(
Math.Abs(qValue - tValue) <= tolerance,
$"Mismatch at index {i} (TA-Lib index {tIndex}): QuanTAlib={qValue:G17}, TA-Lib={tValue:G17}, Diff={Math.Abs(qValue - tValue):G17}");
}
}
public static void VerifyData(double[] qOutput, double[] tOutput, int lookback, int skip = 100, double tolerance = DefaultTolerance)
/// <summary>
/// Verifies double array results against TA-Lib style output with lookback offset.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void VerifyData(
double[] qOutput,
double[] tOutput,
int lookback,
int skip = DefaultVerificationCount,
double tolerance = DefaultTolerance)
{
int count = qOutput.Length;
int start = Math.Max(0, count - skip);
@@ -123,11 +239,23 @@ public static class ValidationHelper
double tValue = tOutput[tIndex];
Assert.Equal(tValue, qValue, tolerance);
Assert.True(
Math.Abs(qValue - tValue) <= tolerance,
$"Mismatch at index {i} (TA-Lib index {tIndex}): QuanTAlib={qValue:G17}, TA-Lib={tValue:G17}, Diff={Math.Abs(qValue - tValue):G17}");
}
}
public static void VerifyData(TSeries qSeries, double[] tOutput, Range outRange, int lookback, int skip = 100, double tolerance = DefaultTolerance)
/// <summary>
/// Verifies TSeries results against TA-Lib style output with range and lookback.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void VerifyData(
TSeries qSeries,
double[] tOutput,
Range outRange,
int lookback,
int skip = DefaultVerificationCount,
double tolerance = DefaultTolerance)
{
int count = qSeries.Count;
int start = Math.Max(0, count - skip);
@@ -144,11 +272,23 @@ public static class ValidationHelper
double tValue = tOutput[tIndex];
Assert.Equal(tValue, qValue, tolerance);
Assert.True(
Math.Abs(qValue - tValue) <= tolerance,
$"Mismatch at index {i} (TA-Lib index {tIndex}): QuanTAlib={qValue:G17}, TA-Lib={tValue:G17}, Diff={Math.Abs(qValue - tValue):G17}");
}
}
public static void VerifyData(IReadOnlyList<double> qResults, double[] tOutput, Range outRange, int lookback, int skip = 100, double tolerance = DefaultTolerance)
/// <summary>
/// Verifies IReadOnlyList results against TA-Lib style output with range and lookback.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void VerifyData(
IReadOnlyList<double> qResults,
double[] tOutput,
Range outRange,
int lookback,
int skip = DefaultVerificationCount,
double tolerance = DefaultTolerance)
{
int count = qResults.Count;
int start = Math.Max(0, count - skip);
@@ -165,11 +305,23 @@ public static class ValidationHelper
double tValue = tOutput[tIndex];
Assert.Equal(tValue, qValue, tolerance);
Assert.True(
Math.Abs(qValue - tValue) <= tolerance,
$"Mismatch at index {i} (TA-Lib index {tIndex}): QuanTAlib={qValue:G17}, TA-Lib={tValue:G17}, Diff={Math.Abs(qValue - tValue):G17}");
}
}
public static void VerifyData(double[] qOutput, double[] tOutput, Range outRange, int lookback, int skip = 100, double tolerance = DefaultTolerance)
/// <summary>
/// Verifies double array results against TA-Lib style output with range and lookback.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void VerifyData(
double[] qOutput,
double[] tOutput,
Range outRange,
int lookback,
int skip = DefaultVerificationCount,
double tolerance = DefaultTolerance)
{
int count = qOutput.Length;
int start = Math.Max(0, count - skip);
@@ -186,7 +338,111 @@ public static class ValidationHelper
double tValue = tOutput[tIndex];
Assert.Equal(tValue, qValue, tolerance);
Assert.True(
Math.Abs(qValue - tValue) <= tolerance,
$"Mismatch at index {i} (TA-Lib index {tIndex}): QuanTAlib={qValue:G17}, TA-Lib={tValue:G17}, Diff={Math.Abs(qValue - tValue):G17}");
}
}
/// <summary>
/// Verifies that all values in the series are finite (not NaN or Infinity).
/// </summary>
/// <param name="series">The series to verify</param>
/// <param name="startIndex">Starting index for verification (default: 0)</param>
public static void VerifyAllFinite(TSeries series, int startIndex = 0)
{
for (int i = startIndex; i < series.Count; i++)
{
Assert.True(
double.IsFinite(series[i].Value),
$"Non-finite value at index {i}: {series[i].Value}");
}
}
/// <summary>
/// Verifies that all values in the array are finite (not NaN or Infinity).
/// </summary>
/// <param name="values">The array to verify</param>
/// <param name="startIndex">Starting index for verification (default: 0)</param>
public static void VerifyAllFinite(double[] values, int startIndex = 0)
{
for (int i = startIndex; i < values.Length; i++)
{
Assert.True(
double.IsFinite(values[i]),
$"Non-finite value at index {i}: {values[i]}");
}
}
/// <summary>
/// Verifies that two series produce the same results (for consistency testing).
/// </summary>
/// <param name="series1">First series</param>
/// <param name="series2">Second series</param>
/// <param name="tolerance">Tolerance for floating-point comparison</param>
public static void VerifySeriesEqual(TSeries series1, TSeries series2, double tolerance = DefaultTolerance)
{
Assert.Equal(series1.Count, series2.Count);
for (int i = 0; i < series1.Count; i++)
{
Assert.True(
Math.Abs(series1[i].Value - series2[i].Value) <= tolerance,
$"Mismatch at index {i}: Series1={series1[i].Value:G17}, Series2={series2[i].Value:G17}");
}
}
/// <summary>
/// Calculates the maximum absolute difference between two series.
/// Useful for debugging tolerance issues.
/// </summary>
public static double MaxAbsoluteDifference<TResult>(
TSeries qSeries,
IReadOnlyList<TResult> sSeries,
Func<TResult, double?> selector)
{
if (qSeries.Count != sSeries.Count)
throw new ArgumentException("Series must have the same count", nameof(sSeries));
double maxDiff = 0;
for (int i = 0; i < qSeries.Count; i++)
{
double? sValue = selector(sSeries[i]);
if (!sValue.HasValue) continue;
double diff = Math.Abs(qSeries[i].Value - sValue.Value);
if (diff > maxDiff)
maxDiff = diff;
}
return maxDiff;
}
/// <summary>
/// Calculates the maximum relative difference between two series.
/// Useful for percentage-based tolerance testing.
/// </summary>
public static double MaxRelativeDifference<TResult>(
TSeries qSeries,
IReadOnlyList<TResult> sSeries,
Func<TResult, double?> selector)
{
if (qSeries.Count != sSeries.Count)
throw new ArgumentException("Series must have the same count", nameof(sSeries));
double maxDiff = 0;
for (int i = 0; i < qSeries.Count; i++)
{
double? sValue = selector(sSeries[i]);
if (!sValue.HasValue || sValue.Value == 0) continue;
double relDiff = Math.Abs((qSeries[i].Value - sValue.Value) / sValue.Value);
if (relDiff > maxDiff)
maxDiff = relDiff;
}
return maxDiff;
}
}