mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-01 19:27: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
299 lines
11 KiB
C#
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}");
|
|
}
|
|
}
|