mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-14 08:38:04 +00:00
- Remove 'C# Implementation Considerations' sections from 34 indicator .md files - Delete 29 temp PowerShell scripts (_fix_mojibake.ps1, _hex_scan.ps1, etc.) - Move test files into tests/ subdirectories for consistent project structure - Add trader-focused bullet points to indicator documentation
354 lines
13 KiB
C#
354 lines
13 KiB
C#
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(TALib.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(TALib.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)");
|
||
}
|
||
}
|