Add validation tests for various volume and momentum indicators

- Introduced Massi validation tests to ensure mathematical properties hold for the Mass Index indicator.
- Added Va validation tests for Volume Accumulation, checking for finite outputs and correct accumulation behavior.
- Implemented Vf validation tests for Volume Force, verifying outputs for rising and falling prices, and ensuring batch and streaming results match.
- Created Vo validation tests for Volume Oscillator, confirming behavior with constant, increasing, and decreasing volumes.
- Developed Vroc validation tests for Volume Rate of Change, validating outputs for constant volume and changes in volume.
- Updated project file to include new momentum indicators (MACD and RSI) in the compilation.
This commit is contained in:
Miha Kralj
2026-02-12 19:43:09 -08:00
parent 92709ef2ed
commit 951842acca
56 changed files with 12350 additions and 359 deletions
+176
View File
@@ -0,0 +1,176 @@
// Va: Mathematical property validation tests
// Volume Accumulation is a cumulative indicator. No standard external library equivalents
// with matching implementation. Validation uses mathematical property testing.
namespace QuanTAlib.Tests;
using Xunit;
public class VaValidationTests
{
private const int TestDataLength = 500;
[Fact]
public void Va_Output_IsFiniteForGbmData()
{
var bars = new GBM(sigma: 0.5, seed: 123).Fetch(TestDataLength, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var va = new Va();
for (int i = 0; i < bars.Count; i++)
{
var result = va.Update(bars[i], isNew: true);
Assert.True(double.IsFinite(result.Value),
$"Va output must be finite at bar {i}, got {result.Value}");
}
}
[Fact]
public void Va_CloseAboveMidpoint_PositiveAccumulation()
{
var va = new Va();
// Close is above midpoint: (H+L)/2 = 100, Close = 102
var bar = new TBar(DateTime.UtcNow, 101, 101, 99, 102, 1000);
var result = va.Update(bar, isNew: true);
// VA_period = volume * (close - midpoint) = 1000 * (102 - 100) = 2000
Assert.True(result.Value > 0,
$"VA should be positive when close > midpoint, got {result.Value}");
}
[Fact]
public void Va_CloseBelowMidpoint_NegativeAccumulation()
{
var va = new Va();
// Close is below midpoint: (H+L)/2 = 100, Close = 98
var bar = new TBar(DateTime.UtcNow, 101, 101, 99, 98, 1000);
var result = va.Update(bar, isNew: true);
// VA_period = volume * (close - midpoint) = 1000 * (98 - 100) = -2000
Assert.True(result.Value < 0,
$"VA should be negative when close < midpoint, got {result.Value}");
}
[Fact]
public void Va_CloseAtMidpoint_ZeroAccumulation()
{
var va = new Va();
// Close is exactly at midpoint
var bar = new TBar(DateTime.UtcNow, 101, 101, 99, 100, 1000);
var result = va.Update(bar, isNew: true);
Assert.Equal(0.0, result.Value, precision: 10);
}
[Fact]
public void Va_ZeroVolume_ZeroAccumulation()
{
var va = new Va();
// Even with close above midpoint, zero volume = zero VA contribution
var bar = new TBar(DateTime.UtcNow, 101, 101, 99, 102, 0);
var result = va.Update(bar, isNew: true);
Assert.Equal(0.0, result.Value, precision: 10);
}
[Fact]
public void Va_IsCumulative_AccumulatesOverBars()
{
var va = new Va();
// Bar 1: close above midpoint
var bar1 = new TBar(DateTime.UtcNow, 101, 101, 99, 102, 1000);
var r1 = va.Update(bar1, isNew: true);
double expectedVa1 = 1000 * (102 - 100.0); // 2000
// Bar 2: close below midpoint
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 101, 101, 99, 98, 500);
var r2 = va.Update(bar2, isNew: true);
double expectedVa2 = expectedVa1 + 500 * (98 - 100.0); // 2000 + (-1000) = 1000
Assert.Equal(expectedVa1, r1.Value, precision: 10);
Assert.Equal(expectedVa2, r2.Value, precision: 10);
}
[Fact]
public void Va_KnownCalculation_MatchesManual()
{
var va = new Va();
// Manually verified calculation
// Bar: O=100, H=105, L=95, C=103, V=2000
// Midpoint = (105 + 95) / 2 = 100
// VA_period = 2000 * (103 - 100) = 6000
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 103, 2000);
var result = va.Update(bar, isNew: true);
Assert.Equal(6000.0, result.Value, precision: 10);
}
[Fact]
public void Va_BatchAndStreaming_ProduceSameResults()
{
var bars = new GBM(sigma: 0.5, seed: 123).Fetch(TestDataLength, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Batch
var batchResults = Va.Batch(bars);
// Streaming
var streamVa = new Va();
var streamResults = new double[bars.Count];
for (int i = 0; i < bars.Count; i++)
{
var result = streamVa.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 Va_SpanAndStreaming_ProduceSameResults()
{
var bars = new GBM(sigma: 0.5, seed: 123).Fetch(TestDataLength, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var spanOutput = new double[bars.Count];
Va.Batch(
bars.High.Values, bars.Low.Values,
bars.Close.Values, bars.Volume.Values,
spanOutput);
// Streaming
var streamVa = new Va();
for (int i = 0; i < bars.Count; i++)
{
var result = streamVa.Update(bars[i], isNew: true);
Assert.Equal(spanOutput[i], result.Value, precision: 8);
}
}
[Fact]
public void Va_BarCorrection_IsNewFalse_RestoresState()
{
var bars = new GBM(sigma: 0.5, seed: 123).Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var va = new Va();
for (int i = 0; i < 30; i++)
{
va.Update(bars[i], isNew: true);
}
va.Update(bars[30], isNew: true);
double afterNew = va.Last.Value;
va.Update(bars[30], isNew: false);
double afterCorrection = va.Last.Value;
Assert.Equal(afterNew, afterCorrection, precision: 10);
}
}
+194
View File
@@ -0,0 +1,194 @@
// Vf: Mathematical property validation tests
// Volume Force is a QuanTAlib-specific indicator combining price change with volume
// and EMA smoothing. No standard external library equivalents. Validation uses
// mathematical property testing.
namespace QuanTAlib.Tests;
using Xunit;
public class VfValidationTests
{
private const int DefaultPeriod = 14;
private const int TestDataLength = 500;
[Fact]
public void Vf_Output_IsFiniteForGbmData()
{
var bars = new GBM(sigma: 0.5, seed: 123).Fetch(TestDataLength, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var vf = new Vf(DefaultPeriod);
for (int i = 0; i < bars.Count; i++)
{
var result = vf.Update(bars[i], isNew: true);
Assert.True(double.IsFinite(result.Value),
$"Vf output must be finite at bar {i}, got {result.Value}");
}
}
[Fact]
public void Vf_FirstBar_ReturnsZero()
{
var vf = new Vf(DefaultPeriod);
var bar = new TBar(DateTime.UtcNow, 100, 101, 99, 100, 1000);
var result = vf.Update(bar, isNew: true);
// First bar has no previous close, so raw VF = 0
Assert.Equal(0.0, result.Value, precision: 10);
}
[Fact]
public void Vf_RisingPrice_PositiveForce()
{
var vf = new Vf(DefaultPeriod);
// First bar
var bar1 = new TBar(DateTime.UtcNow, 100, 100, 100, 100, 1000);
vf.Update(bar1, isNew: true);
// Rising price: positive raw VF
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 105, 105, 100, 105, 1000);
var result = vf.Update(bar2, isNew: true);
// rawVF = (105 - 100) * 1000 = 5000, EMA of that should be positive
Assert.True(result.Value > 0,
$"Vf should be positive for rising price, got {result.Value}");
}
[Fact]
public void Vf_FallingPrice_NegativeForce()
{
var vf = new Vf(DefaultPeriod);
// First bar
var bar1 = new TBar(DateTime.UtcNow, 100, 100, 100, 100, 1000);
vf.Update(bar1, isNew: true);
// Falling price: negative raw VF
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 95, 100, 95, 95, 1000);
var result = vf.Update(bar2, isNew: true);
// rawVF = (95 - 100) * 1000 = -5000, EMA of that should be negative
Assert.True(result.Value < 0,
$"Vf should be negative for falling price, got {result.Value}");
}
[Fact]
public void Vf_ConstantPrice_ZeroForce()
{
var vf = new Vf(DefaultPeriod);
// Feed constant-price bars
for (int i = 0; i < 50; i++)
{
var bar = new TBar(
DateTime.UtcNow.AddMinutes(i),
100, 100, 100, 100, 1000);
vf.Update(bar, isNew: true);
}
// No price change → raw VF = 0 each bar → EMA converges to 0
Assert.Equal(0.0, vf.Last.Value, precision: 8);
}
[Fact]
public void Vf_HighVolume_AmplifiesForce()
{
// Low volume
var vfLow = new Vf(DefaultPeriod);
var bar1Low = new TBar(DateTime.UtcNow, 100, 100, 100, 100, 100);
vfLow.Update(bar1Low, isNew: true);
var bar2Low = new TBar(DateTime.UtcNow.AddMinutes(1), 105, 105, 100, 105, 100);
vfLow.Update(bar2Low, isNew: true);
// High volume
var vfHigh = new Vf(DefaultPeriod);
var bar1High = new TBar(DateTime.UtcNow, 100, 100, 100, 100, 10000);
vfHigh.Update(bar1High, isNew: true);
var bar2High = new TBar(DateTime.UtcNow.AddMinutes(1), 105, 105, 100, 105, 10000);
vfHigh.Update(bar2High, isNew: true);
// Higher volume should produce larger absolute VF
Assert.True(System.Math.Abs(vfHigh.Last.Value) > System.Math.Abs(vfLow.Last.Value),
$"High volume VF ({vfHigh.Last.Value}) should exceed low volume VF ({vfLow.Last.Value})");
}
[Fact]
public void Vf_BatchAndStreaming_ProduceSameResults()
{
var bars = new GBM(sigma: 0.5, seed: 123).Fetch(TestDataLength, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Batch
var batchResults = Vf.Batch(bars, DefaultPeriod);
// Streaming
var streamVf = new Vf(DefaultPeriod);
var streamResults = new double[bars.Count];
for (int i = 0; i < bars.Count; i++)
{
var result = streamVf.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 Vf_SpanAndStreaming_ProduceSameResults()
{
var bars = new GBM(sigma: 0.5, seed: 123).Fetch(TestDataLength, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var spanOutput = new double[bars.Count];
Vf.Batch(bars.Close.Values, bars.Volume.Values, spanOutput, DefaultPeriod);
// Streaming
var streamVf = new Vf(DefaultPeriod);
for (int i = 0; i < bars.Count; i++)
{
var result = streamVf.Update(bars[i], isNew: true);
Assert.Equal(spanOutput[i], result.Value, precision: 8);
}
}
[Fact]
public void Vf_DifferentPeriods_ProduceDifferentSmoothing()
{
var bars = new GBM(sigma: 0.5, seed: 123).Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var vf3 = new Vf(period: 3);
var vf50 = new Vf(period: 50);
for (int i = 0; i < bars.Count; i++)
{
vf3.Update(bars[i], isNew: true);
vf50.Update(bars[i], isNew: true);
}
Assert.NotEqual(vf3.Last.Value, vf50.Last.Value);
}
[Fact]
public void Vf_BarCorrection_IsNewFalse_RestoresState()
{
var bars = new GBM(sigma: 0.5, seed: 123).Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var vf = new Vf(DefaultPeriod);
for (int i = 0; i < 30; i++)
{
vf.Update(bars[i], isNew: true);
}
vf.Update(bars[30], isNew: true);
double afterNew = vf.Last.Value;
vf.Update(bars[30], isNew: false);
double afterCorrection = vf.Last.Value;
Assert.Equal(afterNew, afterCorrection, precision: 10);
}
}
+198
View File
@@ -0,0 +1,198 @@
// 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");
}
}
+227
View File
@@ -0,0 +1,227 @@
// Vroc: Mathematical property validation tests
// Volume Rate of Change measures volume momentum. No standard external library
// equivalents with matching implementation. Validation uses mathematical property testing.
namespace QuanTAlib.Tests;
using Xunit;
public class VrocValidationTests
{
private const int DefaultPeriod = 12;
private const int TestDataLength = 500;
[Fact]
public void Vroc_Output_IsFiniteForGbmData()
{
var bars = new GBM(sigma: 0.5, seed: 123).Fetch(TestDataLength, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var vroc = new Vroc(DefaultPeriod, usePercent: true);
for (int i = 0; i < bars.Count; i++)
{
var result = vroc.Update(bars[i], isNew: true);
Assert.True(double.IsFinite(result.Value),
$"Vroc output must be finite at bar {i}, got {result.Value}");
}
}
[Fact]
public void Vroc_ConstantVolume_ZeroRateOfChange()
{
var vroc = new Vroc(DefaultPeriod, usePercent: true);
for (int i = 0; i < 50; i++)
{
var bar = new TBar(
DateTime.UtcNow.AddMinutes(i),
100, 101, 99, 100, 1000);
vroc.Update(bar, isNew: true);
}
// Constant volume → VROC = ((V - V_prev) / V_prev) * 100 = 0
Assert.Equal(0.0, vroc.Last.Value, precision: 8);
}
[Fact]
public void Vroc_DoublingVolume_Returns100Percent()
{
var vroc = new Vroc(period: 1, usePercent: true);
// First bar: volume = 1000
var bar1 = new TBar(DateTime.UtcNow, 100, 101, 99, 100, 1000);
vroc.Update(bar1, isNew: true);
// Second bar: volume = 2000 (doubled)
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 100, 101, 99, 100, 2000);
var result = vroc.Update(bar2, isNew: true);
// VROC = ((2000 - 1000) / 1000) * 100 = 100%
Assert.Equal(100.0, result.Value, precision: 8);
}
[Fact]
public void Vroc_HalvingVolume_ReturnsMinus50Percent()
{
var vroc = new Vroc(period: 1, usePercent: true);
// First bar: volume = 2000
var bar1 = new TBar(DateTime.UtcNow, 100, 101, 99, 100, 2000);
vroc.Update(bar1, isNew: true);
// Second bar: volume = 1000 (halved)
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 100, 101, 99, 100, 1000);
var result = vroc.Update(bar2, isNew: true);
// VROC = ((1000 - 2000) / 2000) * 100 = -50%
Assert.Equal(-50.0, result.Value, precision: 8);
}
[Fact]
public void Vroc_PointMode_ReturnsAbsoluteDifference()
{
var vroc = new Vroc(period: 1, usePercent: false);
var bar1 = new TBar(DateTime.UtcNow, 100, 101, 99, 100, 1000);
vroc.Update(bar1, isNew: true);
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 100, 101, 99, 100, 3000);
var result = vroc.Update(bar2, isNew: true);
// Point mode: VROC = 3000 - 1000 = 2000
Assert.Equal(2000.0, result.Value, precision: 8);
}
[Fact]
public void Vroc_BeforeWarmup_ReturnsZero()
{
var vroc = new Vroc(DefaultPeriod, usePercent: true);
// Before enough bars to compare
for (int i = 0; i < DefaultPeriod; i++)
{
var bar = new TBar(
DateTime.UtcNow.AddMinutes(i),
100, 101, 99, 100, 1000 + i * 100);
var result = vroc.Update(bar, isNew: true);
Assert.Equal(0.0, result.Value, precision: 10);
}
}
[Fact]
public void Vroc_IncreasingVolume_PositiveRoc()
{
var vroc = new Vroc(DefaultPeriod, usePercent: true);
// Feed steadily increasing volume
for (int i = 0; i < 50; i++)
{
double volume = 1000 + i * 200; // increases by 200 each bar
var bar = new TBar(
DateTime.UtcNow.AddMinutes(i),
100, 101, 99, 100, volume);
vroc.Update(bar, isNew: true);
}
// After warmup, VROC should be positive
Assert.True(vroc.IsHot);
Assert.True(vroc.Last.Value > 0,
$"VROC should be positive with increasing volume, got {vroc.Last.Value}");
}
[Fact]
public void Vroc_DecreasingVolume_NegativeRoc()
{
var vroc = new Vroc(DefaultPeriod, usePercent: true);
// Feed steadily decreasing volume
for (int i = 0; i < 50; i++)
{
double volume = 20000 - i * 200; // decreases by 200 each bar
var bar = new TBar(
DateTime.UtcNow.AddMinutes(i),
100, 101, 99, 100, volume);
vroc.Update(bar, isNew: true);
}
Assert.True(vroc.IsHot);
Assert.True(vroc.Last.Value < 0,
$"VROC should be negative with decreasing volume, got {vroc.Last.Value}");
}
[Fact]
public void Vroc_BatchAndStreaming_ProduceSameResults()
{
var bars = new GBM(sigma: 0.5, seed: 123).Fetch(TestDataLength, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Batch
var batchResults = Vroc.Batch(bars, DefaultPeriod, usePercent: true);
// Streaming
var streamVroc = new Vroc(DefaultPeriod, usePercent: true);
var streamResults = new double[bars.Count];
for (int i = 0; i < bars.Count; i++)
{
var result = streamVroc.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 Vroc_PercentAndPointMode_ProduceDifferentResults()
{
var bars = new GBM(sigma: 0.5, seed: 123).Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var vrocPct = new Vroc(DefaultPeriod, usePercent: true);
var vrocPt = new Vroc(DefaultPeriod, usePercent: false);
for (int i = 0; i < bars.Count; i++)
{
vrocPct.Update(bars[i], isNew: true);
vrocPt.Update(bars[i], isNew: true);
}
// Percent and point modes should produce different final values
Assert.NotEqual(vrocPct.Last.Value, vrocPt.Last.Value);
}
[Fact]
public void Vroc_BarCorrection_IsNewFalse_RestoresState()
{
var bars = new GBM(sigma: 0.5, seed: 123).Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var vroc = new Vroc(DefaultPeriod);
for (int i = 0; i < 30; i++)
{
vroc.Update(bars[i], isNew: true);
}
vroc.Update(bars[30], isNew: true);
double afterNew = vroc.Last.Value;
vroc.Update(bars[30], isNew: false);
double afterCorrection = vroc.Last.Value;
Assert.Equal(afterNew, afterCorrection, precision: 10);
}
[Fact]
public void Vroc_IsHot_AfterWarmupPeriod()
{
var vroc = new Vroc(DefaultPeriod);
for (int i = 0; i <= DefaultPeriod; i++)
{
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), 100, 101, 99, 100, 1000);
vroc.Update(bar, isNew: true);
}
// IsHot should be true after period + 1 bars (Index > period)
Assert.True(vroc.IsHot);
}
}