Files
QuanTAlib/lib/feeds/gbm/ValidationHelper.cs
2026-01-25 16:01:45 -08:00

494 lines
16 KiB
C#

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;
/// <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>
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);
int count = qSeries.Count;
int start = Math.Max(0, count - skip);
for (int i = start; i < count; i++)
{
double qValue = qSeries[i].Value;
double? sValue = selector(sSeries[i]);
if (!sValue.HasValue)
{
continue;
}
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}");
}
}
/// <summary>
/// Verifies IReadOnlyList results against an external library's results.
/// </summary>
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);
int count = qResults.Count;
int start = Math.Max(0, count - skip);
for (int i = start; i < count; i++)
{
double qValue = qResults[i];
double? sValue = selector(sSeries[i]);
if (!sValue.HasValue)
{
continue;
}
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}");
}
}
/// <summary>
/// Verifies double array results against an external library's results.
/// </summary>
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);
int count = qOutput.Length;
int start = Math.Max(0, count - skip);
for (int i = start; i < count; i++)
{
double qValue = qOutput[i];
double? sValue = selector(sSeries[i]);
if (!sValue.HasValue)
{
continue;
}
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}");
}
}
/// <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>
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);
for (int i = start; i < count; i++)
{
double qValue = qSeries[i].Value;
if (i < lookback)
{
continue;
}
int tIndex = i - lookback;
if (tIndex >= tOutput.Length)
{
continue;
}
double tValue = tOutput[tIndex];
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 IReadOnlyList results against TA-Lib style output with lookback offset.
/// </summary>
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);
for (int i = start; i < count; i++)
{
double qValue = qResults[i];
if (i < lookback)
{
continue;
}
int tIndex = i - lookback;
if (tIndex >= tOutput.Length)
{
continue;
}
double tValue = tOutput[tIndex];
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 double array results against TA-Lib style output with lookback offset.
/// </summary>
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);
for (int i = start; i < count; i++)
{
double qValue = qOutput[i];
if (i < lookback)
{
continue;
}
int tIndex = i - lookback;
if (tIndex >= tOutput.Length)
{
continue;
}
double tValue = tOutput[tIndex];
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 TSeries results against TA-Lib style output with range and lookback.
/// </summary>
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);
var (offset, length) = outRange.GetOffsetAndLength(tOutput.Length);
for (int i = start; i < count; i++)
{
double qValue = qSeries[i].Value;
if (i < lookback)
{
continue;
}
int tIndex = i - offset;
if (tIndex < 0 || tIndex >= length)
{
continue;
}
double tValue = tOutput[tIndex];
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 IReadOnlyList results against TA-Lib style output with range and lookback.
/// </summary>
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);
var (offset, length) = outRange.GetOffsetAndLength(tOutput.Length);
for (int i = start; i < count; i++)
{
double qValue = qResults[i];
if (i < lookback)
{
continue;
}
int tIndex = i - offset;
if (tIndex < 0 || tIndex >= length)
{
continue;
}
double tValue = tOutput[tIndex];
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 double array results against TA-Lib style output with range and lookback.
/// </summary>
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);
var (offset, length) = outRange.GetOffsetAndLength(tOutput.Length);
for (int i = start; i < count; i++)
{
double qValue = qOutput[i];
if (i < lookback)
{
continue;
}
int tIndex = i - offset;
if (tIndex < 0 || tIndex >= length)
{
continue;
}
double tValue = tOutput[tIndex];
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 || Math.Abs(sValue.Value) < double.Epsilon)
{
continue;
}
double relDiff = Math.Abs((qSeries[i].Value - sValue.Value) / sValue.Value);
if (relDiff > maxDiff)
{
maxDiff = relDiff;
}
}
return maxDiff;
}
}