mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-17 18:18:04 +00:00
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:
@@ -1,12 +1,14 @@
|
||||
using TALib;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for AccBands indicator.
|
||||
/// Note: Skender.Stock.Indicators, TA-Lib, Tulip, and OoplesFinance do not provide
|
||||
/// AccBands implementation for cross-validation. These tests validate against
|
||||
/// manual calculations and internal consistency across all API modes.
|
||||
/// 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
|
||||
{
|
||||
@@ -378,4 +380,149 @@ public sealed class AccBandsValidationTests : IDisposable
|
||||
|
||||
_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)");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
using TALib;
|
||||
using Skender.Stock.Indicators;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for AtrBands against external libraries.
|
||||
/// AtrBands: Middle = SMA(Close), Upper/Lower = Middle ± ATR × multiplier.
|
||||
/// TALib provides SMA and ATR sub-component validation.
|
||||
/// Skender provides SMA and ATR sub-component validation.
|
||||
/// </summary>
|
||||
public sealed class AtrBandsValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _testData;
|
||||
private readonly ITestOutputHelper _output;
|
||||
private bool _disposed;
|
||||
|
||||
public AtrBandsValidationTests(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();
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Internal Consistency Tests
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void Validate_AllModes_Consistency()
|
||||
{
|
||||
int[] periods = { 5, 10, 20, 50 };
|
||||
double[] multipliers = { 1.0, 2.0, 2.5 };
|
||||
|
||||
foreach (int period in periods)
|
||||
{
|
||||
foreach (double multiplier in multipliers)
|
||||
{
|
||||
// Batch (static)
|
||||
var (bMid, bUp, bLo) = AtrBands.Batch(_testData.Bars, period, multiplier);
|
||||
|
||||
// Streaming
|
||||
var streaming = new AtrBands(period, multiplier);
|
||||
var sMid = new TSeries();
|
||||
var sUp = new TSeries();
|
||||
var sLo = new TSeries();
|
||||
foreach (var bar in _testData.Bars)
|
||||
{
|
||||
streaming.Update(bar);
|
||||
sMid.Add(streaming.Last);
|
||||
sUp.Add(streaming.Upper);
|
||||
sLo.Add(streaming.Lower);
|
||||
}
|
||||
|
||||
ValidationHelper.VerifySeriesEqual(bMid, sMid);
|
||||
ValidationHelper.VerifySeriesEqual(bUp, sUp);
|
||||
ValidationHelper.VerifySeriesEqual(bLo, sLo);
|
||||
|
||||
// Span
|
||||
double[] high = _testData.HighPrices.ToArray();
|
||||
double[] low = _testData.LowPrices.ToArray();
|
||||
double[] close = _testData.ClosePrices.ToArray();
|
||||
double[] spanMid = new double[high.Length];
|
||||
double[] spanUp = new double[high.Length];
|
||||
double[] spanLo = new double[high.Length];
|
||||
AtrBands.Batch(
|
||||
new AtrBands.AtrBandsInput(high.AsSpan(), low.AsSpan(), close.AsSpan()),
|
||||
new AtrBands.AtrBandsOutput(spanMid.AsSpan(), spanUp.AsSpan(), spanLo.AsSpan()),
|
||||
period, multiplier);
|
||||
|
||||
for (int i = 0; i < high.Length; i++)
|
||||
{
|
||||
Assert.Equal(bMid[i].Value, spanMid[i], 9);
|
||||
Assert.Equal(bUp[i].Value, spanUp[i], 9);
|
||||
Assert.Equal(bLo[i].Value, spanLo[i], 9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_output.WriteLine("AtrBands mode consistency validated (batch/stream/span)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_BandSymmetry()
|
||||
{
|
||||
var (mid, up, lo) = AtrBands.Batch(_testData.Bars, 20, 2.0);
|
||||
|
||||
for (int i = 0; i < mid.Count; i++)
|
||||
{
|
||||
double upperWidth = up[i].Value - mid[i].Value;
|
||||
double lowerWidth = mid[i].Value - lo[i].Value;
|
||||
Assert.Equal(upperWidth, lowerWidth, 1e-10);
|
||||
}
|
||||
|
||||
_output.WriteLine("AtrBands band symmetry validated");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_LargeDataset_FiniteOutputs()
|
||||
{
|
||||
var (mid, up, lo) = AtrBands.Batch(_testData.Bars, 50, 2.0);
|
||||
|
||||
ValidationHelper.VerifyAllFinite(mid, startIndex: 0);
|
||||
ValidationHelper.VerifyAllFinite(up, startIndex: 0);
|
||||
ValidationHelper.VerifyAllFinite(lo, startIndex: 0);
|
||||
|
||||
for (int i = 1; i < mid.Count; i++)
|
||||
{
|
||||
Assert.True(up[i].Value >= lo[i].Value, $"Upper >= Lower at {i}");
|
||||
}
|
||||
|
||||
_output.WriteLine("AtrBands large dataset validated");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_MultiplierScaling()
|
||||
{
|
||||
double[] multipliers = { 1.0, 2.0, 3.0, 4.0 };
|
||||
double[] widths = new double[multipliers.Length];
|
||||
|
||||
for (int i = 0; i < multipliers.Length; i++)
|
||||
{
|
||||
var ind = new AtrBands(20, multipliers[i]);
|
||||
foreach (var bar in _testData.Bars)
|
||||
{
|
||||
ind.Update(bar);
|
||||
}
|
||||
widths[i] = ind.Upper.Value - ind.Lower.Value;
|
||||
}
|
||||
|
||||
double baseWidth = widths[0];
|
||||
for (int i = 1; i < multipliers.Length; i++)
|
||||
{
|
||||
double expected = baseWidth * multipliers[i];
|
||||
Assert.Equal(expected, widths[i], 1e-9);
|
||||
}
|
||||
|
||||
_output.WriteLine("AtrBands multiplier scaling validated");
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// TALib Sub-Component Validation
|
||||
// AtrBands middle band = SMA(Close, period) → validates against TALib SMA
|
||||
// AtrBands band width ∝ ATR → validates ATR component against TALib ATR
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void Validate_Talib_SMA_MiddleBand()
|
||||
{
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
|
||||
double[] closeData = _testData.ClosePrices.ToArray();
|
||||
double[] smaOutput = new double[closeData.Length];
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var (qMid, _, _) = AtrBands.Batch(_testData.Bars, period, 2.0);
|
||||
|
||||
var retCode = Functions.Sma<double>(
|
||||
closeData,
|
||||
0..^0,
|
||||
smaOutput,
|
||||
out var outRange,
|
||||
period);
|
||||
|
||||
Assert.Equal(Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = Functions.SmaLookback(period);
|
||||
|
||||
ValidationHelper.VerifyData(qMid, smaOutput, outRange, lookback);
|
||||
}
|
||||
_output.WriteLine("AtrBands middle band validated against TALib SMA for all periods");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Talib_ATR_BandWidth()
|
||||
{
|
||||
// AtrBands: width = 2 × multiplier × ATR, so half-width = multiplier × ATR
|
||||
// We validate that (Upper - Middle) / multiplier ≈ ATR from TALib
|
||||
int[] periods = { 10, 20, 50 };
|
||||
double multiplier = 2.0;
|
||||
|
||||
double[] highData = _testData.HighPrices.ToArray();
|
||||
double[] lowData = _testData.LowPrices.ToArray();
|
||||
double[] closeData = _testData.ClosePrices.ToArray();
|
||||
double[] atrOutput = new double[closeData.Length];
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var (qMid, qUp, _) = AtrBands.Batch(_testData.Bars, period, multiplier);
|
||||
|
||||
var retCode = Functions.Atr<double>(
|
||||
highData,
|
||||
lowData,
|
||||
closeData,
|
||||
0..^0,
|
||||
atrOutput,
|
||||
out var outRange,
|
||||
period);
|
||||
|
||||
Assert.Equal(Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = Functions.AtrLookback(period);
|
||||
var (offset, _) = outRange.GetOffsetAndLength(atrOutput.Length);
|
||||
|
||||
// Compare extracted ATR from our bands vs TALib ATR
|
||||
int count = qMid.Count;
|
||||
int start = Math.Max(0, count - 100);
|
||||
for (int i = start; i < count; i++)
|
||||
{
|
||||
double ourAtr = (qUp[i].Value - qMid[i].Value) / multiplier;
|
||||
|
||||
if (i < lookback)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int tIndex = i - offset;
|
||||
if (tIndex < 0 || tIndex >= atrOutput.Length)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
double talibAtr = atrOutput[tIndex];
|
||||
|
||||
Assert.True(
|
||||
Math.Abs(ourAtr - talibAtr) <= ValidationHelper.TalibTolerance,
|
||||
$"ATR mismatch at {i}: QuanTAlib={ourAtr:G17}, TALib={talibAtr:G17}");
|
||||
}
|
||||
}
|
||||
_output.WriteLine("AtrBands ATR component validated against TALib ATR for all periods");
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Skender Sub-Component Validation
|
||||
// Middle band = SMA → validates against Skender GetSma()
|
||||
// Band width ∝ ATR → validates against Skender GetAtr()
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_SMA_MiddleBand()
|
||||
{
|
||||
int[] periods = { 5, 10, 20, 50 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var (qMid, _, _) = AtrBands.Batch(_testData.Bars, period, 2.0);
|
||||
|
||||
var sResult = _testData.SkenderQuotes
|
||||
.GetSma(period)
|
||||
.ToList();
|
||||
|
||||
ValidationHelper.VerifyData(qMid, sResult, s => s.Sma);
|
||||
}
|
||||
_output.WriteLine("AtrBands middle band validated against Skender SMA for all periods");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_ATR_BandWidth()
|
||||
{
|
||||
int[] periods = { 10, 20, 50 };
|
||||
double multiplier = 2.0;
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var (qMid, qUp, _) = AtrBands.Batch(_testData.Bars, period, multiplier);
|
||||
|
||||
var sResult = _testData.SkenderQuotes
|
||||
.GetAtr(period)
|
||||
.ToList();
|
||||
|
||||
// Compare extracted ATR from our bands vs Skender ATR
|
||||
int count = qMid.Count;
|
||||
int start = Math.Max(0, count - 100);
|
||||
for (int i = start; i < count; i++)
|
||||
{
|
||||
double ourAtr = (qUp[i].Value - qMid[i].Value) / multiplier;
|
||||
double? skenderAtr = sResult[i].Atr;
|
||||
|
||||
if (!skenderAtr.HasValue)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Assert.True(
|
||||
Math.Abs(ourAtr - skenderAtr.Value) <= ValidationHelper.SkenderTolerance,
|
||||
$"ATR mismatch at {i}: QuanTAlib={ourAtr:G17}, Skender={skenderAtr.Value:G17}");
|
||||
}
|
||||
}
|
||||
_output.WriteLine("AtrBands ATR component validated against Skender ATR for all periods");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_BandStructure()
|
||||
{
|
||||
var period = 20;
|
||||
var multiplier = 2.0;
|
||||
|
||||
var (qMid, qUp, qLo) = AtrBands.Batch(_testData.Bars, period, multiplier);
|
||||
|
||||
var smaResult = _testData.SkenderQuotes.GetSma(period).ToList();
|
||||
var atrResult = _testData.SkenderQuotes.GetAtr(period).ToList();
|
||||
|
||||
// Compare only the last 100 fully-converged values to avoid
|
||||
// warmup divergence between QuanTAlib and Skender ATR implementations
|
||||
int count = qMid.Count;
|
||||
int start = Math.Max(0, count - 100);
|
||||
int matched = 0;
|
||||
for (int i = start; i < count; i++)
|
||||
{
|
||||
if (!smaResult[i].Sma.HasValue || !atrResult[i].Atr.HasValue)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
double expectedMid = smaResult[i].Sma!.Value;
|
||||
double expectedAtr = atrResult[i].Atr!.Value;
|
||||
double expectedUp = expectedMid + multiplier * expectedAtr;
|
||||
double expectedLo = expectedMid - multiplier * expectedAtr;
|
||||
|
||||
Assert.True(
|
||||
Math.Abs(qMid[i].Value - expectedMid) <= ValidationHelper.SkenderTolerance,
|
||||
$"Middle mismatch at {i}: QuanTAlib={qMid[i].Value:G17}, Skender={expectedMid:G17}");
|
||||
Assert.True(
|
||||
Math.Abs(qUp[i].Value - expectedUp) <= ValidationHelper.SkenderTolerance,
|
||||
$"Upper mismatch at {i}: QuanTAlib={qUp[i].Value:G17}, Skender={expectedUp:G17}");
|
||||
Assert.True(
|
||||
Math.Abs(qLo[i].Value - expectedLo) <= ValidationHelper.SkenderTolerance,
|
||||
$"Lower mismatch at {i}: QuanTAlib={qLo[i].Value:G17}, Skender={expectedLo:G17}");
|
||||
matched++;
|
||||
}
|
||||
|
||||
Assert.True(matched >= 50, $"Expected at least 50 matched values, got {matched}");
|
||||
_output.WriteLine($"AtrBands full band structure validated against Skender SMA+ATR ({matched} converged values)");
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using Skender.Stock.Indicators;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
@@ -197,4 +198,133 @@ public sealed class DchannelValidationTests : IDisposable
|
||||
|
||||
_output.WriteLine("Dchannel large dataset validated");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_Batch_UpperBand()
|
||||
{
|
||||
// Convention difference: Skender Donchian uses prior N bars [i-N, i-1] (excludes current bar)
|
||||
// QuanTAlib Dchannel uses inclusive N bars [i-N+1, i] (includes current bar).
|
||||
// Therefore: QuanTAlib[i] should match Skender[i+1] for converged values.
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var (_, qUp, _) = Dchannel.Batch(_testData.Bars, period);
|
||||
var sResult = _testData.SkenderQuotes.GetDonchian(period).ToList();
|
||||
|
||||
int count = Math.Min(qUp.Count, sResult.Count);
|
||||
int start = Math.Max(period + 1, count - 100);
|
||||
for (int i = start; i < count - 1; i++)
|
||||
{
|
||||
double qValue = qUp[i].Value;
|
||||
double? sValue = (double?)sResult[i + 1].UpperBand;
|
||||
if (!sValue.HasValue)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Assert.True(
|
||||
Math.Abs(qValue - sValue.Value) <= ValidationHelper.SkenderTolerance,
|
||||
$"Period={period}, Mismatch at q[{i}] vs s[{i + 1}]: QuanTAlib={qValue:G17}, Skender={sValue.Value:G17}");
|
||||
}
|
||||
}
|
||||
_output.WriteLine("Dchannel upper band validated against Skender GetDonchian (offset +1)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_Batch_LowerBand()
|
||||
{
|
||||
// Same offset convention: QuanTAlib[i] == Skender[i+1]
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var (_, _, qLo) = Dchannel.Batch(_testData.Bars, period);
|
||||
var sResult = _testData.SkenderQuotes.GetDonchian(period).ToList();
|
||||
|
||||
int count = Math.Min(qLo.Count, sResult.Count);
|
||||
int start = Math.Max(period + 1, count - 100);
|
||||
for (int i = start; i < count - 1; i++)
|
||||
{
|
||||
double qValue = qLo[i].Value;
|
||||
double? sValue = (double?)sResult[i + 1].LowerBand;
|
||||
if (!sValue.HasValue)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Assert.True(
|
||||
Math.Abs(qValue - sValue.Value) <= ValidationHelper.SkenderTolerance,
|
||||
$"Period={period}, Mismatch at q[{i}] vs s[{i + 1}]: QuanTAlib={qValue:G17}, Skender={sValue.Value:G17}");
|
||||
}
|
||||
}
|
||||
_output.WriteLine("Dchannel lower band validated against Skender GetDonchian (offset +1)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_Batch_Centerline()
|
||||
{
|
||||
// Same offset convention: QuanTAlib[i] == Skender[i+1]
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var (qMid, _, _) = Dchannel.Batch(_testData.Bars, period);
|
||||
var sResult = _testData.SkenderQuotes.GetDonchian(period).ToList();
|
||||
|
||||
int count = Math.Min(qMid.Count, sResult.Count);
|
||||
int start = Math.Max(period + 1, count - 100);
|
||||
for (int i = start; i < count - 1; i++)
|
||||
{
|
||||
double qValue = qMid[i].Value;
|
||||
double? sValue = (double?)sResult[i + 1].Centerline;
|
||||
if (!sValue.HasValue)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Assert.True(
|
||||
Math.Abs(qValue - sValue.Value) <= ValidationHelper.SkenderTolerance,
|
||||
$"Period={period}, Mismatch at q[{i}] vs s[{i + 1}]: QuanTAlib={qValue:G17}, Skender={sValue.Value:G17}");
|
||||
}
|
||||
}
|
||||
_output.WriteLine("Dchannel centerline validated against Skender GetDonchian (offset +1)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_Streaming_UpperBand()
|
||||
{
|
||||
// Same offset convention: QuanTAlib[i] == Skender[i+1]
|
||||
int[] periods = { 10, 20, 50 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var dchannel = new Dchannel(period);
|
||||
var qUpResults = new TSeries();
|
||||
foreach (var bar in _testData.Bars)
|
||||
{
|
||||
dchannel.Update(bar);
|
||||
qUpResults.Add(dchannel.Upper);
|
||||
}
|
||||
|
||||
var sResult = _testData.SkenderQuotes.GetDonchian(period).ToList();
|
||||
|
||||
int count = Math.Min(qUpResults.Count, sResult.Count);
|
||||
int start = Math.Max(period + 1, count - 100);
|
||||
for (int i = start; i < count - 1; i++)
|
||||
{
|
||||
double qValue = qUpResults[i].Value;
|
||||
double? sValue = (double?)sResult[i + 1].UpperBand;
|
||||
if (!sValue.HasValue)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Assert.True(
|
||||
Math.Abs(qValue - sValue.Value) <= ValidationHelper.SkenderTolerance,
|
||||
$"Period={period}, Mismatch at q[{i}] vs s[{i + 1}]: QuanTAlib={qValue:G17}, Skender={sValue.Value:G17}");
|
||||
}
|
||||
}
|
||||
_output.WriteLine("Dchannel streaming upper band validated against Skender GetDonchian (offset +1)");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Skender.Stock.Indicators;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
@@ -281,4 +282,94 @@ public sealed class FcbValidationTests : IDisposable
|
||||
|
||||
_output.WriteLine("FCB band monotonicity validated");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_BandStructure()
|
||||
{
|
||||
// Skender GetFcb(windowSpan) uses Williams fractal carry-forward:
|
||||
// - windowSpan is half-width for 3-bar fractal detection (min=2)
|
||||
// - UpperBand = last confirmed FractalBear (highest high carry-forward)
|
||||
// - LowerBand = last confirmed FractalBull (lowest low carry-forward)
|
||||
// - Results are decimal? (need cast to double)
|
||||
//
|
||||
// NOTE: Skender's UpperBand can be LOWER than LowerBand when the last
|
||||
// bear fractal occurred at a lower price than the last bull fractal.
|
||||
// This is a known property of fractal carry-forward algorithms.
|
||||
//
|
||||
// QuanTAlib Fcb(period) uses monotonic deques over a lookback window
|
||||
// and always maintains Upper >= Lower ordering.
|
||||
|
||||
int windowSpan = 2;
|
||||
var sResult = _testData.SkenderQuotes
|
||||
.GetFcb(windowSpan)
|
||||
.ToList();
|
||||
|
||||
// Verify Skender produces finite values
|
||||
int validCount = 0;
|
||||
for (int i = 0; i < sResult.Count; i++)
|
||||
{
|
||||
if (sResult[i].UpperBand.HasValue && sResult[i].LowerBand.HasValue)
|
||||
{
|
||||
double upper = (double)sResult[i].UpperBand!.Value;
|
||||
double lower = (double)sResult[i].LowerBand!.Value;
|
||||
Assert.True(double.IsFinite(upper), $"Skender Upper finite at bar {i}");
|
||||
Assert.True(double.IsFinite(lower), $"Skender Lower finite at bar {i}");
|
||||
Assert.True(upper > 0, $"Skender Upper positive at bar {i}");
|
||||
Assert.True(lower > 0, $"Skender Lower positive at bar {i}");
|
||||
validCount++;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(validCount > 0, "Skender should produce some valid FCB values");
|
||||
_output.WriteLine($"Skender FCB band structure validated ({validCount} valid bars with finite values)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_BothProduceChannels()
|
||||
{
|
||||
// Both QuanTAlib and Skender FCB should produce meaningful channels
|
||||
// that track price structure. Verify both produce valid finite values.
|
||||
//
|
||||
// NOTE: Skender bands can cross (Upper < Lower) due to fractal
|
||||
// carry-forward semantics, so we only validate finite positive values.
|
||||
|
||||
int windowSpan = 2;
|
||||
int period = 20;
|
||||
|
||||
var sResult = _testData.SkenderQuotes
|
||||
.GetFcb(windowSpan)
|
||||
.ToList();
|
||||
|
||||
var (qMiddle, qUpper, qLower) = Fcb.Batch(_testData.Bars, period);
|
||||
|
||||
// After warmup, both should have valid bands
|
||||
int qValidCount = 0;
|
||||
int sValidCount = 0;
|
||||
|
||||
for (int i = period + 2; i < qMiddle.Count && i < sResult.Count; i++)
|
||||
{
|
||||
if (qUpper[i].Value > 0 && qLower[i].Value > 0)
|
||||
{
|
||||
Assert.True(qUpper[i].Value >= qLower[i].Value,
|
||||
$"QuanTAlib Upper >= Lower at bar {i}");
|
||||
qValidCount++;
|
||||
}
|
||||
|
||||
if (sResult[i].UpperBand.HasValue && sResult[i].LowerBand.HasValue)
|
||||
{
|
||||
double sUpper = (double)sResult[i].UpperBand!.Value;
|
||||
double sLower = (double)sResult[i].LowerBand!.Value;
|
||||
Assert.True(double.IsFinite(sUpper) && sUpper > 0,
|
||||
$"Skender Upper finite and positive at bar {i}");
|
||||
Assert.True(double.IsFinite(sLower) && sLower > 0,
|
||||
$"Skender Lower finite and positive at bar {i}");
|
||||
sValidCount++;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(qValidCount > 100, $"QuanTAlib produced {qValidCount} valid bars");
|
||||
Assert.True(sValidCount > 100, $"Skender produced {sValidCount} valid bars");
|
||||
|
||||
_output.WriteLine($"FCB channel comparison: QuanTAlib={qValidCount}, Skender={sValidCount} valid bars");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,84 +383,96 @@ public sealed class KchannelValidationTests : IDisposable
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_SkenderComparison_BandStructure()
|
||||
public void Validate_Skender_MiddleBand()
|
||||
{
|
||||
// Skender uses ATR-based bands similar to our implementation
|
||||
// Validate structural correctness: upper > middle > lower, symmetric bands
|
||||
// Skender GetKeltner uses EMA center + ATR bands, same as QuanTAlib.
|
||||
// IMPORTANT: Skender defaults atrPeriods=10, but QuanTAlib uses the same period
|
||||
// for both EMA and ATR. We must pass atrPeriods=emaPeriods for exact comparison.
|
||||
// Both use warmup compensation differently, so we skip early bars.
|
||||
|
||||
var skenderPeriod = 20;
|
||||
var skenderMultiplier = 2.0;
|
||||
int[] periods = { 5, 10, 20, 50 };
|
||||
double multiplier = 2.0;
|
||||
|
||||
// Get Skender results (they use EMA middle + ATR bands)
|
||||
var skenderResults = _testData.SkenderQuotes
|
||||
.GetKeltner(skenderPeriod, skenderMultiplier)
|
||||
.ToList();
|
||||
|
||||
// Get our results
|
||||
var (ourMid, _, _) = Kchannel.Batch(_testData.Bars, skenderPeriod, skenderMultiplier);
|
||||
|
||||
// Both should have upper > middle > lower structure
|
||||
int warmup = skenderPeriod * 2;
|
||||
for (int i = warmup; i < ourMid.Count && i < skenderResults.Count; i++)
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var sk = skenderResults[i];
|
||||
if (sk.UpperBand.HasValue && sk.LowerBand.HasValue && sk.Centerline.HasValue)
|
||||
{
|
||||
// Structural check
|
||||
Assert.True(sk.UpperBand.Value > sk.Centerline.Value, $"Skender Upper > Middle at {i}");
|
||||
Assert.True(sk.LowerBand.Value < sk.Centerline.Value, $"Skender Lower < Middle at {i}");
|
||||
var (qMiddle, _, _) = Kchannel.Batch(_testData.Bars, period, multiplier);
|
||||
|
||||
// Both use symmetric ATR-based bands
|
||||
double skWidth = sk.UpperBand.Value - sk.LowerBand.Value;
|
||||
// Skender: atrPeriods = period to match QuanTAlib's single-period design
|
||||
var sResult = _testData.SkenderQuotes
|
||||
.GetKeltner(period, multiplier, period)
|
||||
.ToList();
|
||||
|
||||
Assert.True(skWidth > 0, $"Skender width > 0 at {i}");
|
||||
}
|
||||
// Compare middle band (EMA of close) using ValidationHelper
|
||||
ValidationHelper.VerifyData(qMiddle, sResult, s => s.Centerline);
|
||||
}
|
||||
|
||||
_output.WriteLine($"Kchannel vs Skender structure validated (period={skenderPeriod}, mult={skenderMultiplier})");
|
||||
_output.WriteLine("Kchannel middle band validated against Skender for all periods");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_SkenderComparison_ApproximateMatch()
|
||||
public void Validate_Skender_UpperBand()
|
||||
{
|
||||
// Note: Skender may use slightly different ATR/EMA warmup, so we check approximate match
|
||||
// Our implementation uses sum/weight warmup compensation; Skender may not
|
||||
int[] periods = { 5, 10, 20, 50 };
|
||||
double multiplier = 2.0;
|
||||
|
||||
var skenderPeriod = 20;
|
||||
var skenderMultiplier = 2.0;
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var (_, up, _) = Kchannel.Batch(_testData.Bars, period, multiplier);
|
||||
|
||||
var skenderResults = _testData.SkenderQuotes
|
||||
.GetKeltner(skenderPeriod, skenderMultiplier)
|
||||
var sResult = _testData.SkenderQuotes
|
||||
.GetKeltner(period, multiplier, period)
|
||||
.ToList();
|
||||
|
||||
ValidationHelper.VerifyData(up, sResult, s => s.UpperBand);
|
||||
}
|
||||
_output.WriteLine("Kchannel upper band validated against Skender for all periods");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_LowerBand()
|
||||
{
|
||||
int[] periods = { 5, 10, 20, 50 };
|
||||
double multiplier = 2.0;
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var (_, _, lo) = Kchannel.Batch(_testData.Bars, period, multiplier);
|
||||
|
||||
var sResult = _testData.SkenderQuotes
|
||||
.GetKeltner(period, multiplier, period)
|
||||
.ToList();
|
||||
|
||||
ValidationHelper.VerifyData(lo, sResult, s => s.LowerBand);
|
||||
}
|
||||
_output.WriteLine("Kchannel lower band validated against Skender for all periods");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_BandStructure()
|
||||
{
|
||||
// Structural validation: upper > middle > lower, symmetric bands
|
||||
var period = 20;
|
||||
var multiplier = 2.0;
|
||||
|
||||
var sResult = _testData.SkenderQuotes
|
||||
.GetKeltner(period, multiplier, period)
|
||||
.ToList();
|
||||
|
||||
var (ourMid, _, _) = Kchannel.Batch(_testData.Bars, skenderPeriod, skenderMultiplier);
|
||||
var (ourMid, ourUp, ourLo) = Kchannel.Batch(_testData.Bars, period, multiplier);
|
||||
|
||||
// Compare after significant warmup (values should converge)
|
||||
int compareStart = skenderPeriod * 5; // Well past warmup
|
||||
int closeCount = 0;
|
||||
|
||||
for (int i = compareStart; i < Math.Min(ourMid.Count, skenderResults.Count); i++)
|
||||
int warmup = period * 2;
|
||||
for (int i = warmup; i < ourMid.Count && i < sResult.Count; i++)
|
||||
{
|
||||
var sk = skenderResults[i];
|
||||
if (sk.Centerline.HasValue)
|
||||
var sk = sResult[i];
|
||||
if (sk.UpperBand.HasValue && sk.LowerBand.HasValue && sk.Centerline.HasValue)
|
||||
{
|
||||
double midDiff = Math.Abs(ourMid[i].Value - sk.Centerline.Value);
|
||||
double midPct = midDiff / Math.Max(1, Math.Abs(sk.Centerline.Value));
|
||||
|
||||
// After warmup, values should be within 5% (warmup methods may differ)
|
||||
if (midPct < 0.05)
|
||||
{
|
||||
closeCount++;
|
||||
}
|
||||
Assert.True(sk.UpperBand.Value > sk.Centerline.Value, $"Skender Upper > Middle at {i}");
|
||||
Assert.True(sk.LowerBand.Value < sk.Centerline.Value, $"Skender Lower < Middle at {i}");
|
||||
Assert.True(ourUp[i].Value > ourMid[i].Value, $"Q Upper > Middle at {i}");
|
||||
Assert.True(ourLo[i].Value < ourMid[i].Value, $"Q Lower < Middle at {i}");
|
||||
}
|
||||
}
|
||||
|
||||
// Most values should be close
|
||||
int total = Math.Min(ourMid.Count, skenderResults.Count) - compareStart;
|
||||
double closeRatio = (double)closeCount / total;
|
||||
Assert.True(closeRatio > 0.9, $"Close ratio {closeRatio:P0} should be > 90%");
|
||||
|
||||
_output.WriteLine($"Kchannel vs Skender approximate match: {closeRatio:P0} within 5%");
|
||||
_output.WriteLine($"Kchannel vs Skender band structure validated");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Skender.Stock.Indicators;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
@@ -512,4 +513,66 @@ public sealed class MaenvValidationTests : IDisposable
|
||||
|
||||
_output.WriteLine("Maenv SMA ring buffer O(1) validated");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_SMA_Centerline()
|
||||
{
|
||||
// Skender GetMaEnvelopes(lookbackPeriods, percentOffset, MaType.SMA)
|
||||
// QuanTAlib Maenv(period, percentage, MaenvType.SMA)
|
||||
// Both compute: Middle = SMA(Close), Upper = Middle + Middle*pct/100, Lower = Middle - Middle*pct/100
|
||||
// For SMA type, results should match exactly.
|
||||
|
||||
int[] periods = { 5, 10, 20, 50 };
|
||||
double percentage = 2.5;
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var (qMiddle, _, _) = Maenv.Batch(_testData.Data, period, percentage, MaenvType.SMA);
|
||||
|
||||
var sResult = _testData.SkenderQuotes
|
||||
.GetMaEnvelopes(period, percentage, MaType.SMA)
|
||||
.ToList();
|
||||
|
||||
ValidationHelper.VerifyData(qMiddle, sResult, s => s.Centerline);
|
||||
}
|
||||
_output.WriteLine("Maenv SMA centerline validated against Skender for all periods");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_SMA_UpperEnvelope()
|
||||
{
|
||||
int[] periods = { 5, 10, 20, 50 };
|
||||
double percentage = 2.5;
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var (_, qUpper, _) = Maenv.Batch(_testData.Data, period, percentage, MaenvType.SMA);
|
||||
|
||||
var sResult = _testData.SkenderQuotes
|
||||
.GetMaEnvelopes(period, percentage, MaType.SMA)
|
||||
.ToList();
|
||||
|
||||
ValidationHelper.VerifyData(qUpper, sResult, s => s.UpperEnvelope);
|
||||
}
|
||||
_output.WriteLine("Maenv SMA upper envelope validated against Skender for all periods");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_SMA_LowerEnvelope()
|
||||
{
|
||||
int[] periods = { 5, 10, 20, 50 };
|
||||
double percentage = 2.5;
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var (_, _, qLower) = Maenv.Batch(_testData.Data, period, percentage, MaenvType.SMA);
|
||||
|
||||
var sResult = _testData.SkenderQuotes
|
||||
.GetMaEnvelopes(period, percentage, MaType.SMA)
|
||||
.ToList();
|
||||
|
||||
ValidationHelper.VerifyData(qLower, sResult, s => s.LowerEnvelope);
|
||||
}
|
||||
_output.WriteLine("Maenv SMA lower envelope validated against Skender for all periods");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Skender.Stock.Indicators;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Skender.Stock.Indicators;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
@@ -543,4 +544,90 @@ public sealed class SdchannelValidationTests : IDisposable
|
||||
|
||||
_output.WriteLine($"Sdchannel stdDev formula validated: Slope={ind.Slope:F4}, StdDev={ind.StdDev:F4}");
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Skender.Stock.Indicators Validation
|
||||
// NOTE: Skender's GetStdDevChannels uses a SEGMENTED approach
|
||||
// (non-overlapping windows with a single regression per segment),
|
||||
// while QuanTAlib's Sdchannel uses a ROLLING window approach
|
||||
// (regression recomputed at every bar). These are fundamentally
|
||||
// different algorithms, so exact value matching is not possible.
|
||||
// We validate structural properties instead.
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_BandStructure()
|
||||
{
|
||||
// Both implementations should produce valid channel bands:
|
||||
// Upper >= Centerline >= Lower, all finite after warmup
|
||||
int period = 20;
|
||||
double multiplier = 2.0;
|
||||
|
||||
// QuanTAlib rolling regression
|
||||
var (qMid, qUp, qLo) = Sdchannel.Batch(_testData.Data, period, multiplier);
|
||||
|
||||
// Skender segmented regression
|
||||
var sResult = _testData.SkenderQuotes
|
||||
.GetStdDevChannels(period, multiplier)
|
||||
.ToList();
|
||||
|
||||
// Both should have same count
|
||||
Assert.Equal(qMid.Count, sResult.Count);
|
||||
|
||||
// Verify QuanTAlib structural integrity
|
||||
for (int i = 0; i < qMid.Count; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(qMid[i].Value), $"QTAlib mid NaN at {i}");
|
||||
Assert.True(qUp[i].Value >= qMid[i].Value - 1e-10, $"QTAlib Upper < Mid at {i}");
|
||||
Assert.True(qLo[i].Value <= qMid[i].Value + 1e-10, $"QTAlib Lower > Mid at {i}");
|
||||
}
|
||||
|
||||
// Verify Skender structural integrity (where values exist)
|
||||
int skenderValidCount = 0;
|
||||
for (int i = 0; i < sResult.Count; i++)
|
||||
{
|
||||
if (sResult[i].Centerline.HasValue)
|
||||
{
|
||||
skenderValidCount++;
|
||||
double sMid = sResult[i].Centerline!.Value;
|
||||
double sUp = sResult[i].UpperChannel!.Value;
|
||||
double sLo = sResult[i].LowerChannel!.Value;
|
||||
|
||||
Assert.True(double.IsFinite(sMid), $"Skender mid NaN at {i}");
|
||||
Assert.True(sUp >= sMid - 1e-10, $"Skender Upper < Mid at {i}");
|
||||
Assert.True(sLo <= sMid + 1e-10, $"Skender Lower > Mid at {i}");
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(skenderValidCount > 0, "Skender should produce some valid values");
|
||||
|
||||
_output.WriteLine($"Sdchannel vs Skender structural validation passed " +
|
||||
$"(QTAlib: {qMid.Count} bars, Skender valid: {skenderValidCount} bars). " +
|
||||
$"Note: different algorithms (rolling vs segmented).");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_BandSymmetry()
|
||||
{
|
||||
// Both implementations should produce symmetric bands around centerline
|
||||
int period = 20;
|
||||
double multiplier = 2.0;
|
||||
|
||||
// Skender segmented regression
|
||||
var sResult = _testData.SkenderQuotes
|
||||
.GetStdDevChannels(period, multiplier)
|
||||
.ToList();
|
||||
|
||||
foreach (var r in sResult)
|
||||
{
|
||||
if (r.Centerline.HasValue)
|
||||
{
|
||||
double upperWidth = r.UpperChannel!.Value - r.Centerline.Value;
|
||||
double lowerWidth = r.Centerline.Value - r.LowerChannel!.Value;
|
||||
Assert.Equal(upperWidth, lowerWidth, 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
_output.WriteLine("Skender StdDevChannels band symmetry validated");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Skender.Stock.Indicators;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
@@ -567,4 +568,96 @@ public sealed class StarchannelValidationTests : IDisposable
|
||||
|
||||
_output.WriteLine($"Starchannel vs Kchannel middle difference: {diff:F6}");
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Skender.Stock.Indicators Validation
|
||||
// Skender GetStarcBands(smaPeriods, multiplier, atrPeriods)
|
||||
// uses SMA centerline + ATR bands — same algorithm as QuanTAlib.
|
||||
// We pass atrPeriods = smaPeriods to match QuanTAlib's single-period design.
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_Centerline()
|
||||
{
|
||||
int[] periods = { 5, 10, 20, 50 };
|
||||
double multiplier = 2.0;
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var (qMid, _, _) = Starchannel.Batch(_testData.Bars, period, multiplier);
|
||||
|
||||
var sResult = _testData.SkenderQuotes
|
||||
.GetStarcBands(period, multiplier, period)
|
||||
.ToList();
|
||||
|
||||
ValidationHelper.VerifyData(qMid, sResult, s => s.Centerline);
|
||||
}
|
||||
_output.WriteLine("Starchannel centerline validated against Skender for all periods");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_UpperBand()
|
||||
{
|
||||
int[] periods = { 5, 10, 20, 50 };
|
||||
double multiplier = 2.0;
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var (_, qUp, _) = Starchannel.Batch(_testData.Bars, period, multiplier);
|
||||
|
||||
var sResult = _testData.SkenderQuotes
|
||||
.GetStarcBands(period, multiplier, period)
|
||||
.ToList();
|
||||
|
||||
ValidationHelper.VerifyData(qUp, sResult, s => s.UpperBand);
|
||||
}
|
||||
_output.WriteLine("Starchannel upper band validated against Skender for all periods");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_LowerBand()
|
||||
{
|
||||
int[] periods = { 5, 10, 20, 50 };
|
||||
double multiplier = 2.0;
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var (_, _, qLo) = Starchannel.Batch(_testData.Bars, period, multiplier);
|
||||
|
||||
var sResult = _testData.SkenderQuotes
|
||||
.GetStarcBands(period, multiplier, period)
|
||||
.ToList();
|
||||
|
||||
ValidationHelper.VerifyData(qLo, sResult, s => s.LowerBand);
|
||||
}
|
||||
_output.WriteLine("Starchannel lower band validated against Skender for all periods");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_BandStructure()
|
||||
{
|
||||
var period = 20;
|
||||
var multiplier = 2.0;
|
||||
|
||||
var sResult = _testData.SkenderQuotes
|
||||
.GetStarcBands(period, multiplier, period)
|
||||
.ToList();
|
||||
|
||||
var (qMid, qUp, qLo) = Starchannel.Batch(_testData.Bars, period, multiplier);
|
||||
|
||||
int warmup = period * 2;
|
||||
for (int i = warmup; i < qMid.Count && i < sResult.Count; i++)
|
||||
{
|
||||
var sk = sResult[i];
|
||||
if (sk.UpperBand.HasValue && sk.LowerBand.HasValue && sk.Centerline.HasValue)
|
||||
{
|
||||
Assert.True(sk.UpperBand.Value > sk.Centerline.Value, $"Skender Upper > Middle at {i}");
|
||||
Assert.True(sk.LowerBand.Value < sk.Centerline.Value, $"Skender Lower < Middle at {i}");
|
||||
Assert.True(qUp[i].Value > qMid[i].Value, $"Q Upper > Middle at {i}");
|
||||
Assert.True(qLo[i].Value < qMid[i].Value, $"Q Lower < Middle at {i}");
|
||||
}
|
||||
}
|
||||
|
||||
_output.WriteLine("Starchannel vs Skender band structure validated");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using TALib;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
@@ -645,4 +646,127 @@ public sealed class TtmLrcValidationTests : IDisposable
|
||||
|
||||
_output.WriteLine("TtmLrc ±2σ vs Regchannel(multiplier=2) validated");
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// TALib Validation
|
||||
// TALib LinearReg computes the linear regression value at the end
|
||||
// of the lookback window — same as TtmLrc's midline.
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void Validate_Talib_LinearReg_Midline()
|
||||
{
|
||||
int[] periods = { 10, 20, 50, 100 };
|
||||
double[] sourceData = _testData.RawData.ToArray();
|
||||
double[] linregOutput = new double[sourceData.Length];
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var (qMid, _, _, _, _) = TtmLrc.Batch(_testData.Data, period);
|
||||
|
||||
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("TtmLrc midline validated against TALib LinearReg for all periods");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Talib_LinearRegSlope()
|
||||
{
|
||||
int[] periods = { 10, 20, 50, 100 };
|
||||
double[] sourceData = _testData.RawData.ToArray();
|
||||
double[] slopeOutput = new double[sourceData.Length];
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var ind = new TtmLrc(period);
|
||||
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);
|
||||
var (offset, _) = outRange.GetOffsetAndLength(slopeOutput.Length);
|
||||
|
||||
int count = slopes.Count;
|
||||
int start = Math.Max(0, count - 100);
|
||||
|
||||
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("TtmLrc slope validated against TALib LinearRegSlope for all periods");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Tulip_LinearReg_Midline()
|
||||
{
|
||||
int[] periods = { 10, 20, 50, 100 };
|
||||
double[] sourceData = _testData.RawData.ToArray();
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var (qMid, _, _, _, _) = TtmLrc.Batch(_testData.Data, period);
|
||||
|
||||
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;
|
||||
|
||||
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("TtmLrc midline validated against Tulip linreg for all periods");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user