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.
This commit is contained in:
Miha Kralj
2026-02-11 14:46:56 -08:00
parent 6d6259a47d
commit 75c6a9f135
51 changed files with 7893 additions and 1274 deletions
@@ -1,3 +1,4 @@
using TALib;
using Xunit.Abstractions;
namespace QuanTAlib.Tests;
@@ -533,4 +534,130 @@ public sealed class RegchannelValidationTests : IDisposable
_output.WriteLine("Regchannel stdDev formula validated");
}
// ═══════════════════════════════════════════════════════════════
// TALib Validation
// TALib LinearReg computes the linear regression value at the end
// of the lookback window — same as Regchannel's midline (centerline).
// ═══════════════════════════════════════════════════════════════
[Fact]
public void Validate_Talib_LinearReg_Centerline()
{
int[] periods = { 5, 10, 20, 50 };
double[] sourceData = _testData.RawData.ToArray();
double[] linregOutput = new double[sourceData.Length];
foreach (var period in periods)
{
var (qMid, _, _) = Regchannel.Batch(_testData.Data, period, 2.0);
var retCode = Functions.LinearReg<double>(
sourceData,
0..^0,
linregOutput,
out var outRange,
period);
Assert.Equal(Core.RetCode.Success, retCode);
int lookback = Functions.LinearRegLookback(period);
ValidationHelper.VerifyData(qMid, linregOutput, outRange, lookback);
}
_output.WriteLine("Regchannel centerline validated against TALib LinearReg for all periods");
}
[Fact]
public void Validate_Talib_LinearRegSlope()
{
int[] periods = { 5, 10, 20, 50 };
double[] sourceData = _testData.RawData.ToArray();
double[] slopeOutput = new double[sourceData.Length];
foreach (var period in periods)
{
// Stream Regchannel and collect slopes
var ind = new Regchannel(period, 2.0);
var slopes = new List<double>();
foreach (var tv in _testData.Data)
{
ind.Update(tv);
slopes.Add(ind.Slope);
}
var retCode = Functions.LinearRegSlope<double>(
sourceData,
0..^0,
slopeOutput,
out var outRange,
period);
Assert.Equal(Core.RetCode.Success, retCode);
int lookback = Functions.LinearRegSlopeLookback(period);
// Compare slopes from end of series (converged)
int count = slopes.Count;
int start = Math.Max(0, count - 100);
var (offset, _) = outRange.GetOffsetAndLength(slopeOutput.Length);
for (int i = start; i < count; i++)
{
if (i < lookback)
{
continue;
}
int tIndex = i - offset;
if (tIndex < 0 || tIndex >= slopeOutput.Length)
{
continue;
}
Assert.True(
Math.Abs(slopes[i] - slopeOutput[tIndex]) <= ValidationHelper.TalibTolerance,
$"Slope mismatch at {i}: QuanTAlib={slopes[i]:G17}, TALib={slopeOutput[tIndex]:G17}");
}
}
_output.WriteLine("Regchannel slope validated against TALib LinearRegSlope for all periods");
}
[Fact]
public void Validate_Tulip_LinearReg_Centerline()
{
int[] periods = { 5, 10, 20, 50 };
double[] sourceData = _testData.RawData.ToArray();
foreach (var period in periods)
{
var (qMid, _, _) = Regchannel.Batch(_testData.Data, period, 2.0);
var linregIndicator = Tulip.Indicators.linreg;
double[][] inputs = { sourceData };
double[] options = { period };
double[][] outputs = { new double[sourceData.Length - period + 1] };
linregIndicator.Run(inputs, options, outputs);
var tLinreg = outputs[0];
int offset = period - 1; // Tulip output starts at index (period-1)
// Compare last 100 values
int count = qMid.Count;
int start = Math.Max(0, count - 100);
for (int i = start; i < count; i++)
{
int tIndex = i - offset;
if (tIndex < 0 || tIndex >= tLinreg.Length)
{
continue;
}
Assert.True(
Math.Abs(qMid[i].Value - tLinreg[tIndex]) <= ValidationHelper.TulipTolerance,
$"Mismatch at {i}: QuanTAlib={qMid[i].Value:G17}, Tulip={tLinreg[tIndex]:G17}");
}
}
_output.WriteLine("Regchannel centerline validated against Tulip linreg for all periods");
}
}