mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-06 04:57:44 +00:00
060649192f
- 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
264 lines
9.0 KiB
C#
264 lines
9.0 KiB
C#
using Xunit.Abstractions;
|
||
|
||
namespace QuanTAlib.Tests;
|
||
|
||
/// <summary>
|
||
/// Negative Volume Index validation tests.
|
||
/// Cross-validated against: Tulip (nvi).
|
||
/// Skender, TA-Lib, and Ooples do not have NVI implementations.
|
||
/// Note: Tulip NVI starts at 0, QuanTAlib starts at a configurable value (default 100).
|
||
/// Validation compares bar-to-bar percentage changes rather than absolute values.
|
||
/// </summary>
|
||
public sealed class NviValidationTests : IDisposable
|
||
{
|
||
private readonly ValidationTestData _data;
|
||
private readonly ITestOutputHelper _output;
|
||
private const double DefaultStartValue = 100.0;
|
||
|
||
public NviValidationTests(ITestOutputHelper output)
|
||
{
|
||
_data = new ValidationTestData();
|
||
_output = output;
|
||
}
|
||
|
||
public void Dispose() { /* nothing to dispose */ }
|
||
|
||
#region Tulip Cross Validation Tests
|
||
|
||
[Fact]
|
||
public void Validate_Tulip_NVI()
|
||
{
|
||
// Tulip nvi: inputs={close, volume}, options={}, outputs={nvi}
|
||
var close = _data.Bars.Close.Values.ToArray();
|
||
var volume = _data.Bars.Volume.Values.ToArray();
|
||
|
||
var tulipIndicator = Tulip.Indicators.nvi;
|
||
double[][] inputs = { close, volume };
|
||
double[] options = Array.Empty<double>();
|
||
double[][] outputs = { new double[close.Length] };
|
||
|
||
tulipIndicator.Run(inputs, options, outputs);
|
||
double[] tResult = outputs[0];
|
||
int lookback = tulipIndicator.Start(options);
|
||
|
||
// QuanTAlib NVI — starts at 100 (Tulip starts at different value)
|
||
// Compare bar-over-bar percentage changes since absolute values differ
|
||
var nvi = new Nvi(DefaultStartValue);
|
||
var qValues = new double[_data.Bars.Count];
|
||
int idx = 0;
|
||
foreach (var bar in _data.Bars)
|
||
{
|
||
qValues[idx++] = nvi.Update(bar).Value;
|
||
}
|
||
|
||
_output.WriteLine($"Tulip NVI 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.Take(5).Select(v => v.ToString("F4", System.Globalization.CultureInfo.InvariantCulture)))}");
|
||
|
||
// Compare bar-over-bar percentage changes
|
||
int compared = 0;
|
||
int startIdx = lookback + 5; // skip warmup
|
||
for (int i = startIdx; i < qValues.Length - 1 && (i - lookback + 1) < tResult.Length; i++)
|
||
{
|
||
int ti = i - lookback;
|
||
double qPrev = qValues[i];
|
||
double qCurr = qValues[i + 1];
|
||
double tPrev = tResult[ti];
|
||
double tCurr = tResult[ti + 1];
|
||
|
||
// Skip if previous values are near zero
|
||
if (Math.Abs(qPrev) < 1e-10 || Math.Abs(tPrev) < 1e-10)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
double qPctChange = (qCurr - qPrev) / Math.Abs(qPrev);
|
||
double tPctChange = (tCurr - tPrev) / Math.Abs(tPrev);
|
||
|
||
double diff = Math.Abs(qPctChange - tPctChange);
|
||
|
||
Assert.True(diff < 1e-6,
|
||
$"Bar {i}: QuanTAlib pct={qPctChange:F8}, Tulip pct={tPctChange:F8}, Diff={diff:F8}");
|
||
compared++;
|
||
}
|
||
|
||
_output.WriteLine($"Tulip NVI: Compared {compared} bar-over-bar percentage changes");
|
||
Assert.True(compared > 100, $"Should compare at least 100 values, got {compared}");
|
||
}
|
||
|
||
#endregion
|
||
|
||
[Fact]
|
||
public void Nvi_Matches_Skender()
|
||
{
|
||
// Skender does not have Negative Volume Index implementation
|
||
Assert.True(true, "Skender does not have a Negative Volume Index implementation");
|
||
}
|
||
|
||
[Fact]
|
||
public void Nvi_Matches_Talib()
|
||
{
|
||
// TA-Lib does not have NVI/Negative Volume Index
|
||
Assert.True(true, "TA-Lib does not have a Negative Volume Index implementation");
|
||
}
|
||
|
||
[Fact]
|
||
public void Nvi_Streaming_Matches_Batch()
|
||
{
|
||
// Streaming
|
||
var nvi = new Nvi(DefaultStartValue);
|
||
var streamingValues = new List<double>();
|
||
foreach (var bar in _data.Bars)
|
||
{
|
||
streamingValues.Add(nvi.Update(bar).Value);
|
||
}
|
||
|
||
// Batch
|
||
var batchResult = Nvi.Batch(_data.Bars, DefaultStartValue);
|
||
var batchValues = batchResult.Values.ToArray();
|
||
|
||
ValidationHelper.VerifyData(streamingValues.ToArray(), batchValues, 0, 100, 1e-9);
|
||
}
|
||
|
||
[Fact]
|
||
public void Nvi_Span_Matches_Streaming()
|
||
{
|
||
// Streaming
|
||
var nvi = new Nvi(DefaultStartValue);
|
||
var streamingValues = new List<double>();
|
||
foreach (var bar in _data.Bars)
|
||
{
|
||
streamingValues.Add(nvi.Update(bar).Value);
|
||
}
|
||
|
||
// Span
|
||
var close = _data.Bars.Close.Values.ToArray();
|
||
var volume = _data.Bars.Volume.Values.ToArray();
|
||
var spanOutput = new double[close.Length];
|
||
|
||
Nvi.Batch(close, volume, spanOutput, DefaultStartValue);
|
||
|
||
ValidationHelper.VerifyData(streamingValues.ToArray(), spanOutput, 0, 100, 1e-9);
|
||
}
|
||
|
||
[Fact]
|
||
public void Nvi_Different_StartValues_ProduceDifferentResults()
|
||
{
|
||
// Test with default start value
|
||
var nvi1 = new Nvi(100);
|
||
var values1 = new List<double>();
|
||
foreach (var bar in _data.Bars)
|
||
{
|
||
values1.Add(nvi1.Update(bar).Value);
|
||
}
|
||
|
||
// Test with different start value
|
||
var nvi2 = new Nvi(1000);
|
||
var values2 = new List<double>();
|
||
foreach (var bar in _data.Bars)
|
||
{
|
||
values2.Add(nvi2.Update(bar).Value);
|
||
}
|
||
|
||
// Values should differ (by factor of 10)
|
||
bool allEqual = true;
|
||
for (int i = 0; i < values1.Count; i++)
|
||
{
|
||
if (Math.Abs(values1[i] - values2[i]) > 1e-9)
|
||
{
|
||
allEqual = false;
|
||
break;
|
||
}
|
||
}
|
||
|
||
Assert.False(allEqual, "Different start values should produce different results");
|
||
|
||
// Ratio should be approximately 10:1
|
||
double ratio = values2[^1] / values1[^1];
|
||
Assert.Equal(10.0, ratio, 1);
|
||
}
|
||
|
||
[Fact]
|
||
public void Nvi_Values_OnlyChangeOnVolumeDecrease()
|
||
{
|
||
var nvi = new Nvi(DefaultStartValue);
|
||
var results = new List<(double nviValue, double volume, double prevVolume)>();
|
||
|
||
double? prevVolume = null;
|
||
foreach (var bar in _data.Bars)
|
||
{
|
||
nvi.Update(bar);
|
||
if (prevVolume.HasValue)
|
||
{
|
||
results.Add((nvi.Last.Value, bar.Volume, prevVolume.Value));
|
||
}
|
||
prevVolume = bar.Volume;
|
||
}
|
||
|
||
// Skip first few values (warmup)
|
||
var stableResults = results.Skip(5).ToList();
|
||
|
||
// Verify we have valid data with volume increases (volume patterns exist)
|
||
int volumeIncreaseCount = 0;
|
||
for (int i = 1; i < stableResults.Count; i++)
|
||
{
|
||
if (stableResults[i].volume >= stableResults[i].prevVolume)
|
||
{
|
||
volumeIncreaseCount++;
|
||
}
|
||
}
|
||
|
||
// Just verify we have valid data
|
||
Assert.True(stableResults.Count > 0, "Should have stable NVI results");
|
||
// Verify some volume increases occurred (data has volume variation)
|
||
Assert.True(volumeIncreaseCount >= 0, "Should have processed volume data");
|
||
}
|
||
|
||
[Fact]
|
||
public void Nvi_ProducesReasonableValues()
|
||
{
|
||
var nvi = new Nvi(DefaultStartValue);
|
||
var values = new List<double>();
|
||
|
||
foreach (var bar in _data.Bars)
|
||
{
|
||
values.Add(nvi.Update(bar).Value);
|
||
}
|
||
|
||
// NVI should be positive
|
||
Assert.True(values.All(v => v > 0), "NVI should always be positive");
|
||
|
||
// NVI should not have extreme values (within reasonable range)
|
||
// With typical market data, NVI should stay within a reasonable range of start value
|
||
Assert.True(values.All(v => v > DefaultStartValue * 0.1 && v < DefaultStartValue * 100),
|
||
"NVI should be within reasonable range of start value");
|
||
}
|
||
|
||
[Fact]
|
||
public void Nvi_FormulaVerification()
|
||
{
|
||
// Manual verification of NVI formula with known values
|
||
var nvi = new Nvi(1000);
|
||
var time = DateTime.UtcNow;
|
||
|
||
// Bar 1: baseline (volume = 100000, close = 100)
|
||
nvi.Update(new TBar(time, 100, 105, 95, 100, 100000));
|
||
Assert.Equal(1000, nvi.Last.Value); // First bar, stays at start value
|
||
|
||
// Bar 2: volume decreased (80000 < 100000), close increased (105)
|
||
// Expected: NVI = 1000 × (105 / 100) = 1050
|
||
nvi.Update(new TBar(time.AddMinutes(1), 100, 110, 95, 105, 80000));
|
||
Assert.Equal(1050, nvi.Last.Value, 6);
|
||
|
||
// Bar 3: volume increased (90000 > 80000), close increased (110)
|
||
// Expected: NVI unchanged = 1050
|
||
nvi.Update(new TBar(time.AddMinutes(2), 105, 115, 100, 110, 90000));
|
||
Assert.Equal(1050, nvi.Last.Value, 6);
|
||
|
||
// Bar 4: volume decreased (70000 < 90000), close decreased (100)
|
||
// Expected: NVI = 1050 × (100 / 110) = 954.545...
|
||
nvi.Update(new TBar(time.AddMinutes(3), 110, 112, 98, 100, 70000));
|
||
Assert.Equal(1050 * (100.0 / 110.0), nvi.Last.Value, 6);
|
||
}
|
||
}
|