using TALib; using Xunit; using Xunit.Abstractions; namespace QuanTAlib.Tests; /// /// Validation tests for ROCP (Rate of Change Percentage) against external libraries. /// ROCP = 100 × (Price - Price[N]) / Price[N] /// /// Note: TALib's RocP returns a decimal fraction (0.05 for 5%), while QuanTAlib returns /// a percentage (5.0 for 5%). Tests account for this scaling difference. /// Tulip does not have a direct ROCP indicator. /// public sealed class RocpValidationTests(ITestOutputHelper output) : IDisposable { private readonly ValidationTestData _testData = new(); private readonly ITestOutputHelper _output = output; private bool _disposed; public void Dispose() { Dispose(disposing: true); } private void Dispose(bool disposing) { if (_disposed) { return; } _disposed = true; if (disposing) { _testData?.Dispose(); } } private const int TestPeriod = 10; #region TALib Validation [Fact] public void Rocp_MatchesTalib_Batch() { double[] tData = _testData.RawData.ToArray(); // QuanTAlib ROCP (batch TSeries) var rocp = new Rocp(TestPeriod); var qResult = rocp.Update(_testData.Data); // TALib RocP (returns decimal fraction) double[] tOutput = new double[tData.Length]; var retCode = TALib.Functions.RocP(tData, 0..^0, tOutput, out var outRange, TestPeriod); Assert.Equal(TALib.Core.RetCode.Success, retCode); int lookback = TALib.Functions.RocPLookback(TestPeriod); // Compare: TALib returns decimal, QuanTAlib returns percentage → multiply TALib by 100 int count = qResult.Count; int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount); var (offset, length) = outRange.GetOffsetAndLength(tOutput.Length); for (int i = start; i < count; i++) { if (i < lookback) { continue; } int tIndex = i - offset; if (tIndex < 0 || tIndex >= length) { continue; } double talibScaled = tOutput[tIndex] * 100.0; Assert.True( Math.Abs(qResult[i].Value - talibScaled) <= ValidationHelper.TalibTolerance, $"Mismatch at index {i}: QuanTAlib={qResult[i].Value:G17}, TALib(×100)={talibScaled:G17}"); } _output.WriteLine("ROCP Batch validated successfully against TALib"); } [Fact] public void Rocp_MatchesTalib_Span() { double[] tData = _testData.RawData.ToArray(); // QuanTAlib ROCP (Span) double[] qOutput = new double[tData.Length]; Rocp.Batch(tData.AsSpan(), qOutput.AsSpan(), TestPeriod); // TALib RocP double[] tOutput = new double[tData.Length]; var retCode = TALib.Functions.RocP(tData, 0..^0, tOutput, out var outRange, TestPeriod); Assert.Equal(TALib.Core.RetCode.Success, retCode); int lookback = TALib.Functions.RocPLookback(TestPeriod); int count = qOutput.Length; int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount); var (offset, length) = outRange.GetOffsetAndLength(tOutput.Length); for (int i = start; i < count; i++) { if (i < lookback) { continue; } int tIndex = i - offset; if (tIndex < 0 || tIndex >= length) { continue; } double talibScaled = tOutput[tIndex] * 100.0; Assert.True( Math.Abs(qOutput[i] - talibScaled) <= ValidationHelper.TalibTolerance, $"Mismatch at index {i}: QuanTAlib={qOutput[i]:G17}, TALib(×100)={talibScaled:G17}"); } _output.WriteLine("ROCP Span validated successfully against TALib"); } [Fact] public void Rocp_MatchesTalib_Streaming() { double[] tData = _testData.RawData.ToArray(); // QuanTAlib ROCP (streaming) var rocp = new Rocp(TestPeriod); var qResults = new List(); foreach (var item in _testData.Data) { qResults.Add(rocp.Update(item).Value); } // TALib RocP double[] tOutput = new double[tData.Length]; var retCode = TALib.Functions.RocP(tData, 0..^0, tOutput, out var outRange, TestPeriod); Assert.Equal(TALib.Core.RetCode.Success, retCode); int lookback = TALib.Functions.RocPLookback(TestPeriod); int count = qResults.Count; int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount); var (offset, length) = outRange.GetOffsetAndLength(tOutput.Length); for (int i = start; i < count; i++) { if (i < lookback) { continue; } int tIndex = i - offset; if (tIndex < 0 || tIndex >= length) { continue; } double talibScaled = tOutput[tIndex] * 100.0; Assert.True( Math.Abs(qResults[i] - talibScaled) <= ValidationHelper.TalibTolerance, $"Mismatch at index {i}: QuanTAlib={qResults[i]:G17}, TALib(×100)={talibScaled:G17}"); } _output.WriteLine("ROCP Streaming validated successfully against TALib"); } [Theory] [InlineData(5)] [InlineData(14)] [InlineData(20)] [InlineData(50)] public void Rocp_MatchesTalib_DifferentPeriods(int period) { double[] tData = _testData.RawData.ToArray(); var rocp = new Rocp(period); var qResult = rocp.Update(_testData.Data); double[] tOutput = new double[tData.Length]; var retCode = TALib.Functions.RocP(tData, 0..^0, tOutput, out var outRange, period); Assert.Equal(TALib.Core.RetCode.Success, retCode); int lookback = TALib.Functions.RocPLookback(period); var (offset, length) = outRange.GetOffsetAndLength(tOutput.Length); int count = qResult.Count; int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount); for (int i = start; i < count; i++) { if (i < lookback) { continue; } int tIndex = i - offset; if (tIndex < 0 || tIndex >= length) { continue; } double talibScaled = tOutput[tIndex] * 100.0; Assert.True( Math.Abs(qResult[i].Value - talibScaled) <= ValidationHelper.TalibTolerance, $"Period {period}, index {i}: QuanTAlib={qResult[i].Value:G17}, TALib(×100)={talibScaled:G17}"); } _output.WriteLine($"ROCP period={period} validated against TALib"); } #endregion #region Mathematical Validation [Fact] public void Rocp_ManualCalculation_MatchesExpected() { var rocp = new Rocp(3); var time = DateTime.UtcNow; var values = new double[] { 100, 105, 110, 115, 120, 125 }; for (int i = 0; i < values.Length; i++) { var result = rocp.Update(new TValue(time.AddSeconds(i), values[i]), true); if (i >= 3) { double expected = 100.0 * (values[i] - values[i - 3]) / values[i - 3]; Assert.Equal(expected, result.Value, 10); } else { Assert.Equal(0.0, result.Value, 10); } } } [Fact] public void Batch_MatchesStreaming_IdenticalResults() { var source = _testData.Data; // Streaming var streamingRocp = new Rocp(TestPeriod); var streamingResults = new List(); for (int i = 0; i < source.Count; i++) { streamingResults.Add(streamingRocp.Update(source[i]).Value); } // Batch var batchRocp = new Rocp(TestPeriod); var batchResult = batchRocp.Update(source); int count = source.Count; int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount); for (int i = start; i < count; i++) { Assert.Equal(batchResult[i].Value, streamingResults[i], ValidationHelper.DefaultTolerance); } _output.WriteLine("ROCP Batch vs Streaming consistency validated"); } #endregion }