namespace QuanTAlib.Tests; /// /// Validation tests for the ADF indicator — verifying mathematical properties /// and cross-checking against known statistical behaviors. /// public class AdfValidationTests { // ═══════════════════════════════════════════════════════════════ // 1. P-Value Bounds // ═══════════════════════════════════════════════════════════════ [Fact] public void PValue_AlwaysBetweenZeroAndOne() { var seeds = new[] { 1, 42, 123, 999, 31415 }; foreach (int seed in seeds) { var a = new Adf(30); var gbm = new GBM(startPrice: 100, mu: 0.0, sigma: 0.3, seed: seed); for (int i = 0; i < 100; i++) { var bar = gbm.Next(isNew: true); var result = a.Update(new TValue(bar.Time, bar.Close)); Assert.InRange(result.Value, 0.0, 1.0); } } } // ═══════════════════════════════════════════════════════════════ // 2. Known Stationary Process // ═══════════════════════════════════════════════════════════════ [Fact] public void AR1_WithStrongMeanReversion_DetectsStationarity() { // AR(1): y_t = 0.3 * y_{t-1} + ε_t (|φ| < 1 → stationary) var a = new Adf(50, 1, Adf.AdfRegression.Constant); var rng = new Random(42); double y = 0; var now = DateTime.UtcNow; for (int i = 0; i < 500; i++) { y = 0.3 * y + rng.NextDouble() * 2 - 1; a.Update(new TValue(now.AddMinutes(i), 100 + y)); } // Strong mean-reversion — p should be very low Assert.True(a.PValue < 0.05, $"AR(1) φ=0.3 should be detected as stationary, p={a.PValue}"); } [Fact] public void WhiteNoise_IsStationary() { // Pure white noise is strongly stationary — use explicit lag=1 to avoid // auto-lag overfitting on small windows, and zero-centered noise for clean signal var a = new Adf(50, 1, Adf.AdfRegression.Constant); var rng = new Random(42); var now = DateTime.UtcNow; for (int i = 0; i < 500; i++) { double noise = rng.NextDouble() * 10 - 5; // zero-centered white noise a.Update(new TValue(now.AddMinutes(i), noise)); } Assert.True(a.PValue < 0.10, $"White noise should be stationary, p={a.PValue}"); } // ═══════════════════════════════════════════════════════════════ // 3. Known Non-Stationary Process // ═══════════════════════════════════════════════════════════════ [Fact] public void PureRandomWalk_FailsToRejectUnitRoot() { // y_t = y_{t-1} + ε_t (unit root) var a = new Adf(50, 1, Adf.AdfRegression.Constant); var rng = new Random(789); double y = 100; var now = DateTime.UtcNow; for (int i = 0; i < 500; i++) { y += rng.NextDouble() * 2 - 1; a.Update(new TValue(now.AddMinutes(i), y)); } Assert.True(a.PValue > 0.05, $"Random walk should not reject unit root, p={a.PValue}"); } [Fact] public void LinearTrend_WithNoConstantModel_AppearsNonStationary() { // Pure linear trend y_t = t var a = new Adf(50, 0, Adf.AdfRegression.NoConstant); var now = DateTime.UtcNow; for (int i = 0; i < 100; i++) { a.Update(new TValue(now.AddMinutes(i), 100.0 + i * 0.1)); } // Linear trend without constant/trend in model should appear non-stationary Assert.InRange(a.PValue, 0.0, 1.0); Assert.True(double.IsFinite(a.Statistic)); } // ═══════════════════════════════════════════════════════════════ // 4. MacKinnon P-Value Properties // ═══════════════════════════════════════════════════════════════ [Fact] public void VeryNegativeStatistic_GivesLowPValue() { // Feed data that will produce very negative t-stat (strongly stationary) var a = new Adf(30, 0, Adf.AdfRegression.Constant); var rng = new Random(42); var now = DateTime.UtcNow; // Oscillating series: y_t = -0.9 * y_{t-1} + noise → very negative γ double y = 0; for (int i = 0; i < 100; i++) { y = -0.9 * y + rng.NextDouble() * 0.1; a.Update(new TValue(now.AddMinutes(i), 50 + y)); } Assert.True(a.PValue < 0.01, $"Strong oscillation should give p < 0.01, got {a.PValue}"); } // ═══════════════════════════════════════════════════════════════ // 5. Consistency Across API Modes // ═══════════════════════════════════════════════════════════════ [Fact] public void BatchAndStreaming_ProduceConsistentResults() { int period = 30; var rng = new Random(42); double y = 100; var source = new TSeries(); for (int i = 0; i < 80; i++) { y += rng.NextDouble() * 2 - 1; source.Add(new TValue(DateTime.UtcNow.AddMinutes(i), y)); } // Batch via TSeries var batchResult = Adf.Batch(source, period); // Span batch double[] spanOutput = new double[source.Count]; Adf.Batch(source.Values, spanOutput.AsSpan(), period); // Both should be in valid range for (int i = 0; i < source.Count; i++) { Assert.InRange(batchResult.Values[i], 0.0, 1.0); Assert.InRange(spanOutput[i], 0.0, 1.0); } } // ═══════════════════════════════════════════════════════════════ // 6. Determinism // ═══════════════════════════════════════════════════════════════ [Fact] public void SameInput_ProducesSameOutput() { double[] data = { 100, 101, 99, 102, 98, 103, 97, 104, 96, 105, 94, 106, 93, 107, 92, 108, 91, 109, 90, 110, 89, 111, 88, 112, 87, 113, 86, 114, 85, 115 }; var a1 = new Adf(25); var a2 = new Adf(25); for (int i = 0; i < data.Length; i++) { var tv = new TValue(DateTime.UtcNow.AddMinutes(i), data[i]); a1.Update(tv); a2.Update(tv); } Assert.Equal(a1.PValue, a2.PValue); Assert.Equal(a1.Statistic, a2.Statistic); Assert.Equal(a1.LagsUsed, a2.LagsUsed); } // ═══════════════════════════════════════════════════════════════ // 7. Reset and Reprocess // ═══════════════════════════════════════════════════════════════ [Fact] public void ResetAndReprocess_GivesSameResult() { var a = new Adf(25); var rng = new Random(42); double y = 100; var data = new List(); for (int i = 0; i < 40; i++) { y += rng.NextDouble() * 2 - 1; data.Add(new TValue(DateTime.UtcNow.AddMinutes(i), y)); } // First pass foreach (var tv in data) { a.Update(tv); } double firstPValue = a.PValue; double firstStat = a.Statistic; // Reset and second pass a.Reset(); foreach (var tv in data) { a.Update(tv); } Assert.Equal(firstPValue, a.PValue); Assert.Equal(firstStat, a.Statistic); } // ═══════════════════════════════════════════════════════════════ // 8. Auto-Lag Selection // ═══════════════════════════════════════════════════════════════ [Fact] public void AutoLag_SelectsReasonableLag() { var a = new Adf(50, 0, Adf.AdfRegression.Constant); var rng = new Random(42); double y = 100; var now = DateTime.UtcNow; for (int i = 0; i < 100; i++) { y += rng.NextDouble() * 2 - 1; a.Update(new TValue(now.AddMinutes(i), y)); } // Auto-lag should select a small number of lags Assert.True(a.LagsUsed >= 0); Assert.True(a.LagsUsed <= 5, $"Auto-lag selected {a.LagsUsed} lags — seems excessive for 50-bar window"); } // ═══════════════════════════════════════════════════════════════ // 9. Edge Cases // ═══════════════════════════════════════════════════════════════ [Fact] public void MinimumPeriod_StillWorks() { var a = new Adf(20, 0, Adf.AdfRegression.Constant); var rng = new Random(42); double y = 100; var now = DateTime.UtcNow; for (int i = 0; i < 25; i++) { y += rng.NextDouble() * 2 - 1; a.Update(new TValue(now.AddMinutes(i), y)); } Assert.True(double.IsFinite(a.PValue)); Assert.InRange(a.PValue, 0.0, 1.0); } [Fact] public void FixedLagZero_NoAugmentation() { var a = new Adf(30, 1, Adf.AdfRegression.Constant); var rng = new Random(42); double y = 100; var now = DateTime.UtcNow; for (int i = 0; i < 50; i++) { y += rng.NextDouble() * 2 - 1; a.Update(new TValue(now.AddMinutes(i), y)); } // With explicit lag=1, should get finite result Assert.True(double.IsFinite(a.PValue)); Assert.Equal(1, a.LagsUsed); } }