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; } }