// Vo: Mathematical property validation tests // Volume Oscillator compares short and long SMAs of volume. // No standard external library equivalents with matching implementation. // Validation uses mathematical property testing. namespace QuanTAlib.Tests; using Xunit; public class VoValidationTests { private const int DefaultShortPeriod = 5; private const int DefaultLongPeriod = 10; private const int DefaultSignalPeriod = 10; private const int TestDataLength = 500; [Fact] public void Vo_Output_IsFiniteForGbmData() { var bars = new GBM(sigma: 0.5, seed: 123).Fetch(TestDataLength, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); var vo = new Vo(DefaultShortPeriod, DefaultLongPeriod, DefaultSignalPeriod); for (int i = 0; i < bars.Count; i++) { var result = vo.Update(bars[i], isNew: true); Assert.True(double.IsFinite(result.Value), $"Vo output must be finite at bar {i}, got {result.Value}"); } } [Fact] public void Vo_ConstantVolume_ZeroOscillator() { var vo = new Vo(DefaultShortPeriod, DefaultLongPeriod, DefaultSignalPeriod); // Feed bars with identical volume for (int i = 0; i < 50; i++) { var bar = new TBar( DateTime.UtcNow.AddMinutes(i), 100, 101, 99, 100, 1000); // constant volume vo.Update(bar, isNew: true); } // When volume is constant, short MA == long MA, VO = 0 Assert.Equal(0.0, vo.Last.Value, precision: 8); } [Fact] public void Vo_IncreasingVolume_PositiveOscillator() { var vo = new Vo(DefaultShortPeriod, DefaultLongPeriod, DefaultSignalPeriod); // Feed bars with steadily increasing volume for (int i = 0; i < 50; i++) { double volume = 1000 + i * 100; // increasing var bar = new TBar( DateTime.UtcNow.AddMinutes(i), 100, 101, 99, 100, volume); vo.Update(bar, isNew: true); } // Short MA should be higher than long MA when volume is increasing Assert.True(vo.Last.Value > 0, $"VO should be positive with increasing volume, got {vo.Last.Value}"); } [Fact] public void Vo_DecreasingVolume_NegativeOscillator() { var vo = new Vo(DefaultShortPeriod, DefaultLongPeriod, DefaultSignalPeriod); // Feed bars with steadily decreasing volume for (int i = 0; i < 50; i++) { double volume = 10000 - i * 100; // decreasing var bar = new TBar( DateTime.UtcNow.AddMinutes(i), 100, 101, 99, 100, volume); vo.Update(bar, isNew: true); } // Short MA should be lower than long MA when volume is decreasing Assert.True(vo.Last.Value < 0, $"VO should be negative with decreasing volume, got {vo.Last.Value}"); } [Fact] public void Vo_Signal_IsFinite() { var bars = new GBM(sigma: 0.5, seed: 123).Fetch(TestDataLength, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); var vo = new Vo(DefaultShortPeriod, DefaultLongPeriod, DefaultSignalPeriod); for (int i = 0; i < bars.Count; i++) { vo.Update(bars[i], isNew: true); Assert.True(double.IsFinite(vo.Signal), $"Signal must be finite at bar {i}, got {vo.Signal}"); } } [Fact] public void Vo_ConstantVolume_SignalAlsoZero() { var vo = new Vo(DefaultShortPeriod, DefaultLongPeriod, DefaultSignalPeriod); for (int i = 0; i < 50; i++) { var bar = new TBar( DateTime.UtcNow.AddMinutes(i), 100, 101, 99, 100, 1000); vo.Update(bar, isNew: true); } // Signal is SMA of VO values, all of which are zero Assert.Equal(0.0, vo.Signal, precision: 8); } [Fact] public void Vo_BatchAndStreaming_ProduceSameResults() { var bars = new GBM(sigma: 0.5, seed: 123).Fetch(TestDataLength, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); // Batch var batchResults = Vo.Batch(bars, DefaultShortPeriod, DefaultLongPeriod, DefaultSignalPeriod); // Streaming var streamVo = new Vo(DefaultShortPeriod, DefaultLongPeriod, DefaultSignalPeriod); var streamResults = new double[bars.Count]; for (int i = 0; i < bars.Count; i++) { var result = streamVo.Update(bars[i], isNew: true); streamResults[i] = result.Value; } Assert.Equal(batchResults.Count, bars.Count); for (int i = 0; i < bars.Count; i++) { Assert.Equal(batchResults.Values[i], streamResults[i], precision: 8); } } [Fact] public void Vo_DifferentPeriods_ProduceDifferentResults() { var bars = new GBM(sigma: 0.5, seed: 123).Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); var vo1 = new Vo(3, 7, 5); var vo2 = new Vo(10, 30, 15); for (int i = 0; i < bars.Count; i++) { vo1.Update(bars[i], isNew: true); vo2.Update(bars[i], isNew: true); } Assert.NotEqual(vo1.Last.Value, vo2.Last.Value); } [Fact] public void Vo_BarCorrection_IsNewFalse_RestoresState() { var bars = new GBM(sigma: 0.5, seed: 123).Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); var vo = new Vo(DefaultShortPeriod, DefaultLongPeriod, DefaultSignalPeriod); for (int i = 0; i < 30; i++) { vo.Update(bars[i], isNew: true); } vo.Update(bars[30], isNew: true); double afterNew = vo.Last.Value; vo.Update(bars[30], isNew: false); double afterCorrection = vo.Last.Value; Assert.Equal(afterNew, afterCorrection, precision: 10); } [Fact] public void Vo_IsHot_AfterLongPeriod() { var vo = new Vo(DefaultShortPeriod, DefaultLongPeriod, DefaultSignalPeriod); for (int i = 0; i < DefaultLongPeriod - 1; i++) { var bar = new TBar(DateTime.UtcNow.AddMinutes(i), 100, 101, 99, 100, 1000); vo.Update(bar, isNew: true); Assert.False(vo.IsHot, $"Should not be hot at bar {i}"); } // Bar at index longPeriod-1 should make it hot (Index becomes longPeriod) var finalBar = new TBar(DateTime.UtcNow.AddMinutes(DefaultLongPeriod), 100, 101, 99, 100, 1000); vo.Update(finalBar, isNew: true); Assert.True(vo.IsHot, "Should be hot after longPeriod bars"); } }