using Xunit; namespace QuanTAlib.Tests; /// /// Validation tests for Normalize indicator. /// Since Normalize is a basic mathematical transformation, validation focuses on /// mathematical properties rather than external library comparison. /// public class NormalizeValidationTests { private readonly GBM _gbm = new(100, 0.05, 0.2, seed: 42); [Fact] public void Normalize_OutputBounds_AlwaysZeroToOne() { // Test across multiple periods and data sets int[] periods = { 5, 14, 50, 100 }; foreach (var period in periods) { var norm = new Normalize(period); var series = _gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); foreach (var bar in series) { var result = norm.Update(new TValue(bar.Time, bar.Close)); Assert.True(result.Value >= 0.0 && result.Value <= 1.0, $"Period {period}: output {result.Value} not in [0,1]"); } } } [Fact] public void Normalize_MaxInWindow_ReturnsOne() { var norm = new Normalize(5); // Create ascending sequence double[] values = { 10, 20, 30, 40, 50 }; foreach (var v in values) { norm.Update(new TValue(DateTime.UtcNow, v)); } // Max value (50) should normalize to 1.0 Assert.Equal(1.0, norm.Last.Value, 1e-10); } [Fact] public void Normalize_MinInWindow_ReturnsZero() { var norm = new Normalize(5); // Create descending sequence ending at min double[] values = { 50, 40, 30, 20, 10 }; foreach (var v in values) { norm.Update(new TValue(DateTime.UtcNow, v)); } // Min value (10) should normalize to 0.0 Assert.Equal(0.0, norm.Last.Value, 1e-10); } [Fact] public void Normalize_LinearMapping_Correct() { var norm = new Normalize(5); // Set up window with known range [0, 100] norm.Update(new TValue(DateTime.UtcNow, 0)); norm.Update(new TValue(DateTime.UtcNow, 100)); norm.Update(new TValue(DateTime.UtcNow, 50)); // Placeholder norm.Update(new TValue(DateTime.UtcNow, 50)); // Placeholder norm.Update(new TValue(DateTime.UtcNow, 50)); // Placeholder // Test various values - (value - 0) / (100 - 0) = value / 100 double[] testValues = { 0, 25, 50, 75, 100 }; double[] expected = { 0.0, 0.25, 0.5, 0.75, 1.0 }; for (int i = 0; i < testValues.Length; i++) { // Reset and refill to maintain window [0, 100, test, test, test] norm.Reset(); norm.Update(new TValue(DateTime.UtcNow, 0)); norm.Update(new TValue(DateTime.UtcNow, 100)); norm.Update(new TValue(DateTime.UtcNow, testValues[i])); norm.Update(new TValue(DateTime.UtcNow, testValues[i])); var result = norm.Update(new TValue(DateTime.UtcNow, testValues[i])); Assert.Equal(expected[i], result.Value, 1e-10); } } [Fact] public void Normalize_ConstantInput_ReturnsHalf() { var norm = new Normalize(10); // All same values for (int i = 0; i < 20; i++) { norm.Update(new TValue(DateTime.UtcNow, 42.0)); } // Flat range: should return 0.5 Assert.Equal(0.5, norm.Last.Value, 1e-10); } [Fact] public void Normalize_RollingWindow_AdaptsToNewRange() { var norm = new Normalize(3); // Initial window [10, 20, 30] - range 20 norm.Update(new TValue(DateTime.UtcNow, 10)); norm.Update(new TValue(DateTime.UtcNow, 20)); norm.Update(new TValue(DateTime.UtcNow, 30)); // Value 25 in range [10, 30]: (25-10)/(30-10) = 0.75 var result1 = norm.Update(new TValue(DateTime.UtcNow, 25)); // Window is now [20, 30, 25], range [20, 30] // (25-20)/(30-20) = 0.5 Assert.Equal(0.5, result1.Value, 1e-10); } [Fact] public void Normalize_NegativeValues_WorksCorrectly() { var norm = new Normalize(5); // Range from -50 to +50 norm.Update(new TValue(DateTime.UtcNow, -50)); norm.Update(new TValue(DateTime.UtcNow, -25)); norm.Update(new TValue(DateTime.UtcNow, 0)); norm.Update(new TValue(DateTime.UtcNow, 25)); norm.Update(new TValue(DateTime.UtcNow, 50)); // max=50, value=50: (50-(-50))/(50-(-50)) = 100/100 = 1.0 Assert.Equal(1.0, norm.Last.Value, 1e-10); // Test zero: (0-(-50))/(50-(-50)) = 50/100 = 0.5 norm.Reset(); norm.Update(new TValue(DateTime.UtcNow, -50)); norm.Update(new TValue(DateTime.UtcNow, 50)); norm.Update(new TValue(DateTime.UtcNow, 0)); norm.Update(new TValue(DateTime.UtcNow, 0)); var zeroResult = norm.Update(new TValue(DateTime.UtcNow, 0)); Assert.Equal(0.5, zeroResult.Value, 1e-10); } [Fact] public void Normalize_SmallRange_HighPrecision() { var norm = new Normalize(5); // Very small range double baseVal = 100.0; double epsilon = 1e-8; norm.Update(new TValue(DateTime.UtcNow, baseVal)); norm.Update(new TValue(DateTime.UtcNow, baseVal + epsilon)); norm.Update(new TValue(DateTime.UtcNow, baseVal + epsilon / 2)); norm.Update(new TValue(DateTime.UtcNow, baseVal + epsilon / 4)); var result = norm.Update(new TValue(DateTime.UtcNow, baseVal + epsilon * 0.75)); // Should be in valid range Assert.True(result.Value >= 0.0 && result.Value <= 1.0); } [Fact] public void Normalize_LargeRange_StillPrecise() { var norm = new Normalize(5); // Very large range norm.Update(new TValue(DateTime.UtcNow, -1e10)); norm.Update(new TValue(DateTime.UtcNow, 1e10)); norm.Update(new TValue(DateTime.UtcNow, 0)); norm.Update(new TValue(DateTime.UtcNow, 0)); var result = norm.Update(new TValue(DateTime.UtcNow, 0)); // 0 in range [-1e10, 1e10]: (0 - (-1e10)) / (2e10) = 0.5 Assert.Equal(0.5, result.Value, 1e-6); } [Fact] public void Normalize_StreamingVsBatch_Match() { var series = _gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); double[] values = series.Select(b => b.Close).ToArray(); // Streaming var streamNorm = new Normalize(14); var streamResults = new double[values.Length]; for (int i = 0; i < values.Length; i++) { streamResults[i] = streamNorm.Update(new TValue(DateTime.UtcNow, values[i])).Value; } // Batch double[] batchResults = new double[values.Length]; Normalize.Batch(values, batchResults, 14); // Compare all values for (int i = 0; i < values.Length; i++) { Assert.Equal(batchResults[i], streamResults[i], 1e-10); } } [Fact] public void Normalize_AllModes_Consistent() { var series = _gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); int period = 14; // Mode 1: Streaming via Update(TValue) var norm1 = new Normalize(period); var results1 = new List(); foreach (var bar in series) { results1.Add(norm1.Update(new TValue(bar.Time, bar.Close)).Value); } // Mode 2: Batch via Update(TSeries) var tseries = new TSeries(); foreach (var bar in series) { tseries.Add(new TValue(bar.Time, bar.Close), true); } var results2 = Normalize.Batch(tseries, period); // Mode 3: Static span Calculate double[] values = series.Select(b => b.Close).ToArray(); double[] results3 = new double[values.Length]; Normalize.Batch(values, results3, period); // Mode 4: Event-based chaining var source = new TSeries(); var norm4 = new Normalize(source, period); foreach (var bar in series) { source.Add(new TValue(bar.Time, bar.Close), true); } var results4 = norm4.Last.Value; // Compare all modes (use last 50 values for stability) for (int i = 50; i < 100; i++) { Assert.Equal(results1[i], results2[i].Value, 1e-10); Assert.Equal(results1[i], results3[i], 1e-10); } // Verify Mode 4 matches last value from other modes Assert.Equal(results1[^1], results4, 1e-10); } [Fact] public void Normalize_BarCorrection_WorksCorrectly() { var norm = new Normalize(5); // Build up buffer norm.Update(new TValue(DateTime.UtcNow, 0)); norm.Update(new TValue(DateTime.UtcNow, 100)); norm.Update(new TValue(DateTime.UtcNow, 50)); norm.Update(new TValue(DateTime.UtcNow, 50)); // New bar var first = norm.Update(new TValue(DateTime.UtcNow, 75), isNew: true); // Correction (same bar, different value) var corrected = norm.Update(new TValue(DateTime.UtcNow, 25), isNew: false); // Values should be different Assert.NotEqual(first.Value, corrected.Value); // Further correction should still work var corrected2 = norm.Update(new TValue(DateTime.UtcNow, 50), isNew: false); Assert.NotEqual(corrected.Value, corrected2.Value); } [Fact] public void Normalize_Period1_ReturnsHalf() { var norm = new Normalize(1); // With period 1, min = max = current value, so range = 0 var result = norm.Update(new TValue(DateTime.UtcNow, 42)); // Flat range returns 0.5 Assert.Equal(0.5, result.Value, 1e-10); } [Fact] public void Normalize_VeryLargePeriod_StillWorks() { var norm = new Normalize(1000); var series = _gbm.Fetch(1500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); foreach (var bar in series) { var result = norm.Update(new TValue(bar.Time, bar.Close)); Assert.True(double.IsFinite(result.Value)); Assert.True(result.Value >= 0.0 && result.Value <= 1.0); } Assert.True(norm.IsHot); } }