Files
Miha Kralj e3bd07aa87 feat: add ADF (Augmented Dickey-Fuller) indicator
- Core implementation with Cholesky OLS, MacKinnon p-value, AIC lag selection
- Three regression models: NoConstant, Constant, ConstantAndTrend
- NormCdf via Abramowitz & Stegun 7.1.26 erf approximation
- Quantower adapter, Python bridge (NativeAOT export + ctypes + wrapper)
- 69 tests (41 unit + 12 validation + 14 Quantower + 2 consistency)
- Documentation with Schwert table, MacKinnon coefficients, PineScript ref
- All 19,095 tests pass, zero warnings
2026-03-15 17:56:54 -07:00

293 lines
11 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
namespace QuanTAlib.Tests;
/// <summary>
/// Validation tests for the ADF indicator — verifying mathematical properties
/// and cross-checking against known statistical behaviors.
/// </summary>
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<TValue>();
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);
}
}