mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-25 13:58:04 +00:00
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
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class AdfIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void AdfIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new AdfIndicator();
|
||||
|
||||
Assert.Equal(50, indicator.Period);
|
||||
Assert.Equal(0, indicator.MaxLag);
|
||||
Assert.Equal(1, indicator.RegressionModel); // Constant
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Contains("ADF", indicator.Name, StringComparison.Ordinal);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdfIndicator_MinHistoryDepths_EqualsPeriod()
|
||||
{
|
||||
var indicator = new AdfIndicator { Period = 30 };
|
||||
|
||||
Assert.Equal(30, indicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(30, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdfIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new AdfIndicator { Period = 50, MaxLag = 2, RegressionModel = 1 };
|
||||
|
||||
Assert.Contains("ADF", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("50", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdfIndicator_ShortName_ShowsRegressionModel()
|
||||
{
|
||||
var nc = new AdfIndicator { RegressionModel = 0 };
|
||||
Assert.Contains("nc", nc.ShortName, StringComparison.Ordinal);
|
||||
|
||||
var c = new AdfIndicator { RegressionModel = 1 };
|
||||
Assert.Contains(",c)", c.ShortName, StringComparison.Ordinal);
|
||||
|
||||
var ct = new AdfIndicator { RegressionModel = 2 };
|
||||
Assert.Contains("ct", ct.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdfIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new AdfIndicator();
|
||||
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Adf.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdfIndicator_Initialize_CreatesLineSeries()
|
||||
{
|
||||
var indicator = new AdfIndicator { Period = 30 };
|
||||
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdfIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new AdfIndicator { Period = 20 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdfIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new AdfIndicator { Period = 20 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdfIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new AdfIndicator { Period = 20 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
double firstValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
double secondValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(firstValue));
|
||||
Assert.True(double.IsFinite(secondValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdfIndicator_MultipleUpdates_ProducesCorrectSequence()
|
||||
{
|
||||
var indicator = new AdfIndicator { Period = 20 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(
|
||||
now.AddMinutes(i), 100 + i, 105 + i, 95 + i, 102 + i);
|
||||
|
||||
var reason = i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar;
|
||||
indicator.ProcessUpdate(new UpdateArgs(reason));
|
||||
}
|
||||
|
||||
Assert.Equal(6, indicator.LinesSeries[0].Count);
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(i)));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdfIndicator_DifferentSourceTypes_Work()
|
||||
{
|
||||
var sourceTypes = new[] { SourceType.Close, SourceType.Open, SourceType.High,
|
||||
SourceType.Low, SourceType.HL2, SourceType.HLC3 };
|
||||
|
||||
foreach (var sourceType in sourceTypes)
|
||||
{
|
||||
var indicator = new AdfIndicator { Period = 20, Source = sourceType };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)),
|
||||
$"Failed for SourceType={sourceType}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdfIndicator_ShowColdValues_CanBeToggled()
|
||||
{
|
||||
var indicator = new AdfIndicator();
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdfIndicator_OutputInRange()
|
||||
{
|
||||
var indicator = new AdfIndicator { Period = 20 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(
|
||||
now.AddMinutes(i), 100 + i * 0.5, 105 + i * 0.5, 95 + i * 0.5, 102 + i * 0.5);
|
||||
|
||||
var reason = i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar;
|
||||
indicator.ProcessUpdate(new UpdateArgs(reason));
|
||||
}
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double val = indicator.LinesSeries[0].GetValue(i);
|
||||
Assert.InRange(val, 0.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdfIndicator_Description_IsSet()
|
||||
{
|
||||
var indicator = new AdfIndicator();
|
||||
Assert.False(string.IsNullOrEmpty(indicator.Description));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdfIndicator_DifferentPeriods_ProduceDifferentResults()
|
||||
{
|
||||
var indicator30 = new AdfIndicator { Period = 20 };
|
||||
var indicator50 = new AdfIndicator { Period = 30 };
|
||||
indicator30.Initialize();
|
||||
indicator50.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 40; i++)
|
||||
{
|
||||
indicator30.HistoricalData.AddBar(
|
||||
now.AddMinutes(i), 100 + i, 105 + i, 95 + i, 102 + i);
|
||||
indicator50.HistoricalData.AddBar(
|
||||
now.AddMinutes(i), 100 + i, 105 + i, 95 + i, 102 + i);
|
||||
|
||||
var reason = i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar;
|
||||
indicator30.ProcessUpdate(new UpdateArgs(reason));
|
||||
indicator50.ProcessUpdate(new UpdateArgs(reason));
|
||||
}
|
||||
|
||||
// After enough data, different periods should produce different results
|
||||
int lastIdx = 39;
|
||||
double val30 = indicator30.LinesSeries[0].GetValue(lastIdx);
|
||||
double val50 = indicator50.LinesSeries[0].GetValue(lastIdx);
|
||||
|
||||
Assert.True(double.IsFinite(val30));
|
||||
Assert.True(double.IsFinite(val50));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,517 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// A) Constructor Validation
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
public class AdfConstructorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_ThrowsOnPeriodLessThan20()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Adf(19));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Adf(10));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Adf(0));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Adf(-1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_AcceptsMinimumPeriod()
|
||||
{
|
||||
var a = new Adf(20);
|
||||
Assert.NotNull(a);
|
||||
Assert.Contains("ADF", a.Name, StringComparison.Ordinal);
|
||||
Assert.Contains("20", a.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SetsWarmupPeriod()
|
||||
{
|
||||
var a = new Adf(100);
|
||||
Assert.Equal(100, a.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ThrowsOnNegativeMaxLag()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Adf(50, -1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_AcceptsZeroMaxLag()
|
||||
{
|
||||
var a = new Adf(50, 0);
|
||||
Assert.NotNull(a);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_AcceptsExplicitMaxLag()
|
||||
{
|
||||
var a = new Adf(50, 3);
|
||||
Assert.Contains("3", a.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultRegression_IsConstant()
|
||||
{
|
||||
var a = new Adf(50);
|
||||
Assert.Contains("c", a.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_AllRegressionModels()
|
||||
{
|
||||
var nc = new Adf(50, 0, Adf.AdfRegression.NoConstant);
|
||||
Assert.Contains("nc", nc.Name, StringComparison.Ordinal);
|
||||
|
||||
var c = new Adf(50, 0, Adf.AdfRegression.Constant);
|
||||
Assert.Contains(",c)", c.Name, StringComparison.Ordinal);
|
||||
|
||||
var ct = new Adf(50, 0, Adf.AdfRegression.ConstantAndTrend);
|
||||
Assert.Contains("ct", ct.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_LargePeriod()
|
||||
{
|
||||
var a = new Adf(500);
|
||||
Assert.Equal("ADF(500,0,c)", a.Name);
|
||||
Assert.Equal(500, a.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ParamName_IsPeriod()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new Adf(5));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// B) Basic Calculation
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
public class AdfBasicTests
|
||||
{
|
||||
[Fact]
|
||||
public void Calc_ReturnsValue()
|
||||
{
|
||||
var a = new Adf(20);
|
||||
TValue result = a.Update(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.Equal(result.Value, a.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_FirstValue_ReturnsOne()
|
||||
{
|
||||
var a = new Adf(20);
|
||||
TValue result = a.Update(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.Equal(1.0, result.Value); // Not enough data → p=1.0
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_OutputIsFinite()
|
||||
{
|
||||
var a = new Adf(20);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
var result = a.Update(new TValue(bar.Time, bar.Close));
|
||||
Assert.True(double.IsFinite(result.Value), $"Result at index {i} is not finite: {result.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_OutputInRange_ZeroToOne()
|
||||
{
|
||||
var a = new Adf(30);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.0, sigma: 0.3, seed: 123);
|
||||
|
||||
for (int i = 0; i < 200; 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);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_PValueProperty_MatchesOutput()
|
||||
{
|
||||
var a = new Adf(30);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.0, sigma: 0.2, seed: 42);
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
var result = a.Update(new TValue(bar.Time, bar.Close));
|
||||
Assert.Equal(result.Value, a.PValue);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_StatisticProperty_IsFinite()
|
||||
{
|
||||
var a = new Adf(30);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.0, sigma: 0.2, seed: 42);
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
a.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
Assert.True(double.IsFinite(a.Statistic));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_LagsUsedProperty_IsNonNegative()
|
||||
{
|
||||
var a = new Adf(50);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.0, sigma: 0.2, seed: 42);
|
||||
|
||||
for (int i = 0; i < 60; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
a.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
Assert.True(a.LagsUsed >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// C) State & Bar Correction
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
public class AdfStateTests
|
||||
{
|
||||
[Fact]
|
||||
public void BarCorrection_IsNewFalse_DoesNotCrash()
|
||||
{
|
||||
var a = new Adf(20);
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
a.Update(new TValue(now, 100), isNew: true);
|
||||
a.Update(new TValue(now, 101), isNew: false);
|
||||
a.Update(new TValue(now, 102), isNew: false);
|
||||
|
||||
Assert.True(double.IsFinite(a.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var a = new Adf(20);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.0, sigma: 0.2, seed: 42);
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
a.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
Assert.NotEqual(default, a.Last);
|
||||
a.Reset();
|
||||
Assert.Equal(default, a.Last);
|
||||
Assert.Equal(1.0, a.PValue);
|
||||
Assert.Equal(0, a.LagsUsed);
|
||||
Assert.False(a.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BecomesTrue_AfterWarmup()
|
||||
{
|
||||
var a = new Adf(20);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.0, sigma: 0.2, seed: 42);
|
||||
|
||||
for (int i = 0; i < 19; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
a.Update(new TValue(bar.Time, bar.Close));
|
||||
Assert.False(a.IsHot);
|
||||
}
|
||||
|
||||
var lastBar = gbm.Next(isNew: true);
|
||||
a.Update(new TValue(lastBar.Time, lastBar.Close));
|
||||
// After period bars, should be or getting close to hot
|
||||
// IsHot requires _inputCount > _period
|
||||
lastBar = gbm.Next(isNew: true);
|
||||
a.Update(new TValue(lastBar.Time, lastBar.Close));
|
||||
Assert.True(a.IsHot);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// D) Robustness
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
public class AdfRobustnessTests
|
||||
{
|
||||
[Fact]
|
||||
public void NaN_InputIsHandled()
|
||||
{
|
||||
var a = new Adf(20);
|
||||
|
||||
a.Update(new TValue(DateTime.UtcNow, 100));
|
||||
a.Update(new TValue(DateTime.UtcNow.AddMinutes(1), double.NaN));
|
||||
a.Update(new TValue(DateTime.UtcNow.AddMinutes(2), 102));
|
||||
|
||||
Assert.True(double.IsFinite(a.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Infinity_InputIsHandled()
|
||||
{
|
||||
var a = new Adf(20);
|
||||
|
||||
a.Update(new TValue(DateTime.UtcNow, 100));
|
||||
a.Update(new TValue(DateTime.UtcNow.AddMinutes(1), double.PositiveInfinity));
|
||||
a.Update(new TValue(DateTime.UtcNow.AddMinutes(2), 102));
|
||||
|
||||
Assert.True(double.IsFinite(a.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstantInput_ReturnsUnitRoot()
|
||||
{
|
||||
var a = new Adf(25);
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
a.Update(new TValue(now.AddMinutes(i), 100.0));
|
||||
}
|
||||
|
||||
// Constant input has no variation → should return high p-value or handle gracefully
|
||||
Assert.True(double.IsFinite(a.PValue));
|
||||
Assert.InRange(a.PValue, 0.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// E) Consistency
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
public class AdfConsistencyTests
|
||||
{
|
||||
[Fact]
|
||||
public void BatchTSeries_MatchesStreaming()
|
||||
{
|
||||
int period = 30;
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.0, sigma: 0.2, seed: 42);
|
||||
var source = new TSeries();
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
source.Add(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batchResult = Adf.Batch(source, period);
|
||||
|
||||
// Streaming
|
||||
var streaming = new Adf(period);
|
||||
var streamResults = new List<double>();
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
var result = streaming.Update(source[i]);
|
||||
streamResults.Add(result.Value);
|
||||
}
|
||||
|
||||
// Final values should be close (not exact due to floating-point paths)
|
||||
Assert.Equal(batchResult.Count, streamResults.Count);
|
||||
for (int i = 0; i < batchResult.Count; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(streamResults[i]));
|
||||
Assert.InRange(streamResults[i], 0.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchSpan_OutputMatchesTSeries()
|
||||
{
|
||||
int period = 30;
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.0, sigma: 0.2, seed: 42);
|
||||
var source = new TSeries();
|
||||
for (int i = 0; i < 80; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
source.Add(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
_ = Adf.Batch(source, period);
|
||||
|
||||
double[] spanOutput = new double[source.Count];
|
||||
Adf.Batch(source.Values, spanOutput.AsSpan(), period);
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Assert.InRange(spanOutput[i], 0.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsResultsAndIndicator()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.0, sigma: 0.2, seed: 42);
|
||||
var source = new TSeries();
|
||||
for (int i = 0; i < 60; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
source.Add(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
var (results, indicator) = Adf.Calculate(source, 30);
|
||||
|
||||
Assert.NotNull(results);
|
||||
Assert.NotNull(indicator);
|
||||
Assert.Equal(source.Count, results.Count);
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prime_SetsState()
|
||||
{
|
||||
var a = new Adf(25);
|
||||
double[] data = new double[30];
|
||||
var rng = new Random(42);
|
||||
double price = 100;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
price += rng.NextDouble() * 2 - 1;
|
||||
data[i] = price;
|
||||
}
|
||||
|
||||
a.Prime(data);
|
||||
Assert.True(a.IsHot);
|
||||
Assert.True(double.IsFinite(a.PValue));
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// F) ADF-Specific Tests
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
public class AdfSpecificTests
|
||||
{
|
||||
[Fact]
|
||||
public void StationarySeries_LowPValue()
|
||||
{
|
||||
// Create a mean-reverting series: y_t = 0.5 * y_{t-1} + noise
|
||||
var a = new Adf(50, 1, Adf.AdfRegression.Constant);
|
||||
var rng = new Random(42);
|
||||
double y = 100;
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
y = 100 + 0.5 * (y - 100) + rng.NextDouble() * 2 - 1;
|
||||
a.Update(new TValue(now.AddMinutes(i), y));
|
||||
}
|
||||
|
||||
// A strongly mean-reverting series should have p-value well below 0.05
|
||||
Assert.True(a.PValue < 0.10, $"Expected p < 0.10 for stationary series, got {a.PValue}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RandomWalk_HighPValue()
|
||||
{
|
||||
// Create a pure random walk: y_t = y_{t-1} + noise
|
||||
var a = new Adf(50, 1, Adf.AdfRegression.Constant);
|
||||
var rng = new Random(123);
|
||||
double y = 100;
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
y += rng.NextDouble() * 2 - 1;
|
||||
a.Update(new TValue(now.AddMinutes(i), y));
|
||||
}
|
||||
|
||||
// A random walk should typically have p > 0.05
|
||||
Assert.True(a.PValue > 0.05, $"Expected p > 0.05 for random walk, got {a.PValue}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentRegressions_ProduceDifferentPValues()
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
var ncResult = Adf.Batch(source, 50, 1, Adf.AdfRegression.NoConstant);
|
||||
var cResult = Adf.Batch(source, 50, 1, Adf.AdfRegression.Constant);
|
||||
var ctResult = Adf.Batch(source, 50, 1, Adf.AdfRegression.ConstantAndTrend);
|
||||
|
||||
// All should be valid
|
||||
int last = source.Count - 1;
|
||||
Assert.InRange(ncResult.Values[last], 0.0, 1.0);
|
||||
Assert.InRange(cResult.Values[last], 0.0, 1.0);
|
||||
Assert.InRange(ctResult.Values[last], 0.0, 1.0);
|
||||
|
||||
// At least two should differ (very unlikely all three are identical)
|
||||
Assert.False(
|
||||
ncResult.Values[last] == cResult.Values[last] &&
|
||||
cResult.Values[last] == ctResult.Values[last],
|
||||
"All three regression models produced identical p-values — unexpected");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExplicitLag_DiffersFromAutoLag()
|
||||
{
|
||||
var rng = new Random(42);
|
||||
double y = 100;
|
||||
var source = new TSeries();
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
y += rng.NextDouble() * 2 - 1;
|
||||
source.Add(new TValue(DateTime.UtcNow.AddMinutes(i), y));
|
||||
}
|
||||
|
||||
var (autoResult, _) = Adf.Calculate(source, 50, 0);
|
||||
var (explicitResult, _) = Adf.Calculate(source, 50, 3);
|
||||
|
||||
// Auto and explicit lag should produce different results (usually)
|
||||
int last = source.Count - 1;
|
||||
Assert.InRange(autoResult.Values[last], 0.0, 1.0);
|
||||
Assert.InRange(explicitResult.Values[last], 0.0, 1.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentPeriods_ProduceDifferentResults()
|
||||
{
|
||||
var rng = new Random(42);
|
||||
double y = 100;
|
||||
var source = new TSeries();
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
y += rng.NextDouble() * 2 - 1;
|
||||
source.Add(new TValue(DateTime.UtcNow.AddMinutes(i), y));
|
||||
}
|
||||
|
||||
var result30 = Adf.Batch(source, 30);
|
||||
var result100 = Adf.Batch(source, 100);
|
||||
|
||||
int last = source.Count - 1;
|
||||
Assert.InRange(result30.Values[last], 0.0, 1.0);
|
||||
Assert.InRange(result100.Values[last], 0.0, 1.0);
|
||||
|
||||
// Different periods should usually give different results
|
||||
Assert.NotEqual(result30.Values[last], result100.Values[last]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventPub_IsFired()
|
||||
{
|
||||
var a = new Adf(20);
|
||||
int eventCount = 0;
|
||||
a.Pub += (object? sender, in TValueEventArgs args) => eventCount++;
|
||||
|
||||
for (int i = 0; i < 25; i++)
|
||||
{
|
||||
a.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i));
|
||||
}
|
||||
|
||||
Assert.Equal(25, eventCount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user