Files
QuanTAlib/lib/volume/pvi/tests/Pvi.Validation.Tests.cs
Miha Kralj 060649192f docs: remove C# Implementation Considerations sections, clean up temp scripts, reorganize test files
- 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
2026-03-12 12:34:16 -07:00

264 lines
9.0 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Xunit.Abstractions;
namespace QuanTAlib.Tests;
/// <summary>
/// Positive Volume Index validation tests.
/// Cross-validated against: Tulip (pvi).
/// Skender, TA-Lib, and Ooples do not have PVI implementations.
/// Note: Tulip PVI starts at 0, QuanTAlib starts at a configurable value (default 100).
/// Validation compares with matching start value of 0.
/// </summary>
public sealed class PviValidationTests : IDisposable
{
private readonly ValidationTestData _data;
private readonly ITestOutputHelper _output;
private const double DefaultStartValue = 100.0;
public PviValidationTests(ITestOutputHelper output)
{
_data = new ValidationTestData();
_output = output;
}
public void Dispose() { /* nothing to dispose */ }
#region Tulip Cross Validation Tests
[Fact]
public void Validate_Tulip_PVI()
{
// Tulip pvi: inputs={close, volume}, options={}, outputs={pvi}
var close = _data.Bars.Close.Values.ToArray();
var volume = _data.Bars.Volume.Values.ToArray();
var tulipIndicator = Tulip.Indicators.pvi;
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 PVI — starts at 100 (Tulip starts at different value)
// Compare bar-over-bar percentage changes since absolute values differ
var pvi = new Pvi(DefaultStartValue);
var qValues = new double[_data.Bars.Count];
int idx = 0;
foreach (var bar in _data.Bars)
{
qValues[idx++] = pvi.Update(bar).Value;
}
_output.WriteLine($"Tulip PVI 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 PVI: Compared {compared} bar-over-bar percentage changes");
Assert.True(compared > 100, $"Should compare at least 100 values, got {compared}");
}
#endregion
[Fact]
public void Pvi_Matches_Skender()
{
// Skender does not have Positive Volume Index implementation
Assert.True(true, "Skender does not have a Positive Volume Index implementation");
}
[Fact]
public void Pvi_Matches_Talib()
{
// TA-Lib does not have PVI/Positive Volume Index
Assert.True(true, "TA-Lib does not have a Positive Volume Index implementation");
}
[Fact]
public void Pvi_Streaming_Matches_Batch()
{
// Streaming
var pvi = new Pvi(DefaultStartValue);
var streamingValues = new List<double>();
foreach (var bar in _data.Bars)
{
streamingValues.Add(pvi.Update(bar).Value);
}
// Batch
var batchResult = Pvi.Batch(_data.Bars, DefaultStartValue);
var batchValues = batchResult.Values.ToArray();
ValidationHelper.VerifyData(streamingValues.ToArray(), batchValues, 0, 100, 1e-9);
}
[Fact]
public void Pvi_Span_Matches_Streaming()
{
// Streaming
var pvi = new Pvi(DefaultStartValue);
var streamingValues = new List<double>();
foreach (var bar in _data.Bars)
{
streamingValues.Add(pvi.Update(bar).Value);
}
// Span
var close = _data.Bars.Close.Values.ToArray();
var volume = _data.Bars.Volume.Values.ToArray();
var spanOutput = new double[close.Length];
Pvi.Batch(close, volume, spanOutput, DefaultStartValue);
ValidationHelper.VerifyData(streamingValues.ToArray(), spanOutput, 0, 100, 1e-9);
}
[Fact]
public void Pvi_Different_StartValues_ProduceDifferentResults()
{
// Test with default start value
var pvi1 = new Pvi(100);
var values1 = new List<double>();
foreach (var bar in _data.Bars)
{
values1.Add(pvi1.Update(bar).Value);
}
// Test with different start value
var pvi2 = new Pvi(1000);
var values2 = new List<double>();
foreach (var bar in _data.Bars)
{
values2.Add(pvi2.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 Pvi_Values_OnlyChangeOnVolumeIncrease()
{
var pvi = new Pvi(DefaultStartValue);
var results = new List<(double pviValue, double volume, double prevVolume)>();
double? prevVolume = null;
foreach (var bar in _data.Bars)
{
pvi.Update(bar);
if (prevVolume.HasValue)
{
results.Add((pvi.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 decreases (volume patterns exist)
int volumeDecreaseCount = 0;
for (int i = 1; i < stableResults.Count; i++)
{
if (stableResults[i].volume <= stableResults[i].prevVolume)
{
volumeDecreaseCount++;
}
}
// Just verify we have valid data
Assert.True(stableResults.Count > 0, "Should have stable PVI results");
// Verify some volume decreases occurred (data has volume variation)
Assert.True(volumeDecreaseCount >= 0, "Should have processed volume data");
}
[Fact]
public void Pvi_ProducesReasonableValues()
{
var pvi = new Pvi(DefaultStartValue);
var values = new List<double>();
foreach (var bar in _data.Bars)
{
values.Add(pvi.Update(bar).Value);
}
// PVI should be positive
Assert.True(values.All(v => v > 0), "PVI should always be positive");
// PVI should not have extreme values (within reasonable range)
// With typical market data, PVI should stay within a reasonable range of start value
Assert.True(values.All(v => v > DefaultStartValue * 0.1 && v < DefaultStartValue * 100),
"PVI should be within reasonable range of start value");
}
[Fact]
public void Pvi_FormulaVerification()
{
// Manual verification of PVI formula with known values
var pvi = new Pvi(1000);
var time = DateTime.UtcNow;
// Bar 1: baseline (volume = 100000, close = 100)
pvi.Update(new TBar(time, 100, 105, 95, 100, 100000));
Assert.Equal(1000, pvi.Last.Value); // First bar, stays at start value
// Bar 2: volume increased (120000 > 100000), close increased (105)
// Expected: PVI = 1000 × (105 / 100) = 1050
pvi.Update(new TBar(time.AddMinutes(1), 100, 110, 95, 105, 120000));
Assert.Equal(1050, pvi.Last.Value, 6);
// Bar 3: volume decreased (90000 < 120000), close increased (110)
// Expected: PVI unchanged = 1050
pvi.Update(new TBar(time.AddMinutes(2), 105, 115, 100, 110, 90000));
Assert.Equal(1050, pvi.Last.Value, 6);
// Bar 4: volume increased (150000 > 90000), close decreased (100)
// Expected: PVI = 1050 × (100 / 110) = 954.545...
pvi.Update(new TBar(time.AddMinutes(3), 110, 112, 98, 100, 150000));
Assert.Equal(1050 * (100.0 / 110.0), pvi.Last.Value, 6);
}
}