Files
QuanTAlib/lib/channels/accbands/AccBands.Validation.Tests.cs
T
Miha Kralj 75c6a9f135 Enhance validation tests for various indicators with external library comparisons
- Added detailed comments explaining the validation limitations for MMA and ZLEMA due to differences in algorithm implementations.
- Implemented validation tests for True Range against TALib and Tulip, ensuring directional agreement.
- Updated Ulcer Index validation to clarify differences in algorithmic approaches between QuanTAlib and Skender.
- Enhanced Ease of Movement tests to verify directional agreement with Tulip's EMV, noting differences in volume scaling.
- Expanded Klinger Volume Oscillator tests to validate against Skender and Tulip, focusing on directional agreement across multiple period configurations.
- Improved Negative Volume Index tests to compare percentage changes with Tulip, addressing differences in starting values.
- Updated Positive Volume Index tests to validate against Tulip, emphasizing percentage change comparisons.
- Enhanced Williams Accumulation/Distribution tests to verify directional agreement with Tulip, highlighting formula differences.
2026-02-11 14:46:56 -08:00

529 lines
19 KiB
C#

using TALib;
using Xunit.Abstractions;
namespace QuanTAlib.Tests;
/// <summary>
/// Validation tests for AccBands indicator.
/// Note: TA-Lib provides ACCBANDS but uses a different formula (per-bar adaptive width
/// via High*(1+4*(H-L)/(H+L))) whereas QuanTAlib uses SMA-based band width.
/// The middle band (SMA of Close) matches exactly between both implementations.
/// Skender, Tulip, and OoplesFinance do not provide AccBands.
/// </summary>
public sealed class AccBandsValidationTests : IDisposable
{
private readonly ValidationTestData _testData;
private readonly ITestOutputHelper _output;
private bool _disposed;
public AccBandsValidationTests(ITestOutputHelper output)
{
_output = output;
_testData = new ValidationTestData();
}
public void Dispose()
{
Dispose(true);
}
private void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
_disposed = true;
if (disposing)
{
_testData?.Dispose();
}
}
[Fact]
public void Validate_ManualCalculation_Period3()
{
// Manual calculation verification
// Given: High = [12, 14, 16], Low = [8, 10, 12], Close = [10, 12, 14]
// SMA(High, 3) = (12 + 14 + 16) / 3 = 14
// SMA(Low, 3) = (8 + 10 + 12) / 3 = 10
// SMA(Close, 3) = (10 + 12 + 14) / 3 = 12
// BandWidth = (14 - 10) * 2.0 = 8
// Upper = 14 + 8 = 22
// Lower = 10 - 8 = 2
// Middle = 12
var series = new TBarSeries();
var time = DateTime.UtcNow;
series.Add(new TBar(time, 10, 12, 8, 10, 100));
series.Add(new TBar(time.AddMinutes(1), 12, 14, 10, 12, 100));
series.Add(new TBar(time.AddMinutes(2), 14, 16, 12, 14, 100));
var accBands = new AccBands(3, 2.0);
var (middle, upper, lower) = accBands.Update(series);
Assert.Equal(12.0, middle.Last.Value, 1e-10);
Assert.Equal(22.0, upper.Last.Value, 1e-10);
Assert.Equal(2.0, lower.Last.Value, 1e-10);
_output.WriteLine("AccBands manual calculation (period 3) validated successfully");
}
[Fact]
public void Validate_ManualCalculation_Period5()
{
// Manual calculation verification with period 5
var series = new TBarSeries();
var time = DateTime.UtcNow;
// Create predictable data: High = Close + 5, Low = Close - 5
double[] closes = { 100, 102, 104, 106, 108 };
for (int i = 0; i < closes.Length; i++)
{
double c = closes[i];
series.Add(new TBar(time.AddMinutes(i), c, c + 5, c - 5, c, 1000));
}
// SMA(High, 5) = (105 + 107 + 109 + 111 + 113) / 5 = 109
// SMA(Low, 5) = (95 + 97 + 99 + 101 + 103) / 5 = 99
// SMA(Close, 5) = (100 + 102 + 104 + 106 + 108) / 5 = 104
// BandWidth = (109 - 99) * 2.0 = 20
// Upper = 109 + 20 = 129
// Lower = 99 - 20 = 79
var accBands = new AccBands(5, 2.0);
var (middle, upper, lower) = accBands.Update(series);
Assert.Equal(104.0, middle.Last.Value, 1e-10);
Assert.Equal(129.0, upper.Last.Value, 1e-10);
Assert.Equal(79.0, lower.Last.Value, 1e-10);
_output.WriteLine("AccBands manual calculation (period 5) validated successfully");
}
[Fact]
public void Validate_Factor_Effect()
{
// Verify factor affects band width correctly
var series = new TBarSeries();
var time = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
series.Add(new TBar(time.AddMinutes(i), 100, 110, 90, 100, 1000));
}
// With constant H/L/C: SMA(High)=110, SMA(Low)=90, SMA(Close)=100
// Spread = 110 - 90 = 20
var (middle1, upper1, lower1) = AccBands.Batch(series, 5, 1.0);
var (middle2, upper2, lower2) = AccBands.Batch(series, 5, 2.0);
var (middle3, upper3, lower3) = AccBands.Batch(series, 5, 3.0);
// Middle should be the same regardless of factor
Assert.Equal(middle1.Last.Value, middle2.Last.Value, 1e-10);
Assert.Equal(middle2.Last.Value, middle3.Last.Value, 1e-10);
Assert.Equal(100.0, middle1.Last.Value, 1e-10);
// BandWidth with factor 1.0 = 20
// BandWidth with factor 2.0 = 40
// BandWidth with factor 3.0 = 60
// Upper = SMA(High) + BandWidth
Assert.Equal(110.0 + 20.0, upper1.Last.Value, 1e-10); // 130
Assert.Equal(110.0 + 40.0, upper2.Last.Value, 1e-10); // 150
Assert.Equal(110.0 + 60.0, upper3.Last.Value, 1e-10); // 170
// Lower = SMA(Low) - BandWidth
Assert.Equal(90.0 - 20.0, lower1.Last.Value, 1e-10); // 70
Assert.Equal(90.0 - 40.0, lower2.Last.Value, 1e-10); // 50
Assert.Equal(90.0 - 60.0, lower3.Last.Value, 1e-10); // 30
_output.WriteLine("AccBands factor effect validated successfully");
}
[Fact]
public void Validate_AllModes_Consistency_Batch()
{
int[] periods = { 5, 10, 20, 50, 100 };
foreach (var period in periods)
{
// Batch mode using instance
var accBands = new AccBands(period, 2.0);
var (qMiddle, qUpper, qLower) = accBands.Update(_testData.Bars);
// Static batch
var (sMiddle, sUpper, sLower) = AccBands.Batch(_testData.Bars, period, 2.0);
// Verify match
ValidationHelper.VerifySeriesEqual(qMiddle, sMiddle);
ValidationHelper.VerifySeriesEqual(qUpper, sUpper);
ValidationHelper.VerifySeriesEqual(qLower, sLower);
}
_output.WriteLine("AccBands Batch modes consistency validated successfully");
}
[Fact]
public void Validate_AllModes_Consistency_Streaming()
{
int[] periods = { 5, 10, 20, 50, 100 };
foreach (var period in periods)
{
// Streaming mode
var streamingAcc = new AccBands(period, 2.0);
var streamMiddle = new TSeries();
var streamUpper = new TSeries();
var streamLower = new TSeries();
foreach (var bar in _testData.Bars)
{
streamingAcc.Update(bar);
streamMiddle.Add(streamingAcc.Last);
streamUpper.Add(streamingAcc.Upper);
streamLower.Add(streamingAcc.Lower);
}
// Batch mode for comparison
var (batchMiddle, batchUpper, batchLower) = AccBands.Batch(_testData.Bars, period, 2.0);
// Verify match
ValidationHelper.VerifySeriesEqual(batchMiddle, streamMiddle);
ValidationHelper.VerifySeriesEqual(batchUpper, streamUpper);
ValidationHelper.VerifySeriesEqual(batchLower, streamLower);
}
_output.WriteLine("AccBands Streaming mode consistency validated successfully");
}
[Fact]
public void Validate_AllModes_Consistency_Span()
{
int[] periods = { 5, 10, 20, 50, 100 };
double[] high = _testData.HighPrices.ToArray();
double[] low = _testData.LowPrices.ToArray();
double[] close = _testData.ClosePrices.ToArray();
foreach (var period in periods)
{
// Span mode
int len = close.Length;
double[] spanMiddle = new double[len];
double[] spanUpper = new double[len];
double[] spanLower = new double[len];
AccBands.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(),
spanMiddle.AsSpan(), spanUpper.AsSpan(), spanLower.AsSpan(),
period, 2.0);
// Batch mode for comparison
var (batchMiddle, batchUpper, batchLower) = AccBands.Batch(_testData.Bars, period, 2.0);
// Verify match
for (int i = 0; i < len; i++)
{
Assert.Equal(batchMiddle[i].Value, spanMiddle[i], 9);
Assert.Equal(batchUpper[i].Value, spanUpper[i], 9);
Assert.Equal(batchLower[i].Value, spanLower[i], 9);
}
}
_output.WriteLine("AccBands Span mode consistency validated successfully");
}
[Fact]
public void Validate_AllModes_Consistency_Eventing()
{
int[] periods = { 5, 10, 20, 50 };
foreach (var period in periods)
{
// Eventing mode
var pubSource = new TBarSeries();
var eventingInd = new AccBands(pubSource, period, 2.0);
var eventMiddle = new TSeries();
var eventUpper = new TSeries();
var eventLower = new TSeries();
foreach (var bar in _testData.Bars)
{
pubSource.Add(bar);
eventMiddle.Add(eventingInd.Last);
eventUpper.Add(eventingInd.Upper);
eventLower.Add(eventingInd.Lower);
}
// Batch mode for comparison
var (batchMiddle, batchUpper, batchLower) = AccBands.Batch(_testData.Bars, period, 2.0);
// Verify match
ValidationHelper.VerifySeriesEqual(batchMiddle, eventMiddle);
ValidationHelper.VerifySeriesEqual(batchUpper, eventUpper);
ValidationHelper.VerifySeriesEqual(batchLower, eventLower);
}
_output.WriteLine("AccBands Eventing mode consistency validated successfully");
}
[Fact]
public void Validate_Calculate_ReturnsHotIndicator()
{
int[] periods = { 5, 10, 20, 50, 100 };
foreach (var period in periods)
{
var ((middle, upper, lower), indicator) = AccBands.Calculate(_testData.Bars, period, 2.0);
// Verify indicator is hot
Assert.True(indicator.IsHot);
Assert.Equal(period, indicator.WarmupPeriod);
// Verify results match indicator state
Assert.Equal(middle.Last.Value, indicator.Last.Value, 1e-10);
Assert.Equal(upper.Last.Value, indicator.Upper.Value, 1e-10);
Assert.Equal(lower.Last.Value, indicator.Lower.Value, 1e-10);
// Verify can continue streaming
var nextBar = new TBar(DateTime.UtcNow.AddDays(1), 100, 110, 90, 105, 1000);
indicator.Update(nextBar);
Assert.True(indicator.IsHot);
}
_output.WriteLine("AccBands Calculate method validated successfully");
}
[Fact]
public void Validate_LargeDataset_NoOverflow()
{
// Test with the full 5000 bar dataset
var (middle, upper, lower) = AccBands.Batch(_testData.Bars, 100, 2.0);
// All outputs should be finite
ValidationHelper.VerifyAllFinite(middle, startIndex: 0);
ValidationHelper.VerifyAllFinite(upper, startIndex: 0);
ValidationHelper.VerifyAllFinite(lower, startIndex: 0);
// Upper should always be >= Middle, Middle should always be >= Lower (for normal data)
for (int i = 100; i < middle.Count; i++)
{
Assert.True(upper[i].Value >= middle[i].Value,
$"Upper ({upper[i].Value}) should be >= Middle ({middle[i].Value}) at index {i}");
Assert.True(middle[i].Value >= lower[i].Value,
$"Middle ({middle[i].Value}) should be >= Lower ({lower[i].Value}) at index {i}");
}
_output.WriteLine("AccBands large dataset (5000 bars) validated successfully");
}
[Fact]
public void Validate_BandWidth_IsSymmetric()
{
// Verify that Upper - SMA(High) == SMA(Low) - Lower
// This confirms the band width is applied symmetrically
var (middle, upper, lower) = AccBands.Batch(_testData.Bars, 20, 2.0);
// Calculate SMA(High) and SMA(Low) separately for verification
_ = middle; // Suppress unused variable warning - middle is not needed for symmetry test
var smaHigh = new Sma(20);
var smaLow = new Sma(20);
var smaHighResults = new TSeries();
var smaLowResults = new TSeries();
for (int i = 0; i < _testData.Bars.Count; i++)
{
var bar = _testData.Bars[i];
smaHighResults.Add(smaHigh.Update(new TValue(bar.Time, bar.High)));
smaLowResults.Add(smaLow.Update(new TValue(bar.Time, bar.Low)));
}
// After warmup, verify symmetry
for (int i = 20; i < _testData.Bars.Count; i++)
{
double upperDiff = upper[i].Value - smaHighResults[i].Value;
double lowerDiff = smaLowResults[i].Value - lower[i].Value;
Assert.Equal(upperDiff, lowerDiff, 1e-9);
}
_output.WriteLine("AccBands band width symmetry validated successfully");
}
[Fact]
public void Validate_Prime_ProducesCorrectState()
{
// Prime with history and verify state matches full calculation
const int period = 20;
// Full batch calculation
var (batchMiddle, batchUpper, batchLower) = AccBands.Batch(_testData.Bars, period, 2.0);
// Prime indicator with subset and continue
var primedIndicator = new AccBands(period, 2.0);
var subset = new TBarSeries();
for (int i = 0; i < 100; i++)
{
subset.Add(_testData.Bars[i]);
}
primedIndicator.Prime(subset);
// Continue streaming from where Prime left off
for (int i = 100; i < _testData.Bars.Count; i++)
{
primedIndicator.Update(_testData.Bars[i]);
}
// Final values should match
Assert.Equal(batchMiddle.Last.Value, primedIndicator.Last.Value, 1e-9);
Assert.Equal(batchUpper.Last.Value, primedIndicator.Upper.Value, 1e-9);
Assert.Equal(batchLower.Last.Value, primedIndicator.Lower.Value, 1e-9);
_output.WriteLine("AccBands Prime method validated successfully");
}
[Fact]
public void Validate_Talib_MiddleBand_Batch()
{
// TALib ACCBANDS uses a different upper/lower formula (per-bar adaptive width via
// High*(1+4*(H-L)/(H+L))) but the MIDDLE band is SMA(Close) which matches exactly.
int[] periods = { 5, 10, 20, 50, 100 };
double[] high = _testData.HighPrices.ToArray();
double[] low = _testData.LowPrices.ToArray();
double[] close = _testData.ClosePrices.ToArray();
int len = close.Length;
double[] talibUpper = new double[len];
double[] talibMiddle = new double[len];
double[] talibLower = new double[len];
foreach (var period in periods)
{
// QuanTAlib AccBands (batch)
var (qMiddle, _, _) = AccBands.Batch(_testData.Bars, period, 2.0);
// TALib Accbands
var retCode = Functions.Accbands<double>(
high, low, close,
0..^0,
talibUpper, talibMiddle, talibLower,
out var outRange,
period);
Assert.Equal(Core.RetCode.Success, retCode);
int lookback = Functions.AccbandsLookback(period);
// Middle band = SMA(Close) in both implementations — should match exactly
ValidationHelper.VerifyData(qMiddle, talibMiddle, outRange, lookback);
}
_output.WriteLine("AccBands middle band validated successfully against TA-Lib");
}
[Fact]
public void Validate_Talib_MiddleBand_Span()
{
// Validate middle band match using Span API
int[] periods = { 5, 10, 20, 50, 100 };
double[] high = _testData.HighPrices.ToArray();
double[] low = _testData.LowPrices.ToArray();
double[] close = _testData.ClosePrices.ToArray();
int len = close.Length;
double[] talibUpper = new double[len];
double[] talibMiddle = new double[len];
double[] talibLower = new double[len];
foreach (var period in periods)
{
// QuanTAlib AccBands (Span API)
double[] qMiddle = new double[len];
double[] qUpper = new double[len];
double[] qLower = new double[len];
AccBands.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(),
qMiddle.AsSpan(), qUpper.AsSpan(), qLower.AsSpan(),
period, 2.0);
// TALib Accbands
var retCode = Functions.Accbands<double>(
high, low, close,
0..^0,
talibUpper, talibMiddle, talibLower,
out var outRange,
period);
Assert.Equal(Core.RetCode.Success, retCode);
int lookback = Functions.AccbandsLookback(period);
// Middle band = SMA(Close) — exact match
ValidationHelper.VerifyData(qMiddle, talibMiddle, outRange, lookback);
}
_output.WriteLine("AccBands Span middle band validated successfully against TA-Lib");
}
[Fact]
public void Validate_Talib_FormulaConventionDifference()
{
// Document and verify that upper/lower bands differ between implementations.
// TALib: Upper = SMA(High * (1 + 4*(H-L)/(H+L))), per-bar adaptive width
// QuanTAlib: Upper = SMA(High) + factor*(SMA(High)-SMA(Low)), SMA-based width
// Both are valid "Acceleration Bands" variants.
const int period = 20;
double[] high = _testData.HighPrices.ToArray();
double[] low = _testData.LowPrices.ToArray();
double[] close = _testData.ClosePrices.ToArray();
int len = close.Length;
double[] talibUpper = new double[len];
double[] talibMiddle = new double[len];
double[] talibLower = new double[len];
var retCode = Functions.Accbands<double>(
high, low, close,
0..^0,
talibUpper, talibMiddle, talibLower,
out var outRange,
period);
Assert.Equal(Core.RetCode.Success, retCode);
var (qMiddle, qUpper, qLower) = AccBands.Batch(_testData.Bars, period, 2.0);
int lookback = Functions.AccbandsLookback(period);
int talibStart = outRange.Start.Value;
// Middle bands should match (both SMA of Close)
for (int i = lookback; i < qMiddle.Count && (i - talibStart) < len; i++)
{
int tIdx = i - talibStart;
if (tIdx >= 0 && tIdx < len && talibMiddle[tIdx] != 0)
{
Assert.Equal(qMiddle[i].Value, talibMiddle[tIdx], 1e-7);
}
}
// Upper/Lower bands should differ (different formulas) but maintain same structure
int structuralCount = 0;
for (int i = lookback; i < qMiddle.Count && (i - talibStart) < len; i++)
{
int tIdx = i - talibStart;
if (tIdx >= 0 && tIdx < len && talibUpper[tIdx] != 0)
{
// Both should have Upper > Middle > Lower
Assert.True(qUpper[i].Value > qMiddle[i].Value, $"Q: Upper > Middle at {i}");
Assert.True(qLower[i].Value < qMiddle[i].Value, $"Q: Lower < Middle at {i}");
Assert.True(talibUpper[tIdx] > talibMiddle[tIdx], $"TALib: Upper > Middle at {i}");
Assert.True(talibLower[tIdx] < talibMiddle[tIdx], $"TALib: Lower < Middle at {i}");
structuralCount++;
}
}
Assert.True(structuralCount > 100, $"Validated {structuralCount} bars structurally");
_output.WriteLine($"AccBands formula convention difference validated ({structuralCount} bars)");
}
}