namespace QuanTAlib.Tests;
///
/// Provides validation utilities for comparing indicator results against external libraries.
/// Contains tolerance constants and verification methods for cross-library validation.
///
public static class ValidationHelper
{
///
/// Default tolerance for floating-point comparisons (1e-7).
/// Suitable for most indicator comparisons.
///
public const double DefaultTolerance = 1e-7;
///
/// Tolerance for Ooples Finance library comparisons (1e-7).
/// May need adjustment for specific indicators with different internal precision.
///
public const double OoplesTolerance = 1e-7;
///
/// Tolerance for Skender.Stock.Indicators library comparisons (1e-7).
/// Skender uses decimal internally, so some precision loss is expected.
///
public const double SkenderTolerance = 1e-7;
///
/// Tolerance for TA-Lib (TALib.NETCore) library comparisons (1e-7).
/// TA-Lib uses double precision throughout.
///
public const double TalibTolerance = 1e-7;
///
/// Tolerance for Tulip library comparisons (1e-7).
/// Note: Tulip may have 1-bar shifts due to different initialization strategies.
///
public const double TulipTolerance = 1e-7;
///
/// Relative tolerance for percentage-based comparisons (0.5%).
/// Use when absolute tolerance is not appropriate.
///
public const double RelativeTolerance = 0.005;
///
/// Default number of bars to verify from the end of the series.
/// Using 100 bars ensures we're comparing converged values.
///
public const int DefaultVerificationCount = 100;
///
/// Verifies TSeries results against an external library's results.
/// Compares the last 'skip' values by default.
///
/// The type of results from the external library
/// QuanTAlib TSeries results
/// External library results
/// Function to extract the comparable value from external results
/// Number of values to verify from the end (default: 100)
/// Tolerance for floating-point comparison
public static void VerifyData(
TSeries qSeries,
IReadOnlyList sSeries,
Func 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}");
}
}
///
/// Verifies IReadOnlyList results against an external library's results.
///
public static void VerifyData(
IReadOnlyList qResults,
IReadOnlyList sSeries,
Func 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}");
}
}
///
/// Verifies double array results against an external library's results.
///
public static void VerifyData(
double[] qOutput,
IReadOnlyList sSeries,
Func 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}");
}
}
///
/// Verifies TSeries results against TA-Lib style output with lookback offset.
///
/// QuanTAlib TSeries results
/// TA-Lib output array
/// TA-Lib lookback period (output is shifted by this amount)
/// Number of values to verify from the end
/// Tolerance for floating-point comparison
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}");
}
}
///
/// Verifies IReadOnlyList results against TA-Lib style output with lookback offset.
///
public static void VerifyData(
IReadOnlyList 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}");
}
}
///
/// Verifies double array results against TA-Lib style output with lookback offset.
///
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}");
}
}
///
/// Verifies TSeries results against TA-Lib style output with range and lookback.
///
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}");
}
}
///
/// Verifies IReadOnlyList results against TA-Lib style output with range and lookback.
///
public static void VerifyData(
IReadOnlyList 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}");
}
}
///
/// Verifies double array results against TA-Lib style output with range and lookback.
///
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}");
}
}
///
/// Verifies that all values in the series are finite (not NaN or Infinity).
///
/// The series to verify
/// Starting index for verification (default: 0)
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}");
}
}
///
/// Verifies that all values in the array are finite (not NaN or Infinity).
///
/// The array to verify
/// Starting index for verification (default: 0)
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]}");
}
}
///
/// Verifies that two series produce the same results (for consistency testing).
///
/// First series
/// Second series
/// Tolerance for floating-point comparison
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}");
}
}
///
/// Calculates the maximum absolute difference between two series.
/// Useful for debugging tolerance issues.
///
public static double MaxAbsoluteDifference(
TSeries qSeries,
IReadOnlyList sSeries,
Func 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;
}
///
/// Calculates the maximum relative difference between two series.
/// Useful for percentage-based tolerance testing.
///
public static double MaxRelativeDifference(
TSeries qSeries,
IReadOnlyList sSeries,
Func 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;
}
}