using Xunit.Abstractions; namespace QuanTAlib.Tests; /// /// Validation tests for USF (Ehlers Ultimate Smoother Filter). /// /// Note: USF was introduced by John Ehlers in April 2024. /// As a very recent indicator, it is not yet available in external validation libraries /// (Skender, TA-Lib, Tulip, OoplesFinance). These tests focus on internal consistency /// and mathematical property verification. /// public sealed class UsfValidationTests : IDisposable { private readonly ValidationTestData _testData; private readonly ITestOutputHelper _output; private bool _disposed; public UsfValidationTests(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(); } } /// /// Validates that batch, streaming, and span modes produce identical results. /// This is a critical self-consistency check for all indicators. /// [Fact] public void Validate_AllModes_ProduceSameResults() { int[] periods = { 5, 10, 20, 50, 100 }; foreach (var period in periods) { // 1. Batch Mode (TSeries) var usfBatch = new Usf(period); var batchResult = usfBatch.Update(_testData.Data); // 2. Streaming Mode var usfStreaming = new Usf(period); var streamingResults = new List(); foreach (var item in _testData.Data) { streamingResults.Add(usfStreaming.Update(item).Value); } // 3. Span Mode double[] sourceData = _testData.RawData.ToArray(); double[] spanOutput = new double[sourceData.Length]; Usf.Calculate(sourceData.AsSpan(), spanOutput.AsSpan(), period); // Compare batch vs streaming Assert.Equal(batchResult.Count, streamingResults.Count); for (int i = 0; i < batchResult.Count; i++) { Assert.Equal(batchResult[i].Value, streamingResults[i], 1e-10); } // Compare batch vs span Assert.Equal(batchResult.Count, spanOutput.Length); for (int i = 0; i < batchResult.Count; i++) { Assert.Equal(batchResult[i].Value, spanOutput[i], 1e-10); } } _output.WriteLine("USF all modes validated successfully (batch, streaming, span produce identical results)"); } /// /// Validates the mathematical properties of USF: /// - Smooth filter (reduces noise) /// - Zero-lag characteristics (tracks trend closely) /// - Converges to constant input /// [Fact] public void Validate_MathematicalProperties() { int period = 10; // Test 1: Constant input should produce constant output (after warmup) var usfConstant = new Usf(period); for (int i = 0; i < period * 3; i++) { usfConstant.Update(new TValue(DateTime.UtcNow, 100.0)); } Assert.Equal(100.0, usfConstant.Last.Value, 1e-6); // Test 2: Linear trend - USF should track closely (zero-lag property) var usfLinear = new Usf(period); for (int i = 0; i < period * 5; i++) { usfLinear.Update(new TValue(DateTime.UtcNow, 100.0 + i)); } // After warmup on a linear trend, USF should be close to the current value double expectedLinear = 100.0 + (period * 5 - 1); Assert.True(Math.Abs(usfLinear.Last.Value - expectedLinear) < period, $"USF should track linear trend closely. Expected ~{expectedLinear}, got {usfLinear.Last.Value}"); // Test 3: Smoother than raw input (variance reduction on differences) // Use first differences (returns) to measure noise reduction var usf = new Usf(period); var gbm = new GBM(startPrice: 100, mu: 0.0, sigma: 0.3, seed: 42); var rawValues = new List(); var smoothedValues = new List(); for (int i = 0; i < 2000; i++) { var bar = gbm.Next(); rawValues.Add(bar.Close); usf.Update(new TValue(bar.Time, bar.Close)); if (usf.IsHot) { smoothedValues.Add(usf.Last.Value); } } // Calculate variance of first differences (measures noise/roughness) var rawDiffs = CalculateFirstDifferences(rawValues.Skip(period).ToList()); var smoothedDiffs = CalculateFirstDifferences(smoothedValues); double rawDiffVariance = CalculateVariance(rawDiffs); double smoothedDiffVariance = CalculateVariance(smoothedDiffs); Assert.True(smoothedDiffVariance < rawDiffVariance, $"USF should reduce noise (diff variance). Raw diff variance: {rawDiffVariance}, Smoothed diff variance: {smoothedDiffVariance}"); _output.WriteLine($"USF mathematical properties validated. Noise reduction: {rawDiffVariance / smoothedDiffVariance:F2}x"); } /// /// Validates that USF coefficients are correctly computed based on Ehlers' formula. /// The formula is: /// arg = sqrt(2) * PI / period /// c2 = 2 * exp(-arg) * cos(arg) /// c3 = -exp(-2 * arg) /// c1 = (1 + c2 - c3) / 4 /// [Fact] public void Validate_CoefficientCalculation() { // Verify by checking output for known input sequences int period = 10; var usf = new Usf(period); // Initialize with known values usf.Update(new TValue(DateTime.UtcNow, 100)); usf.Update(new TValue(DateTime.UtcNow, 100)); usf.Update(new TValue(DateTime.UtcNow, 100)); usf.Update(new TValue(DateTime.UtcNow, 100)); // After 4 values (count >= 4), the filter formula is applied // For constant input of 100, output should converge to 100 for (int i = 0; i < 20; i++) { usf.Update(new TValue(DateTime.UtcNow, 100)); } Assert.Equal(100.0, usf.Last.Value, 1e-6); _output.WriteLine("USF coefficient calculation validated"); } /// /// Validates USF against different period values to ensure stability. /// [Fact] public void Validate_PeriodStability() { int[] periods = { 2, 5, 10, 20, 50, 100, 200 }; foreach (var period in periods) { var usf = new Usf(period); // Feed realistic data foreach (var item in _testData.Data) { var result = usf.Update(item); // All outputs should be finite Assert.True(double.IsFinite(result.Value), $"USF with period {period} produced non-finite value: {result.Value}"); } // Should be hot after sufficient data Assert.True(usf.IsHot, $"USF with period {period} should be hot after {_testData.Data.Count} bars"); } _output.WriteLine("USF period stability validated for periods: " + string.Join(", ", periods)); } private static double CalculateVariance(List values) { if (values.Count == 0) return 0; double mean = values.Average(); return values.Sum(v => (v - mean) * (v - mean)) / values.Count; } private static List CalculateFirstDifferences(List values) { var diffs = new List(); for (int i = 1; i < values.Count; i++) { diffs.Add(values[i] - values[i - 1]); } return diffs; } }