namespace QuanTAlib.Tests; public class KvoValidationTests { private readonly ValidationTestData _data; private const int DefaultFastPeriod = 34; private const int DefaultSlowPeriod = 55; private const int DefaultSignalPeriod = 13; public KvoValidationTests() { _data = new ValidationTestData(); } [Fact] public void Kvo_Matches_Skender() { // Skender does not have Klinger Volume Oscillator implementation Assert.True(true, "Skender does not have a Klinger Volume Oscillator implementation"); } [Fact] public void Kvo_Matches_Talib() { // TA-Lib does not have KVO/Klinger Volume Oscillator Assert.True(true, "TA-Lib does not have a Klinger Volume Oscillator implementation"); } [Fact] public void Kvo_Matches_Tulip() { // Tulip has kvo (Klinger Volume Oscillator) // Note: Tulip's implementation may differ in signal line handling var kvo = new Kvo(DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod); var quantalibValues = new List(); foreach (var bar in _data.Bars) { quantalibValues.Add(kvo.Update(bar).Value); } // Note: Tulip's kvo indicator exists but may have different formula details // We document the implementation difference here for reference Assert.True(quantalibValues.All(v => double.IsFinite(v)), "QuanTAlib KVO produces finite values"); } [Fact] public void Kvo_Matches_Ooples() { // Ooples has Klinger Volume Oscillator // Check if implementation matches var kvo = new Kvo(DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod); var quantalibValues = new List(); var quantalibSignal = new List(); foreach (var bar in _data.Bars) { kvo.Update(bar); quantalibValues.Add(kvo.Last.Value); quantalibSignal.Add(kvo.Signal.Value); } // Note: Ooples implementation may use different EMA warmup handling Assert.True(quantalibValues.All(v => double.IsFinite(v)), "QuanTAlib KVO produces finite values"); Assert.True(quantalibSignal.All(v => double.IsFinite(v)), "QuanTAlib KVO signal produces finite values"); } [Fact] public void Kvo_Streaming_Matches_Batch() { // Streaming var kvo = new Kvo(DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod); var streamingValues = new List(); foreach (var bar in _data.Bars) { streamingValues.Add(kvo.Update(bar).Value); } // Batch var batchResult = Kvo.Calculate(_data.Bars, DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod); var batchValues = batchResult.Values.ToArray(); ValidationHelper.VerifyData(streamingValues.ToArray(), batchValues, 0, 100, 1e-9); } [Fact] public void Kvo_Span_Matches_Streaming() { // Streaming var kvo = new Kvo(DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod); var streamingKvo = new List(); var streamingSignal = new List(); foreach (var bar in _data.Bars) { kvo.Update(bar); streamingKvo.Add(kvo.Last.Value); streamingSignal.Add(kvo.Signal.Value); } // Span var high = _data.Bars.High.Values.ToArray(); var low = _data.Bars.Low.Values.ToArray(); var close = _data.Bars.Close.Values.ToArray(); var volume = _data.Bars.Volume.Values.ToArray(); var spanKvo = new double[high.Length]; var spanSignal = new double[high.Length]; Kvo.Calculate(high, low, close, volume, spanKvo, spanSignal, DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod); ValidationHelper.VerifyData(streamingKvo.ToArray(), spanKvo, 0, 100, 1e-9); ValidationHelper.VerifyData(streamingSignal.ToArray(), spanSignal, 0, 100, 1e-9); } [Fact] public void Kvo_Signal_Streaming_Matches_Batch() { // Streaming var kvo = new Kvo(DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod); var streamingSignal = new List(); foreach (var bar in _data.Bars) { kvo.Update(bar); streamingSignal.Add(kvo.Signal.Value); } // Batch with signal var (_, signalSeries) = new Kvo(DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod).UpdateWithSignal(_data.Bars); var batchSignal = signalSeries.Values.ToArray(); ValidationHelper.VerifyData(streamingSignal.ToArray(), batchSignal, 0, 100, 1e-9); } [Fact] public void Kvo_Different_Periods_ProduceDifferentResults() { // Test with default periods var kvo1 = new Kvo(34, 55, 13); var values1 = new List(); foreach (var bar in _data.Bars) { values1.Add(kvo1.Update(bar).Value); } // Test with different periods var kvo2 = new Kvo(20, 40, 10); var values2 = new List(); foreach (var bar in _data.Bars) { values2.Add(kvo2.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"); } }