using Xunit.Abstractions; namespace QuanTAlib.Tests; /// /// Williams Accumulation/Distribution validation tests. /// Cross-validated against: Tulip (wad). /// Skender, TA-Lib, and Ooples do not have WAD implementations. /// /// NOTE: QuanTAlib WAD = cumulative sum(PM × Volume) — volume-weighted. /// Tulip WAD = cumulative sum(PM) — NOT volume-weighted. /// Direct value comparison is not possible due to this formula difference. /// Instead, we verify bar-over-bar directional agreement (both should trend /// in the same direction when only price movement drives the delta). /// public sealed class WadValidationTests : IDisposable { private readonly ValidationTestData _data; private readonly ITestOutputHelper _output; public WadValidationTests(ITestOutputHelper output) { _data = new ValidationTestData(); _output = output; } public void Dispose() { /* nothing to dispose */ } #region Tulip Cross Validation Tests [Fact] public void Validate_Tulip_WAD() { // Tulip wad: inputs={high, low, close}, options={}, outputs={wad} // Tulip WAD computes WAD = cumulative(PM) without volume weighting // QuanTAlib WAD computes WAD = cumulative(PM × Volume) // Since volume is always positive, PM sign is identical so // bar-over-bar changes should have the same SIGN. var high = _data.Bars.High.Values.ToArray(); var low = _data.Bars.Low.Values.ToArray(); var close = _data.Bars.Close.Values.ToArray(); var tulipIndicator = Tulip.Indicators.wad; double[][] inputs = { high, low, close }; double[] options = Array.Empty(); double[][] outputs = { new double[high.Length] }; tulipIndicator.Run(inputs, options, outputs); double[] tResult = outputs[0]; int lookback = tulipIndicator.Start(options); // QuanTAlib WAD var wad = new Wad(); var qValues = new double[_data.Bars.Count]; int idx = 0; foreach (var bar in _data.Bars) { qValues[idx++] = wad.Update(bar).Value; } _output.WriteLine($"Tulip WAD lookback: {lookback}, output length: {tResult.Length}"); _output.WriteLine($"Tulip first 5: {string.Join(", ", tResult.Take(5).Select(v => v.ToString("F4", System.Globalization.CultureInfo.InvariantCulture)))}"); _output.WriteLine($"QuanTAlib first 5: {string.Join(", ", qValues.Skip(lookback + 1).Take(5).Select(v => v.ToString("F4", System.Globalization.CultureInfo.InvariantCulture)))}"); // Compare bar-over-bar sign agreement // When Tulip WAD delta > 0 (accumulation), QuanTAlib WAD delta should also be > 0 int compared = 0; int agreed = 0; int startIdx = lookback + 3; // skip initial convergence for (int i = startIdx; i < qValues.Length && (i - lookback) < tResult.Length; i++) { int tIdx = i - lookback; if (tIdx < 1) { continue; } double qDelta = qValues[i] - qValues[i - 1]; double tDelta = tResult[tIdx] - tResult[tIdx - 1]; // Skip near-zero deltas (ambiguous direction) if (Math.Abs(tDelta) < 1e-10 || Math.Abs(qDelta) < 1e-10) { compared++; agreed++; continue; } compared++; if (Math.Sign(qDelta) == Math.Sign(tDelta)) { agreed++; } } double agreementRate = compared > 0 ? (double)agreed / compared : 0; _output.WriteLine($"Tulip WAD directional agreement: {agreed}/{compared} = {agreementRate:P1}"); // Both formulas use the same PM (price movement) sign, so direction should match strongly // Volume only scales the magnitude, not the direction Assert.True(agreementRate > 0.95, $"WAD directional agreement should exceed 95%, got {agreementRate:P1} ({agreed}/{compared})"); Assert.True(compared > 100, $"Should compare at least 100 values, got {compared}"); } #endregion [Fact] public void Wad_BatchMatchesStreaming() { // Batch calculation var batchResult = Wad.Batch(_data.Bars); // Streaming calculation var wad = new Wad(); var streamingResult = wad.Update(_data.Bars); // Compare all values Assert.Equal(batchResult.Count, streamingResult.Count); for (int i = 0; i < batchResult.Count; i++) { Assert.Equal(batchResult[i].Value, streamingResult[i].Value, precision: 10); } } [Fact] public void Wad_SpanMatchesStreaming() { var high = _data.Bars.High.Values.ToArray(); var low = _data.Bars.Low.Values.ToArray(); var close = _data.Bars.Close.Values.ToArray(); var volume = _data.Bars.Volume.Values.ToArray(); var spanOutput = new double[high.Length]; // Span calculation Wad.Batch(high, low, close, volume, spanOutput); // Streaming calculation var wad = new Wad(); var streamingValues = new List(); foreach (var bar in _data.Bars) { streamingValues.Add(wad.Update(bar).Value); } // Compare all values Assert.Equal(spanOutput.Length, streamingValues.Count); for (int i = 0; i < spanOutput.Length; i++) { Assert.Equal(spanOutput[i], streamingValues[i], precision: 10); } } }