mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-24 05:28:05 +00:00
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
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class PvdIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void PvdIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new PvdIndicator();
|
||||
|
||||
Assert.Equal("PVD - Price Volume Divergence", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Equal(14, indicator.PricePeriod);
|
||||
Assert.Equal(14, indicator.VolumePeriod);
|
||||
Assert.Equal(3, indicator.SmoothingPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PvdIndicator_ShortName_IsConstant()
|
||||
{
|
||||
var indicator = new PvdIndicator();
|
||||
Assert.Equal("PVD", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PvdIndicator_MinHistoryDepths_CalculatedCorrectly()
|
||||
{
|
||||
var indicator = new PvdIndicator
|
||||
{
|
||||
PricePeriod = 10,
|
||||
VolumePeriod = 20,
|
||||
SmoothingPeriod = 5
|
||||
};
|
||||
|
||||
// max(10,20) + 5 + 1 = 26
|
||||
Assert.Equal(26, indicator.MinHistoryDepths);
|
||||
Assert.Equal(26, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PvdIndicator_MinHistoryDepths_DefaultValue()
|
||||
{
|
||||
var indicator = new PvdIndicator();
|
||||
|
||||
// max(14,14) + 3 + 1 = 18
|
||||
Assert.Equal(18, indicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PvdIndicator_Initialize_CreatesInternalPvd()
|
||||
{
|
||||
var indicator = new PvdIndicator();
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PvdIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new PvdIndicator
|
||||
{
|
||||
PricePeriod = 5,
|
||||
VolumePeriod = 5,
|
||||
SmoothingPeriod = 2
|
||||
};
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double close = 100 + i * 0.5;
|
||||
double volume = 100000 + (i % 3 == 0 ? 20000 : -10000);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), close - 1, close + 1, close - 2, close, volume);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PvdIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new PvdIndicator
|
||||
{
|
||||
PricePeriod = 3,
|
||||
VolumePeriod = 3,
|
||||
SmoothingPeriod = 2
|
||||
};
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 110, 90, 105, 100000);
|
||||
}
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Add new bar
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(10), 105, 115, 100, 112, 80000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PvdIndicator_PositiveDivergence_PriceUpVolumeDown()
|
||||
{
|
||||
var indicator = new PvdIndicator
|
||||
{
|
||||
PricePeriod = 2,
|
||||
VolumePeriod = 2,
|
||||
SmoothingPeriod = 1
|
||||
};
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Establish baseline with stable prices and volumes
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 100, 100000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 100, 105, 95, 100, 100000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(2), 100, 105, 95, 100, 100000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
// Price up, volume down = positive divergence
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(3), 108, 112, 105, 110, 70000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(val > 0, $"PVD should be positive when price up and volume down: {val}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PvdIndicator_NegativeDivergence_PriceUpVolumeUp()
|
||||
{
|
||||
var indicator = new PvdIndicator
|
||||
{
|
||||
PricePeriod = 2,
|
||||
VolumePeriod = 2,
|
||||
SmoothingPeriod = 1
|
||||
};
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Establish baseline
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 100, 100000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 100, 105, 95, 100, 100000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(2), 100, 105, 95, 100, 100000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
// Price up, volume up = negative (same direction, no divergence)
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(3), 108, 112, 105, 110, 130000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(val < 0, $"PVD should be negative when price and volume move same direction: {val}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PvdIndicator_NoDivergence_StablePriceAndVolume()
|
||||
{
|
||||
var indicator = new PvdIndicator
|
||||
{
|
||||
PricePeriod = 2,
|
||||
VolumePeriod = 2,
|
||||
SmoothingPeriod = 1
|
||||
};
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// All bars with same values - no momentum
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 105, 95, 100, 100000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.Equal(0, val, precision: 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PvdIndicator_CustomPeriods_Applied()
|
||||
{
|
||||
var indicator = new PvdIndicator
|
||||
{
|
||||
PricePeriod = 5,
|
||||
VolumePeriod = 10,
|
||||
SmoothingPeriod = 3
|
||||
};
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double close = 100 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), close - 1, close + 2, close - 2, close, 100000 + i * 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,603 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class PvdTests
|
||||
{
|
||||
private readonly GBM _gbm;
|
||||
private readonly TBarSeries _bars;
|
||||
private const int TestDataLength = 1000;
|
||||
|
||||
public PvdTests()
|
||||
{
|
||||
_gbm = new GBM(seed: 42);
|
||||
_bars = new TBarSeries();
|
||||
for (int i = 0; i < TestDataLength; i++)
|
||||
{
|
||||
_bars.Add(_gbm.Next());
|
||||
}
|
||||
}
|
||||
|
||||
#region Constructor Tests
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultParameters_SetsCorrectValues()
|
||||
{
|
||||
var pvd = new Pvd();
|
||||
Assert.Equal("Pvd(14,14,3)", pvd.Name);
|
||||
Assert.Equal(17, pvd.WarmupPeriod); // max(14,14) + 3
|
||||
Assert.False(pvd.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_CustomPeriods_SetsCorrectValues()
|
||||
{
|
||||
var pvd = new Pvd(pricePeriod: 10, volumePeriod: 20, smoothingPeriod: 5);
|
||||
Assert.Equal("Pvd(10,20,5)", pvd.Name);
|
||||
Assert.Equal(25, pvd.WarmupPeriod); // max(10,20) + 5
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InvalidPricePeriod_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Pvd(pricePeriod: 0));
|
||||
Assert.Equal("pricePeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InvalidVolumePeriod_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Pvd(volumePeriod: 0));
|
||||
Assert.Equal("volumePeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InvalidSmoothingPeriod_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Pvd(smoothingPeriod: 0));
|
||||
Assert.Equal("smoothingPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NegativePeriod_ThrowsArgumentException()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Pvd(pricePeriod: -1));
|
||||
Assert.Throws<ArgumentException>(() => new Pvd(volumePeriod: -5));
|
||||
Assert.Throws<ArgumentException>(() => new Pvd(smoothingPeriod: -2));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Basic Calculation Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_ReturnsTValue()
|
||||
{
|
||||
var pvd = new Pvd();
|
||||
var bar = _bars[0];
|
||||
var result = pvd.Update(bar);
|
||||
Assert.IsType<TValue>(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_SetsLastProperty()
|
||||
{
|
||||
var pvd = new Pvd();
|
||||
var bar = _bars[0];
|
||||
var result = pvd.Update(bar);
|
||||
Assert.Equal(result.Value, pvd.Last.Value);
|
||||
Assert.Equal(result.Time, pvd.Last.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_SingleBar_ReturnsZero()
|
||||
{
|
||||
var pvd = new Pvd();
|
||||
var result = pvd.Update(_bars[0]);
|
||||
Assert.Equal(0.0, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_AfterWarmup_ReturnsFiniteValue()
|
||||
{
|
||||
var pvd = new Pvd(pricePeriod: 5, volumePeriod: 5, smoothingPeriod: 3);
|
||||
for (int i = 0; i < pvd.WarmupPeriod + 10; i++)
|
||||
{
|
||||
pvd.Update(_bars[i]);
|
||||
}
|
||||
Assert.True(double.IsFinite(pvd.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_DetectsPositiveDivergence()
|
||||
{
|
||||
// Create scenario: price up, volume down = positive divergence
|
||||
var pvd = new Pvd(pricePeriod: 2, volumePeriod: 2, smoothingPeriod: 1);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Establish baseline
|
||||
pvd.Update(new TBar(time, 100.0, 100.0, 100.0, 100.0, 1000.0), isNew: true);
|
||||
pvd.Update(new TBar(time.AddMinutes(1), 100.0, 100.0, 100.0, 100.0, 1000.0), isNew: true);
|
||||
pvd.Update(new TBar(time.AddMinutes(2), 100.0, 100.0, 100.0, 100.0, 1000.0), isNew: true);
|
||||
|
||||
// Price up, volume down
|
||||
pvd.Update(new TBar(time.AddMinutes(3), 110.0, 110.0, 110.0, 110.0, 800.0), isNew: true);
|
||||
|
||||
// Should show divergence (price up + volume down = positive)
|
||||
Assert.True(pvd.Last.Value > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_DetectsNegativeDivergence()
|
||||
{
|
||||
// Create scenario: price up, volume up = negative divergence (same direction)
|
||||
var pvd = new Pvd(pricePeriod: 2, volumePeriod: 2, smoothingPeriod: 1);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Establish baseline
|
||||
pvd.Update(new TBar(time, 100.0, 100.0, 100.0, 100.0, 1000.0), isNew: true);
|
||||
pvd.Update(new TBar(time.AddMinutes(1), 100.0, 100.0, 100.0, 100.0, 1000.0), isNew: true);
|
||||
pvd.Update(new TBar(time.AddMinutes(2), 100.0, 100.0, 100.0, 100.0, 1000.0), isNew: true);
|
||||
|
||||
// Price up, volume up
|
||||
pvd.Update(new TBar(time.AddMinutes(3), 110.0, 110.0, 110.0, 110.0, 1200.0), isNew: true);
|
||||
|
||||
// Should show negative divergence (same direction)
|
||||
Assert.True(pvd.Last.Value < 0);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region State Management Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewTrue_AdvancesState()
|
||||
{
|
||||
var pvd = new Pvd();
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
pvd.Update(_bars[i], isNew: true);
|
||||
}
|
||||
|
||||
_ = pvd.Last.Value;
|
||||
pvd.Update(_bars[20], isNew: true);
|
||||
// State should advance (can't easily verify internal state, but no exception means success)
|
||||
Assert.True(true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewFalse_RollsBackState()
|
||||
{
|
||||
var pvd = new Pvd();
|
||||
|
||||
for (int i = 0; i < 25; i++)
|
||||
{
|
||||
pvd.Update(_bars[i], isNew: true);
|
||||
}
|
||||
|
||||
_ = pvd.Last.Value;
|
||||
|
||||
// Update with isNew=false should rollback and recalculate
|
||||
pvd.Update(_bars[25], isNew: false);
|
||||
double valueAfterCorrection = pvd.Last.Value;
|
||||
|
||||
// Values may differ since we're using different input
|
||||
// The key is that state was rolled back properly
|
||||
Assert.True(double.IsFinite(valueAfterCorrection));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IterativeCorrections_RestoreState()
|
||||
{
|
||||
var pvd = new Pvd();
|
||||
|
||||
// Build up state
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
pvd.Update(_bars[i], isNew: true);
|
||||
}
|
||||
|
||||
_ = pvd.Last.Value;
|
||||
|
||||
// Make several corrections
|
||||
for (int c = 0; c < 5; c++)
|
||||
{
|
||||
pvd.Update(_bars[30], isNew: false);
|
||||
}
|
||||
|
||||
// Apply final new bar
|
||||
pvd.Update(_bars[30], isNew: true);
|
||||
double afterCorrections = pvd.Last.Value;
|
||||
|
||||
// After applying the same bar as new, should get same result
|
||||
Assert.True(double.IsFinite(afterCorrections));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var pvd = new Pvd();
|
||||
|
||||
// Build up state
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
pvd.Update(_bars[i], isNew: true);
|
||||
}
|
||||
|
||||
Assert.True(pvd.IsHot);
|
||||
|
||||
pvd.Reset();
|
||||
|
||||
Assert.False(pvd.IsHot);
|
||||
Assert.Equal(default, pvd.Last);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Warmup and IsHot Tests
|
||||
|
||||
[Fact]
|
||||
public void IsHot_FalseBeforeWarmup()
|
||||
{
|
||||
var pvd = new Pvd(pricePeriod: 5, volumePeriod: 5, smoothingPeriod: 3);
|
||||
|
||||
for (int i = 0; i < pvd.WarmupPeriod - 1; i++)
|
||||
{
|
||||
pvd.Update(_bars[i], isNew: true);
|
||||
Assert.False(pvd.IsHot);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_TrueAfterWarmup()
|
||||
{
|
||||
var pvd = new Pvd(pricePeriod: 5, volumePeriod: 5, smoothingPeriod: 3);
|
||||
|
||||
for (int i = 0; i < pvd.WarmupPeriod; i++)
|
||||
{
|
||||
pvd.Update(_bars[i], isNew: true);
|
||||
}
|
||||
|
||||
Assert.True(pvd.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_CalculatedCorrectly()
|
||||
{
|
||||
var pvd1 = new Pvd(pricePeriod: 10, volumePeriod: 5, smoothingPeriod: 3);
|
||||
Assert.Equal(13, pvd1.WarmupPeriod); // max(10,5) + 3
|
||||
|
||||
var pvd2 = new Pvd(pricePeriod: 5, volumePeriod: 20, smoothingPeriod: 5);
|
||||
Assert.Equal(25, pvd2.WarmupPeriod); // max(5,20) + 5
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region NaN and Infinity Handling Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_NaNInput_UsesLastValidValue()
|
||||
{
|
||||
var pvd = new Pvd(pricePeriod: 3, volumePeriod: 3, smoothingPeriod: 2);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Build up state
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
pvd.Update(new TBar(time.AddMinutes(i), 100.0 + i, 100.0 + i, 100.0 + i, 100.0 + i, 1000.0 + i * 10), isNew: true);
|
||||
}
|
||||
|
||||
_ = pvd.Last.Value;
|
||||
|
||||
// Update with NaN close - should use last valid
|
||||
pvd.Update(new TBar(time.AddMinutes(10), double.NaN, double.NaN, double.NaN, double.NaN, 1100.0), isNew: true);
|
||||
|
||||
// Should return NaN when close is NaN and no prior valid close
|
||||
// But since we have prior valid, it should use that
|
||||
Assert.True(double.IsFinite(pvd.Last.Value) || double.IsNaN(pvd.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_InfinityInput_UsesLastValidValue()
|
||||
{
|
||||
var pvd = new Pvd(pricePeriod: 3, volumePeriod: 3, smoothingPeriod: 2);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Build up state
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
pvd.Update(new TBar(time.AddMinutes(i), 100.0 + i, 100.0 + i, 100.0 + i, 100.0 + i, 1000.0 + i * 10), isNew: true);
|
||||
}
|
||||
|
||||
// Update with Infinity
|
||||
pvd.Update(new TBar(time.AddMinutes(10), double.PositiveInfinity, double.PositiveInfinity, double.PositiveInfinity, double.PositiveInfinity, 1100.0), isNew: true);
|
||||
|
||||
Assert.True(double.IsFinite(pvd.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_NegativeInfinityInput_UsesLastValidValue()
|
||||
{
|
||||
var pvd = new Pvd(pricePeriod: 3, volumePeriod: 3, smoothingPeriod: 2);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Build up state
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
pvd.Update(new TBar(time.AddMinutes(i), 100.0 + i, 100.0 + i, 100.0 + i, 100.0 + i, 1000.0 + i * 10), isNew: true);
|
||||
}
|
||||
|
||||
// Update with negative infinity
|
||||
pvd.Update(new TBar(time.AddMinutes(10), double.NegativeInfinity, double.NegativeInfinity, double.NegativeInfinity, double.NegativeInfinity, 1100.0), isNew: true);
|
||||
|
||||
Assert.True(double.IsFinite(pvd.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Span_HandlesNaN()
|
||||
{
|
||||
double[] closes = [100, 101, double.NaN, 103, 104];
|
||||
double[] volumes = [1000, 1100, 1200, 1300, 1400];
|
||||
double[] output = new double[5];
|
||||
|
||||
Pvd.Batch(closes.AsSpan(), volumes.AsSpan(), output.AsSpan(), pricePeriod: 2, volumePeriod: 2, smoothingPeriod: 1);
|
||||
|
||||
// Should handle NaN gracefully - result might be NaN or computed value
|
||||
Assert.True(output.Length == 5);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Mode Consistency Tests
|
||||
|
||||
[Fact]
|
||||
public void AllModes_ProduceSameResults()
|
||||
{
|
||||
int period = 10;
|
||||
|
||||
// Mode 1: Streaming Update
|
||||
var pvdStreaming = new Pvd(pricePeriod: period, volumePeriod: period, smoothingPeriod: 3);
|
||||
for (int i = 0; i < _bars.Count; i++)
|
||||
{
|
||||
pvdStreaming.Update(_bars[i], isNew: true);
|
||||
}
|
||||
var streamingResults = new List<double>();
|
||||
pvdStreaming.Reset();
|
||||
for (int i = 0; i < _bars.Count; i++)
|
||||
{
|
||||
streamingResults.Add(pvdStreaming.Update(_bars[i], isNew: true).Value);
|
||||
}
|
||||
|
||||
// Mode 2: Batch via instance Update(TBarSeries)
|
||||
var pvdBatch = new Pvd(pricePeriod: period, volumePeriod: period, smoothingPeriod: 3);
|
||||
var batchResult = pvdBatch.Update(_bars);
|
||||
|
||||
// Mode 3: Static Batch(TBarSeries)
|
||||
var staticResult = Pvd.Batch(_bars, pricePeriod: period, volumePeriod: period, smoothingPeriod: 3);
|
||||
|
||||
// Mode 4: Static Batch(Span)
|
||||
double[] closes = new double[_bars.Count];
|
||||
double[] volumes = new double[_bars.Count];
|
||||
double[] spanOutput = new double[_bars.Count];
|
||||
for (int i = 0; i < _bars.Count; i++)
|
||||
{
|
||||
closes[i] = _bars[i].Close;
|
||||
volumes[i] = _bars[i].Volume;
|
||||
}
|
||||
Pvd.Batch(closes.AsSpan(), volumes.AsSpan(), spanOutput.AsSpan(), pricePeriod: period, volumePeriod: period, smoothingPeriod: 3);
|
||||
|
||||
// Compare last 100 values (after warmup)
|
||||
int compareStart = _bars.Count - 100;
|
||||
for (int i = compareStart; i < _bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult[i].Value, staticResult[i].Value, precision: 10);
|
||||
Assert.Equal(batchResult[i].Value, spanOutput[i], precision: 10);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Span API Tests
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Span_ValidatesLengths()
|
||||
{
|
||||
double[] closes = [1, 2, 3, 4, 5];
|
||||
double[] volumes = [100, 200, 300]; // Wrong length
|
||||
double[] output = new double[5];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Pvd.Batch(closes.AsSpan(), volumes.AsSpan(), output.AsSpan()));
|
||||
Assert.Equal("volume", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Span_ValidatesOutputLength()
|
||||
{
|
||||
double[] closes = [1, 2, 3, 4, 5];
|
||||
double[] volumes = [100, 200, 300, 400, 500];
|
||||
double[] output = new double[3]; // Too short
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Pvd.Batch(closes.AsSpan(), volumes.AsSpan(), output.AsSpan()));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Span_ValidatesPricePeriod()
|
||||
{
|
||||
double[] closes = [1, 2, 3, 4, 5];
|
||||
double[] volumes = [100, 200, 300, 400, 500];
|
||||
double[] output = new double[5];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Pvd.Batch(closes.AsSpan(), volumes.AsSpan(), output.AsSpan(), pricePeriod: 0));
|
||||
Assert.Equal("pricePeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Span_ValidatesVolumePeriod()
|
||||
{
|
||||
double[] closes = [1, 2, 3, 4, 5];
|
||||
double[] volumes = [100, 200, 300, 400, 500];
|
||||
double[] output = new double[5];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Pvd.Batch(closes.AsSpan(), volumes.AsSpan(), output.AsSpan(), volumePeriod: 0));
|
||||
Assert.Equal("volumePeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Span_ValidatesSmoothingPeriod()
|
||||
{
|
||||
double[] closes = [1, 2, 3, 4, 5];
|
||||
double[] volumes = [100, 200, 300, 400, 500];
|
||||
double[] output = new double[5];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Pvd.Batch(closes.AsSpan(), volumes.AsSpan(), output.AsSpan(), smoothingPeriod: 0));
|
||||
Assert.Equal("smoothingPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Span_LargeData_NoStackOverflow()
|
||||
{
|
||||
int size = 10000;
|
||||
double[] closes = new double[size];
|
||||
double[] volumes = new double[size];
|
||||
double[] output = new double[size];
|
||||
|
||||
for (int i = 0; i < size; i++)
|
||||
{
|
||||
closes[i] = 100.0 + i * 0.01;
|
||||
volumes[i] = 1000000.0 + i * 100;
|
||||
}
|
||||
|
||||
// Should not stack overflow
|
||||
Pvd.Batch(closes.AsSpan(), volumes.AsSpan(), output.AsSpan());
|
||||
|
||||
Assert.True(double.IsFinite(output[size - 1]));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Chaining Tests
|
||||
|
||||
[Fact]
|
||||
public void Pub_FiresOnUpdate()
|
||||
{
|
||||
var pvd = new Pvd();
|
||||
int eventCount = 0;
|
||||
|
||||
pvd.Pub += (object? sender, in TValueEventArgs args) => eventCount++;
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
pvd.Update(_bars[i], isNew: true);
|
||||
}
|
||||
|
||||
Assert.Equal(10, eventCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Chaining_ProcessBars_Works()
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
var pvd = new Pvd(pricePeriod: 5, volumePeriod: 5, smoothingPeriod: 2);
|
||||
|
||||
// Process bars through indicator
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
pvd.Update(gbm.Next());
|
||||
}
|
||||
|
||||
Assert.True(pvd.IsHot);
|
||||
Assert.True(double.IsFinite(pvd.Last.Value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Edge Case Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_ZeroVolume_HandlesGracefully()
|
||||
{
|
||||
var pvd = new Pvd(pricePeriod: 2, volumePeriod: 2, smoothingPeriod: 1);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
pvd.Update(new TBar(time, 100.0, 100.0, 100.0, 100.0, 0.0), isNew: true);
|
||||
pvd.Update(new TBar(time.AddMinutes(1), 101.0, 101.0, 101.0, 101.0, 0.0), isNew: true);
|
||||
pvd.Update(new TBar(time.AddMinutes(2), 102.0, 102.0, 102.0, 102.0, 0.0), isNew: true);
|
||||
pvd.Update(new TBar(time.AddMinutes(3), 103.0, 103.0, 103.0, 103.0, 0.0), isNew: true);
|
||||
|
||||
Assert.True(double.IsFinite(pvd.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_NegativeVolume_TreatedAsZero()
|
||||
{
|
||||
var pvd = new Pvd(pricePeriod: 2, volumePeriod: 2, smoothingPeriod: 1);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
pvd.Update(new TBar(time, 100.0, 100.0, 100.0, 100.0, -1000.0), isNew: true);
|
||||
pvd.Update(new TBar(time.AddMinutes(1), 101.0, 101.0, 101.0, 101.0, -500.0), isNew: true);
|
||||
pvd.Update(new TBar(time.AddMinutes(2), 102.0, 102.0, 102.0, 102.0, 1000.0), isNew: true);
|
||||
pvd.Update(new TBar(time.AddMinutes(3), 103.0, 103.0, 103.0, 103.0, 1200.0), isNew: true);
|
||||
|
||||
Assert.True(double.IsFinite(pvd.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ConstantPriceAndVolume_ReturnsZero()
|
||||
{
|
||||
var pvd = new Pvd(pricePeriod: 3, volumePeriod: 3, smoothingPeriod: 1);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// All same values - no momentum in either direction
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
pvd.Update(new TBar(time.AddMinutes(i), 100.0, 100.0, 100.0, 100.0, 1000.0), isNew: true);
|
||||
}
|
||||
|
||||
// With no change, ROC is 0, so divergence should be 0
|
||||
Assert.Equal(0.0, pvd.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_MinimumPeriods_Works()
|
||||
{
|
||||
var pvd = new Pvd(pricePeriod: 1, volumePeriod: 1, smoothingPeriod: 1);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
pvd.Update(new TBar(time, 100.0, 100.0, 100.0, 100.0, 1000.0), isNew: true);
|
||||
pvd.Update(new TBar(time.AddMinutes(1), 105.0, 105.0, 105.0, 105.0, 900.0), isNew: true);
|
||||
|
||||
Assert.True(double.IsFinite(pvd.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_AsymmetricPeriods_Works()
|
||||
{
|
||||
var pvd = new Pvd(pricePeriod: 5, volumePeriod: 20, smoothingPeriod: 3);
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
pvd.Update(_bars[i], isNew: true);
|
||||
}
|
||||
|
||||
Assert.True(pvd.IsHot);
|
||||
Assert.True(double.IsFinite(pvd.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TValueInput_ThrowsNotSupported()
|
||||
{
|
||||
var pvd = new Pvd();
|
||||
var value = new TValue(DateTime.UtcNow, 100);
|
||||
|
||||
Assert.Throws<NotSupportedException>(() => pvd.Update(value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for PVD (Price Volume Divergence) indicator.
|
||||
/// PVD is a custom indicator not found in standard libraries (TA-Lib, Skender, Tulip, Ooples).
|
||||
/// Validation focuses on mathematical correctness and self-consistency.
|
||||
/// </summary>
|
||||
public class PvdValidationTests
|
||||
{
|
||||
private readonly TBarSeries _data;
|
||||
private const int TestDataLength = 500;
|
||||
private const double Tolerance = 1e-10;
|
||||
|
||||
public PvdValidationTests()
|
||||
{
|
||||
var gbm = new GBM(seed: 123);
|
||||
_data = new TBarSeries();
|
||||
for (int i = 0; i < TestDataLength; i++)
|
||||
{
|
||||
_data.Add(gbm.Next());
|
||||
}
|
||||
}
|
||||
|
||||
#region Self-Consistency Validation
|
||||
|
||||
[Fact]
|
||||
public void Pvd_StreamingVsBatch_ExactMatch()
|
||||
{
|
||||
int pricePeriod = 14;
|
||||
int volumePeriod = 14;
|
||||
int smoothingPeriod = 3;
|
||||
|
||||
// Streaming calculation
|
||||
var pvdStreaming = new Pvd(pricePeriod, volumePeriod, smoothingPeriod);
|
||||
var streamingResults = new List<double>();
|
||||
for (int i = 0; i < _data.Count; i++)
|
||||
{
|
||||
streamingResults.Add(pvdStreaming.Update(_data[i], isNew: true).Value);
|
||||
}
|
||||
|
||||
// Batch calculation (uses static Calculate which uses span internally)
|
||||
var batchResults = Pvd.Batch(_data, pricePeriod, volumePeriod, smoothingPeriod);
|
||||
|
||||
// Compare after full warmup (streaming and span may differ during warmup due to smoothing initialization)
|
||||
Assert.Equal(_data.Count, batchResults.Count);
|
||||
int warmup = Math.Max(pricePeriod, volumePeriod) + smoothingPeriod;
|
||||
for (int i = warmup; i < _data.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamingResults[i], batchResults[i].Value, precision: 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pvd_SpanVsBatch_ExactMatch()
|
||||
{
|
||||
int pricePeriod = 10;
|
||||
int volumePeriod = 10;
|
||||
int smoothingPeriod = 5;
|
||||
|
||||
// Extract data for span
|
||||
double[] closes = new double[_data.Count];
|
||||
double[] volumes = new double[_data.Count];
|
||||
for (int i = 0; i < _data.Count; i++)
|
||||
{
|
||||
closes[i] = _data[i].Close;
|
||||
volumes[i] = _data[i].Volume;
|
||||
}
|
||||
|
||||
// Span calculation
|
||||
double[] spanResults = new double[_data.Count];
|
||||
Pvd.Batch(closes.AsSpan(), volumes.AsSpan(), spanResults.AsSpan(), pricePeriod, volumePeriod, smoothingPeriod);
|
||||
|
||||
// Batch calculation
|
||||
var batchResults = Pvd.Batch(_data, pricePeriod, volumePeriod, smoothingPeriod);
|
||||
|
||||
// Compare after warmup
|
||||
int startCompare = Math.Max(pricePeriod, volumePeriod) + smoothingPeriod;
|
||||
for (int i = startCompare; i < _data.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResults[i].Value, spanResults[i], precision: 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pvd_DifferentPeriods_ProduceDifferentResults()
|
||||
{
|
||||
var pvd1 = Pvd.Batch(_data, pricePeriod: 5, volumePeriod: 5, smoothingPeriod: 3);
|
||||
var pvd2 = Pvd.Batch(_data, pricePeriod: 20, volumePeriod: 20, smoothingPeriod: 3);
|
||||
|
||||
// After warmup, values should differ
|
||||
int compareIdx = _data.Count - 1;
|
||||
Assert.NotEqual(pvd1[compareIdx].Value, pvd2[compareIdx].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pvd_AsymmetricPeriods_Work()
|
||||
{
|
||||
// Price period longer than volume period
|
||||
var pvd1 = Pvd.Batch(_data, pricePeriod: 20, volumePeriod: 5, smoothingPeriod: 3);
|
||||
|
||||
// Volume period longer than price period
|
||||
var pvd2 = Pvd.Batch(_data, pricePeriod: 5, volumePeriod: 20, smoothingPeriod: 3);
|
||||
|
||||
// Results should differ
|
||||
int compareIdx = _data.Count - 1;
|
||||
Assert.NotEqual(pvd1[compareIdx].Value, pvd2[compareIdx].Value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Mathematical Correctness Validation
|
||||
|
||||
[Fact]
|
||||
public void Pvd_KnownScenario_PositiveDivergence()
|
||||
{
|
||||
// Price up, volume down = positive divergence
|
||||
var bars = new TBarSeries();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Build baseline
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
bars.Add(new TBar(time.AddMinutes(i), 100.0, 100.0, 100.0, 100.0, 1000.0));
|
||||
}
|
||||
|
||||
// Price increasing, volume decreasing
|
||||
bars.Add(new TBar(time.AddMinutes(5), 110.0, 110.0, 110.0, 110.0, 800.0));
|
||||
|
||||
var result = Pvd.Batch(bars, pricePeriod: 2, volumePeriod: 2, smoothingPeriod: 1);
|
||||
|
||||
// Last value should be positive (divergence detected)
|
||||
Assert.True(result[^1].Value > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pvd_KnownScenario_NegativeDivergence()
|
||||
{
|
||||
// Price up, volume up = negative (same direction, no divergence)
|
||||
var bars = new TBarSeries();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Build baseline
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
bars.Add(new TBar(time.AddMinutes(i), 100.0, 100.0, 100.0, 100.0, 1000.0));
|
||||
}
|
||||
|
||||
// Price increasing, volume also increasing
|
||||
bars.Add(new TBar(time.AddMinutes(5), 110.0, 110.0, 110.0, 110.0, 1200.0));
|
||||
|
||||
var result = Pvd.Batch(bars, pricePeriod: 2, volumePeriod: 2, smoothingPeriod: 1);
|
||||
|
||||
// Last value should be negative (price and volume moving same direction)
|
||||
Assert.True(result[^1].Value < 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pvd_KnownScenario_NoMomentum()
|
||||
{
|
||||
// No price change = zero divergence
|
||||
var bars = new TBarSeries();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// All same values
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
bars.Add(new TBar(time.AddMinutes(i), 100.0, 100.0, 100.0, 100.0, 1000.0));
|
||||
}
|
||||
|
||||
var result = Pvd.Batch(bars, pricePeriod: 3, volumePeriod: 3, smoothingPeriod: 2);
|
||||
|
||||
// Should be zero (no momentum in either direction)
|
||||
Assert.Equal(0.0, result[^1].Value, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pvd_ManualCalculation_MatchesFormula()
|
||||
{
|
||||
// Create known data
|
||||
var bars = new TBarSeries();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
double[] closes = [100.0, 102.0, 104.0, 106.0, 105.0];
|
||||
double[] volumes = [1000.0, 1100.0, 900.0, 1200.0, 800.0];
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
bars.Add(new TBar(time.AddMinutes(i), closes[i], closes[i], closes[i], closes[i], volumes[i]));
|
||||
}
|
||||
|
||||
// Manual calculation for last bar with period=2, smoothing=1
|
||||
// Price ROC at index 4: (105 - 104) / 104 * 100 = 0.9615...
|
||||
// Volume ROC at index 4: (800 - 900) / 900 * 100 = -11.111...
|
||||
// Price momentum = 1 (positive)
|
||||
// Volume momentum = -1 (negative)
|
||||
// Magnitude = |0.9615| + |-11.111| = 12.073...
|
||||
// Divergence = 1 * -(-1) * 12.073 = 12.073... (positive: price up, volume down)
|
||||
|
||||
var result = Pvd.Batch(bars, pricePeriod: 2, volumePeriod: 2, smoothingPeriod: 1);
|
||||
|
||||
// Last value should be positive
|
||||
Assert.True(result[^1].Value > 0);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Smoothing Validation
|
||||
|
||||
[Fact]
|
||||
public void Pvd_SmoothingPeriod1_NoSmoothing()
|
||||
{
|
||||
var result1 = Pvd.Batch(_data, pricePeriod: 10, volumePeriod: 10, smoothingPeriod: 1);
|
||||
var result3 = Pvd.Batch(_data, pricePeriod: 10, volumePeriod: 10, smoothingPeriod: 3);
|
||||
|
||||
// Smoothing should make values different (and generally smoother)
|
||||
bool foundDifference = false;
|
||||
for (int i = 20; i < _data.Count; i++)
|
||||
{
|
||||
if (Math.Abs(result1[i].Value - result3[i].Value) > Tolerance)
|
||||
{
|
||||
foundDifference = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Assert.True(foundDifference);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pvd_HigherSmoothing_ReducesVolatility()
|
||||
{
|
||||
var result1 = Pvd.Batch(_data, pricePeriod: 10, volumePeriod: 10, smoothingPeriod: 1);
|
||||
var result10 = Pvd.Batch(_data, pricePeriod: 10, volumePeriod: 10, smoothingPeriod: 10);
|
||||
|
||||
// Calculate variance of last 100 values
|
||||
double variance1 = CalculateVariance(result1.Skip(400).Select(x => x.Value).ToArray());
|
||||
double variance10 = CalculateVariance(result10.Skip(400).Select(x => x.Value).ToArray());
|
||||
|
||||
// Higher smoothing should reduce variance
|
||||
Assert.True(variance10 <= variance1);
|
||||
}
|
||||
|
||||
private static double CalculateVariance(double[] values)
|
||||
{
|
||||
if (values.Length == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
double mean = values.Average();
|
||||
return values.Sum(v => (v - mean) * (v - mean)) / values.Length;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Edge Cases Validation
|
||||
|
||||
[Fact]
|
||||
public void Pvd_ZeroVolume_HandlesGracefully()
|
||||
{
|
||||
var bars = new TBarSeries();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Mix of zero and non-zero volumes
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double volume = i % 3 == 0 ? 0.0 : 1000.0 + i * 10;
|
||||
bars.Add(new TBar(time.AddMinutes(i), 100.0 + i, 101.0 + i, 99.0 + i, 100.5 + i, volume));
|
||||
}
|
||||
|
||||
var result = Pvd.Batch(bars, pricePeriod: 3, volumePeriod: 3, smoothingPeriod: 2);
|
||||
|
||||
// Should complete without errors
|
||||
Assert.Equal(20, result.Count);
|
||||
Assert.True(double.IsFinite(result[^1].Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pvd_SingleBar_ReturnsZero()
|
||||
{
|
||||
var bars = new TBarSeries();
|
||||
bars.Add(new TBar(DateTime.UtcNow, 100.0, 100.0, 100.0, 100.0, 1000.0));
|
||||
|
||||
var result = Pvd.Batch(bars, pricePeriod: 5, volumePeriod: 5, smoothingPeriod: 2);
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Equal(0.0, result[0].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pvd_LargeDataset_CompletesWithoutError()
|
||||
{
|
||||
var gbm = new GBM(seed: 456);
|
||||
var largeData = new TBarSeries();
|
||||
|
||||
for (int i = 0; i < 10000; i++)
|
||||
{
|
||||
largeData.Add(gbm.Next());
|
||||
}
|
||||
|
||||
var result = Pvd.Batch(largeData, pricePeriod: 14, volumePeriod: 14, smoothingPeriod: 3);
|
||||
|
||||
Assert.Equal(10000, result.Count);
|
||||
Assert.True(double.IsFinite(result[^1].Value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reset and State Validation
|
||||
|
||||
[Fact]
|
||||
public void Pvd_ResetAndRecalculate_SameResult()
|
||||
{
|
||||
var pvd = new Pvd(pricePeriod: 10, volumePeriod: 10, smoothingPeriod: 3);
|
||||
|
||||
// First pass
|
||||
for (int i = 0; i < _data.Count; i++)
|
||||
{
|
||||
pvd.Update(_data[i], isNew: true);
|
||||
}
|
||||
double firstResult = pvd.Last.Value;
|
||||
|
||||
// Reset and second pass
|
||||
pvd.Reset();
|
||||
for (int i = 0; i < _data.Count; i++)
|
||||
{
|
||||
pvd.Update(_data[i], isNew: true);
|
||||
}
|
||||
double secondResult = pvd.Last.Value;
|
||||
|
||||
Assert.Equal(firstResult, secondResult, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pvd_BarCorrection_ProducesConsistentResults()
|
||||
{
|
||||
var pvd = new Pvd(pricePeriod: 5, volumePeriod: 5, smoothingPeriod: 2);
|
||||
|
||||
// Process bars up to correction point
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
pvd.Update(_data[i], isNew: true);
|
||||
}
|
||||
|
||||
_ = pvd.Last.Value;
|
||||
|
||||
// Make multiple corrections
|
||||
for (int c = 0; c < 3; c++)
|
||||
{
|
||||
pvd.Update(_data[50], isNew: false);
|
||||
}
|
||||
|
||||
// Final value after corrections should be consistent
|
||||
pvd.Update(_data[50], isNew: true);
|
||||
double valueAfterCorrections = pvd.Last.Value;
|
||||
|
||||
Assert.True(double.IsFinite(valueAfterCorrections));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Documentation Validation
|
||||
|
||||
/// <summary>
|
||||
/// PVD is not implemented in major libraries.
|
||||
/// This test documents the validation status.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ValidationStatus_NotInMajorLibraries()
|
||||
{
|
||||
// PVD is a custom indicator created for QuanTAlib
|
||||
// Not found in: TA-Lib, Skender.Stock.Indicators, Tulip, OoplesFinance
|
||||
// Validation is performed through self-consistency tests and mathematical verification
|
||||
Assert.True(true, "PVD is a custom indicator - validated through self-consistency and math verification");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user