using Xunit.Abstractions; namespace QuanTAlib.Tests; /// /// Validation tests for AFIRMA indicator. /// AFIRMA is a specialized FIR filter with windowed sinc coefficients. /// Since no external library implements this exact algorithm, validation /// focuses on internal consistency and mathematical properties. /// public sealed class AfirmaValidationTests : IDisposable { private readonly ValidationTestData _testData; private readonly ITestOutputHelper _output; private bool _disposed; public AfirmaValidationTests(ITestOutputHelper output) { _output = output; _testData = new ValidationTestData(); } public void Dispose() { Dispose(true); } private void Dispose(bool disposing) { if (_disposed) { return; } _disposed = true; if (disposing) { _testData?.Dispose(); } } [Fact] public void Validate_InternalConsistency_Batch() { int[] periods = { 5, 10, 20, 50 }; foreach (var period in periods) { // Calculate QuanTAlib AFIRMA (batch TSeries) var afirma = new Afirma(period); var qResult = afirma.Update(_testData.Data); // Verify all results are finite foreach (var val in qResult) { Assert.True(double.IsFinite(val.Value), $"AFIRMA({period}) produced non-finite value"); } // Verify count matches input Assert.Equal(_testData.Data.Count, qResult.Count); } _output.WriteLine("AFIRMA Batch(TSeries) internal consistency validated"); } [Fact] public void Validate_InternalConsistency_Streaming() { int[] periods = { 5, 10, 20, 50 }; foreach (var period in periods) { // Calculate QuanTAlib AFIRMA (streaming) var afirma = new Afirma(period); var qResults = new List(); foreach (var item in _testData.Data) { qResults.Add(afirma.Update(item).Value); } // Verify all results are finite foreach (var val in qResults) { Assert.True(double.IsFinite(val), $"AFIRMA({period}) streaming produced non-finite value"); } // Verify count matches input Assert.Equal(_testData.Data.Count, qResults.Count); } _output.WriteLine("AFIRMA Streaming internal consistency validated"); } [Fact] public void Validate_InternalConsistency_Span() { int[] periods = { 5, 10, 20, 50 }; // Prepare data for Span API double[] sourceData = _testData.RawData.ToArray(); foreach (var period in periods) { // Calculate QuanTAlib AFIRMA (Span API) double[] qOutput = new double[sourceData.Length]; Afirma.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period); // Verify all results are finite foreach (var val in qOutput) { Assert.True(double.IsFinite(val), $"AFIRMA({period}) span produced non-finite value"); } } _output.WriteLine("AFIRMA Span internal consistency validated"); } [Fact] public void Validate_BatchStreamingConsistency() { int[] periods = { 5, 10, 20 }; foreach (var period in periods) { // Batch calculation var afirmaBatch = new Afirma(period); var batchResult = afirmaBatch.Update(_testData.Data); // Streaming calculation var afirmaStream = new Afirma(period); var streamResults = new List(); foreach (var item in _testData.Data) { streamResults.Add(afirmaStream.Update(item).Value); } // Compare last 100 values int compareCount = Math.Min(100, batchResult.Count); for (int i = 0; i < compareCount; i++) { int idx = batchResult.Count - compareCount + i; Assert.Equal(batchResult[idx].Value, streamResults[idx], 1e-10); } } _output.WriteLine("AFIRMA Batch/Streaming consistency validated"); } [Fact] public void Validate_SpanBatchConsistency() { int[] periods = { 5, 10, 20 }; double[] sourceData = _testData.RawData.ToArray(); foreach (var period in periods) { // TSeries Batch var afirma = new Afirma(period); var tseriesResult = afirma.Update(_testData.Data); // Span Batch double[] spanOutput = new double[sourceData.Length]; Afirma.Batch(sourceData.AsSpan(), spanOutput.AsSpan(), period); // Compare for (int i = 0; i < sourceData.Length; i++) { Assert.Equal(tseriesResult[i].Value, spanOutput[i], 1e-10); } } _output.WriteLine("AFIRMA Span/Batch consistency validated"); } [Fact] public void Validate_WindowTypes_Consistency() { var windows = new[] { Afirma.WindowType.Rectangular, Afirma.WindowType.Hanning, Afirma.WindowType.Hamming, Afirma.WindowType.Blackman, Afirma.WindowType.BlackmanHarris }; const int period = 10; foreach (var window in windows) { // Batch var afirmaBatch = new Afirma(period, window); var batchResult = afirmaBatch.Update(_testData.Data); // Streaming var afirmaStream = new Afirma(period, window); foreach (var item in _testData.Data) { afirmaStream.Update(item); } // Compare last values Assert.Equal(batchResult.Last.Value, afirmaStream.Last.Value, 1e-10); _output.WriteLine($"Window {window}: Batch={batchResult.Last.Value:F6}, Stream={afirmaStream.Last.Value:F6}"); } _output.WriteLine("AFIRMA Window types consistency validated"); } [Fact] public void Validate_FlatInput_ReturnsConstant() { int period = 10; double constantValue = 100.0; // Create flat input var flatSeries = new TSeries(); for (int i = 0; i < 100; i++) { flatSeries.Add(DateTime.UtcNow.AddSeconds(i), constantValue); } var afirma = new Afirma(period); var result = afirma.Update(flatSeries); // After warmup, all values should equal the constant for (int i = period; i < result.Count; i++) { Assert.Equal(constantValue, result[i].Value, 1e-9); } _output.WriteLine($"AFIRMA flat input returns constant: {result.Last.Value:F9}"); } [Fact] public void Validate_Smoothing_ReducesVariance() { int period = 21; // Calculate variance of input var rawData = _testData.RawData.ToArray(); double inputMean = rawData.Average(); double inputVariance = rawData.Average(x => Math.Pow(x - inputMean, 2)); // Calculate AFIRMA var afirma = new Afirma(period); var result = afirma.Update(_testData.Data); // Calculate variance of output (after warmup) var outputValues = result.Skip(period).Select(v => v.Value).ToList(); double outputMean = outputValues.Average(); double outputVariance = outputValues.Average(x => Math.Pow(x - outputMean, 2)); // Output variance should be less than input variance (smoothing effect) Assert.True(outputVariance < inputVariance, $"AFIRMA should reduce variance. Input: {inputVariance:F4}, Output: {outputVariance:F4}"); _output.WriteLine($"AFIRMA smoothing effect: Input variance={inputVariance:F4}, Output variance={outputVariance:F4}"); } [Fact] public void Validate_LargerPeriod_MoreSmoothing() { // Calculate with different periods (which implies different tap counts) var afirma5 = new Afirma(5); var afirma11 = new Afirma(11); var afirma21 = new Afirma(21); var result5 = afirma5.Update(_testData.Data); var result11 = afirma11.Update(_testData.Data); var result21 = afirma21.Update(_testData.Data); // Calculate variance of each double GetVariance(TSeries series, int skip) { var values = series.Skip(skip).Select(v => v.Value).ToList(); double mean = values.Average(); return values.Average(x => Math.Pow(x - mean, 2)); } double var5 = GetVariance(result5, 5); double var11 = GetVariance(result11, 11); double var21 = GetVariance(result21, 21); // Larger period should generally produce smoother output (lower variance) // This is a statistical property, not guaranteed for all data _output.WriteLine($"Variance by period: 5={var5:F4}, 11={var11:F4}, 21={var21:F4}"); // At minimum, all should be finite Assert.True(double.IsFinite(var5)); Assert.True(double.IsFinite(var11)); Assert.True(double.IsFinite(var21)); } [Fact] public void Validate_DifferentWindows_DifferentCharacteristics() { int period = 10; var rectangularResult = Afirma.Batch(_testData.Data, period, Afirma.WindowType.Rectangular); var blackmanHarrisResult = Afirma.Batch(_testData.Data, period, Afirma.WindowType.BlackmanHarris); // Results should be different (different window characteristics) double rectLast = rectangularResult.Last.Value; double bhLast = blackmanHarrisResult.Last.Value; // They should generally not be exactly equal // (unless input happens to be perfectly constant) _output.WriteLine($"Rectangular: {rectLast:F6}, Blackman-Harris: {bhLast:F6}"); // Both should be finite and reasonable Assert.True(double.IsFinite(rectLast)); Assert.True(double.IsFinite(bhLast)); } [Fact] public void Afirma_LeastSquares_Streaming_Matches_Batch() { int[] periods = { 5, 10, 20 }; foreach (var period in periods) { // Batch calculation with leastSquares=true var afirmaBatch = new Afirma(period, leastSquares: true); var batchResult = afirmaBatch.Update(_testData.Data); // Streaming calculation with leastSquares=true var afirmaStream = new Afirma(period, leastSquares: true); var streamResults = new List(); foreach (var item in _testData.Data) { streamResults.Add(afirmaStream.Update(item).Value); } // Compare last 100 values int compareCount = Math.Min(100, batchResult.Count); for (int i = 0; i < compareCount; i++) { int idx = batchResult.Count - compareCount + i; Assert.Equal(batchResult[idx].Value, streamResults[idx], 1e-10); } } } [Fact] public void Afirma_Correction_Recomputes() { var ind = new Afirma(20); var t0 = DateTime.MinValue; // Build state well past warmup for (int i = 0; i < 50; i++) { ind.Update(new TValue(t0.AddSeconds(i), 100.0 + (i * 0.5))); } // Anchor bar var anchorTime = t0.AddSeconds(50); const double anchorValue = 125.0; ind.Update(new TValue(anchorTime, anchorValue), isNew: true); double anchorResult = ind.Last.Value; // Correction with dramatically different value — must yield different result ind.Update(new TValue(anchorTime, anchorValue * 10), isNew: false); Assert.NotEqual(anchorResult, ind.Last.Value); // Correction back to original — must exactly restore original result ind.Update(new TValue(anchorTime, anchorValue), isNew: false); Assert.Equal(anchorResult, ind.Last.Value, 1e-9); } }