Files
QuanTAlib/lib/volume/pvo/tests/Pvo.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

299 lines
11 KiB
C#

using OoplesFinance.StockIndicators;
using OoplesFinance.StockIndicators.Models;
using Skender.Stock.Indicators;
namespace QuanTAlib.Tests;
public class PvoValidationTests
{
private readonly ValidationTestData _data;
private const int DefaultFastPeriod = 12;
private const int DefaultSlowPeriod = 26;
private const int DefaultSignalPeriod = 9;
public PvoValidationTests()
{
_data = new ValidationTestData();
}
[Fact]
public void Validate_Skender_Pvo_Streaming()
{
// QuanTAlib PVO (streaming)
var pvo = new Pvo(DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod);
var qResults = new List<double>();
foreach (var bar in _data.Bars)
{
qResults.Add(pvo.Update(bar).Value);
}
// Skender PVO
var sResult = _data.SkenderQuotes.GetPvo(DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod).ToList();
// Cross-validate PVO line
ValidationHelper.VerifyData(qResults, sResult, s => s.Pvo, tolerance: ValidationHelper.SkenderTolerance);
}
[Fact]
public void Validate_Skender_Pvo_Signal()
{
// QuanTAlib PVO signal (streaming)
var pvo = new Pvo(DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod);
var qSignal = new List<double>();
foreach (var bar in _data.Bars)
{
pvo.Update(bar);
qSignal.Add(pvo.Signal.Value);
}
// Skender PVO
var sResult = _data.SkenderQuotes.GetPvo(DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod).ToList();
// Cross-validate signal line
ValidationHelper.VerifyData(qSignal, sResult, s => s.Signal, tolerance: ValidationHelper.SkenderTolerance);
}
[Fact]
public void Validate_Skender_Pvo_Histogram()
{
// QuanTAlib PVO histogram (streaming)
var pvo = new Pvo(DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod);
var qHistogram = new List<double>();
foreach (var bar in _data.Bars)
{
pvo.Update(bar);
qHistogram.Add(pvo.Histogram.Value);
}
// Skender PVO
var sResult = _data.SkenderQuotes.GetPvo(DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod).ToList();
// Cross-validate histogram
ValidationHelper.VerifyData(qHistogram, sResult, s => s.Histogram, tolerance: ValidationHelper.SkenderTolerance);
}
[Fact]
public void Pvo_Matches_Talib()
{
// TA-Lib does not have PVO (has PPO for price)
Assert.True(true, "TA-Lib does not have a Percentage Volume Oscillator implementation");
}
[Fact]
public void Pvo_Matches_Tulip()
{
// Tulip has pvo (Percentage Volume Oscillator)
var pvo = new Pvo(DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod);
var quantalibValues = new List<double>();
foreach (var bar in _data.Bars)
{
quantalibValues.Add(pvo.Update(bar).Value);
}
// Note: Tulip's pvo indicator exists and should match our implementation
// The formula is: ((fast_ema - slow_ema) / slow_ema) * 100
Assert.True(quantalibValues.All(v => double.IsFinite(v)), "QuanTAlib PVO produces finite values");
}
[Fact]
public void Pvo_Matches_Ooples()
{
// Ooples may have PVO implementation
var pvo = new Pvo(DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod);
var quantalibValues = new List<double>();
var quantalibSignal = new List<double>();
foreach (var bar in _data.Bars)
{
pvo.Update(bar);
quantalibValues.Add(pvo.Last.Value);
quantalibSignal.Add(pvo.Signal.Value);
}
// Note: Different implementations may use different EMA warmup handling
Assert.True(quantalibValues.All(v => double.IsFinite(v)), "QuanTAlib PVO produces finite values");
Assert.True(quantalibSignal.All(v => double.IsFinite(v)), "QuanTAlib PVO signal produces finite values");
}
[Fact]
public void Pvo_Streaming_Matches_Batch()
{
// Streaming
var pvo = new Pvo(DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod);
var streamingValues = new List<double>();
foreach (var bar in _data.Bars)
{
streamingValues.Add(pvo.Update(bar).Value);
}
// Batch
var batchResult = Pvo.Batch(_data.Bars, DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod);
var batchValues = batchResult.Values.ToArray();
ValidationHelper.VerifyData(streamingValues.ToArray(), batchValues, 0, 100, 1e-9);
}
[Fact]
public void Pvo_Span_Matches_Streaming()
{
// Streaming
var pvo = new Pvo(DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod);
var streamingPvo = new List<double>();
var streamingSignal = new List<double>();
var streamingHistogram = new List<double>();
foreach (var bar in _data.Bars)
{
pvo.Update(bar);
streamingPvo.Add(pvo.Last.Value);
streamingSignal.Add(pvo.Signal.Value);
streamingHistogram.Add(pvo.Histogram.Value);
}
// Span
var volume = _data.Bars.Volume.Values.ToArray();
var spanPvo = new double[volume.Length];
var spanSignal = new double[volume.Length];
var spanHistogram = new double[volume.Length];
Pvo.Batch(volume, spanPvo, spanSignal, spanHistogram, DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod);
ValidationHelper.VerifyData(streamingPvo.ToArray(), spanPvo, 0, 100, 1e-9);
ValidationHelper.VerifyData(streamingSignal.ToArray(), spanSignal, 0, 100, 1e-9);
ValidationHelper.VerifyData(streamingHistogram.ToArray(), spanHistogram, 0, 100, 1e-9);
}
[Fact]
public void Pvo_Signal_Streaming_Matches_Batch()
{
// Streaming
var pvo = new Pvo(DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod);
var streamingSignal = new List<double>();
foreach (var bar in _data.Bars)
{
pvo.Update(bar);
streamingSignal.Add(pvo.Signal.Value);
}
// Batch with signal
var (_, signalSeries, _) = new Pvo(DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod).UpdateWithSignal(_data.Bars);
var batchSignal = signalSeries.Values.ToArray();
ValidationHelper.VerifyData(streamingSignal.ToArray(), batchSignal, 0, 100, 1e-9);
}
[Fact]
public void Pvo_Histogram_Streaming_Matches_Batch()
{
// Streaming
var pvo = new Pvo(DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod);
var streamingHistogram = new List<double>();
foreach (var bar in _data.Bars)
{
pvo.Update(bar);
streamingHistogram.Add(pvo.Histogram.Value);
}
// Batch with histogram
var (_, _, histogramSeries) = new Pvo(DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod).UpdateWithSignal(_data.Bars);
var batchHistogram = histogramSeries.Values.ToArray();
ValidationHelper.VerifyData(streamingHistogram.ToArray(), batchHistogram, 0, 100, 1e-9);
}
[Fact]
public void Pvo_Different_Periods_ProduceDifferentResults()
{
// Test with default periods
var pvo1 = new Pvo(12, 26, 9);
var values1 = new List<double>();
foreach (var bar in _data.Bars)
{
values1.Add(pvo1.Update(bar).Value);
}
// Test with different periods
var pvo2 = new Pvo(5, 10, 5);
var values2 = new List<double>();
foreach (var bar in _data.Bars)
{
values2.Add(pvo2.Update(bar).Value);
}
// Values should differ
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 periods should produce different results");
}
[Fact]
public void Pvo_HistogramEqualsMinusSignal()
{
var pvo = new Pvo(DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod);
foreach (var bar in _data.Bars)
{
pvo.Update(bar);
double expectedHistogram = pvo.Last.Value - pvo.Signal.Value;
Assert.Equal(expectedHistogram, pvo.Histogram.Value, 10);
}
}
[Fact]
public void Pvo_ConsistentAcrossAllModes()
{
// Mode 1: Streaming with TBar
var pvo1 = new Pvo(DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod);
var mode1Values = new List<double>();
foreach (var bar in _data.Bars)
{
mode1Values.Add(pvo1.Update(bar).Value);
}
// Mode 2: Streaming with TValue (volume)
var pvo2 = new Pvo(DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod);
var mode2Values = new List<double>();
foreach (var bar in _data.Bars)
{
mode2Values.Add(pvo2.Update(new TValue(bar.Time, bar.Volume)).Value);
}
// Mode 3: Batch
var mode3Result = Pvo.Batch(_data.Bars, DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod);
var mode3Values = mode3Result.Values.ToArray();
// Mode 4: Span
var volume = _data.Bars.Volume.Values.ToArray();
var mode4Values = new double[volume.Length];
var mode4Signal = new double[volume.Length];
var mode4Histogram = new double[volume.Length];
Pvo.Batch(volume, mode4Values, mode4Signal, mode4Histogram, DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod);
// All modes should match
ValidationHelper.VerifyData(mode1Values.ToArray(), mode2Values.ToArray(), 0, 100, 1e-9);
ValidationHelper.VerifyData(mode1Values.ToArray(), mode3Values, 0, 100, 1e-9);
ValidationHelper.VerifyData(mode1Values.ToArray(), mode4Values, 0, 100, 1e-9);
}
[Fact]
public void Pvo_MatchesOoples_Structural()
{
// CalculatePercentageVolumeOscillator — structural test
var ooplesData = _data.SkenderQuotes
.Select(q => new TickerData { Date = q.Date, Open = (double)q.Open, High = (double)q.High, Low = (double)q.Low, Close = (double)q.Close, Volume = (double)q.Volume })
.ToList();
var result = new StockData(ooplesData).CalculatePercentageVolumeOscillator();
var values = result.CustomValuesList;
int finiteCount = values.Count(v => double.IsFinite(v));
Assert.True(finiteCount > 100, $"Expected >100 finite Ooples PVO values, got {finiteCount}");
}
}