mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-24 21:48:03 +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,187 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class PviIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void PviIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new PviIndicator();
|
||||
|
||||
Assert.Equal("PVI - Positive Volume Index", indicator.Name);
|
||||
Assert.Equal(100, indicator.StartValue);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Equal(2, indicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PviIndicator_ShortName_ReflectsStartValue()
|
||||
{
|
||||
var indicator = new PviIndicator { StartValue = 1000 };
|
||||
Assert.Equal("PVI(1000)", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PviIndicator_MinHistoryDepths_EqualsTwo()
|
||||
{
|
||||
var indicator = new PviIndicator();
|
||||
|
||||
Assert.Equal(2, indicator.MinHistoryDepths);
|
||||
Assert.Equal(2, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PviIndicator_Initialize_CreatesInternalPvi()
|
||||
{
|
||||
var indicator = new PviIndicator();
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PviIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new PviIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
// Volume increasing pattern to trigger PVI changes
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i, 100000 + (i * 1000));
|
||||
|
||||
// Process update for each bar to simulate history loading
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Line series should have a value
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PviIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new PviIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i, 100000);
|
||||
}
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Add new bar with higher volume to trigger PVI update
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(30), 130, 140, 120, 135, 150000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PviIndicator_Value_IsPositive()
|
||||
{
|
||||
var indicator = new PviIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
// Create varying price and volume patterns
|
||||
double open = 100 + i;
|
||||
double high = open + 10 + (i % 5);
|
||||
double low = open - 5;
|
||||
double close = (i % 2 == 0) ? high - 1 : low + 1;
|
||||
// Alternate volume up/down to trigger PVI updates
|
||||
double volume = (i % 2 == 0) ? 100000 + (i * 1000) : 100000 - (i * 1000);
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), open, high, low, close, volume);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(val > 0, $"PVI value {val} should be positive");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PviIndicator_CustomStartValue_AffectsResult()
|
||||
{
|
||||
var indicator1 = new PviIndicator { StartValue = 100 };
|
||||
var indicator2 = new PviIndicator { StartValue = 1000 };
|
||||
|
||||
indicator1.Initialize();
|
||||
indicator2.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator1.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i, 100000 + (i * 2000));
|
||||
indicator2.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i, 100000 + (i * 2000));
|
||||
|
||||
indicator1.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator2.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val1 = indicator1.LinesSeries[0].GetValue(0);
|
||||
double val2 = indicator2.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Ratio should be approximately 10:1
|
||||
Assert.Equal(10.0, val2 / val1, 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PviIndicator_VolumeDecrease_PviUnchanged()
|
||||
{
|
||||
var indicator = new PviIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// First bar
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 100000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
double firstVal = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Second bar with lower volume - PVI should not change
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 110, 100, 108, 80000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
double secondVal = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.Equal(firstVal, secondVal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PviIndicator_VolumeIncrease_PviUpdates()
|
||||
{
|
||||
var indicator = new PviIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// First bar
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 100, 100000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
double firstVal = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Second bar with higher volume and higher close - PVI should increase
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 100, 110, 98, 108, 150000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
double secondVal = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(secondVal > firstVal, $"PVI should increase when volume increases and price rises: {secondVal} vs {firstVal}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,429 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class PviTests
|
||||
{
|
||||
private const double DefaultStartValue = 100.0;
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultParameters_CreatesValidIndicator()
|
||||
{
|
||||
var pvi = new Pvi();
|
||||
Assert.Equal($"Pvi({DefaultStartValue})", pvi.Name);
|
||||
Assert.Equal(2, pvi.WarmupPeriod);
|
||||
Assert.False(pvi.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_CustomParameters_CreatesValidIndicator()
|
||||
{
|
||||
var pvi = new Pvi(startValue: 1000);
|
||||
Assert.Equal("Pvi(1000)", pvi.Name);
|
||||
Assert.Equal(2, pvi.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InvalidStartValue_ThrowsArgumentException()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Pvi(startValue: 0));
|
||||
Assert.Throws<ArgumentException>(() => new Pvi(startValue: -100));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WithTBar_ReturnsValidValue()
|
||||
{
|
||||
var pvi = new Pvi();
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000000);
|
||||
var result = pvi.Update(bar);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.Equal(DefaultStartValue, result.Value); // First bar stays at start value
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WithTValue_ReturnsCurrentValue()
|
||||
{
|
||||
var pvi = new Pvi();
|
||||
var value = new TValue(DateTime.UtcNow, 100);
|
||||
var result = pvi.Update(value);
|
||||
// PVI without volume data returns current PVI value
|
||||
Assert.Equal(DefaultStartValue, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_VolumeIncreases_UpdatesPvi()
|
||||
{
|
||||
var pvi = new Pvi();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// First bar - establishes baseline
|
||||
pvi.Update(new TBar(time, 100, 105, 95, 100, 100000));
|
||||
|
||||
// Second bar with higher volume and higher close - PVI should increase
|
||||
var result = pvi.Update(new TBar(time.AddMinutes(1), 100, 108, 98, 105, 150000));
|
||||
|
||||
Assert.True(result.Value > DefaultStartValue, $"PVI should increase when volume increases and price rises, was {result.Value}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_VolumeDecreases_PviUnchanged()
|
||||
{
|
||||
var pvi = new Pvi();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// First bar - establishes baseline
|
||||
pvi.Update(new TBar(time, 100, 105, 95, 100, 100000));
|
||||
var firstPvi = pvi.Last.Value;
|
||||
|
||||
// Second bar with lower volume - PVI should stay the same
|
||||
var result = pvi.Update(new TBar(time.AddMinutes(1), 100, 108, 98, 105, 80000));
|
||||
|
||||
Assert.Equal(firstPvi, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_VolumeEqual_PviUnchanged()
|
||||
{
|
||||
var pvi = new Pvi();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// First bar
|
||||
pvi.Update(new TBar(time, 100, 105, 95, 100, 100000));
|
||||
var firstPvi = pvi.Last.Value;
|
||||
|
||||
// Second bar with equal volume
|
||||
var result = pvi.Update(new TBar(time.AddMinutes(1), 100, 108, 98, 105, 100000));
|
||||
|
||||
Assert.Equal(firstPvi, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ConsistentHighVolumeBullish_PviIncreases()
|
||||
{
|
||||
var pvi = new Pvi(startValue: 1000);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Build up with consistently higher volume and rising prices
|
||||
double volume = 100000;
|
||||
double price = 100;
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
pvi.Update(new TBar(time.AddMinutes(i), price, price + 2, price - 1, price, volume));
|
||||
volume *= 1.05; // Volume increasing each day
|
||||
price *= 1.02; // Price increasing each day
|
||||
}
|
||||
|
||||
Assert.True(pvi.Last.Value > 1000, $"PVI should be above start value after consistent bullish high-volume days, was {pvi.Last.Value}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ConsistentHighVolumeBearish_PviDecreases()
|
||||
{
|
||||
var pvi = new Pvi(startValue: 1000);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Build up with consistently higher volume and falling prices
|
||||
double volume = 100000;
|
||||
double price = 100;
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
pvi.Update(new TBar(time.AddMinutes(i), price, price + 2, price - 1, price, volume));
|
||||
volume *= 1.05; // Volume increasing each day
|
||||
price *= 0.98; // Price decreasing each day
|
||||
}
|
||||
|
||||
Assert.True(pvi.Last.Value < 1000, $"PVI should be below start value after consistent bearish high-volume days, was {pvi.Last.Value}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewTrue_AdvancesState()
|
||||
{
|
||||
var pvi = new Pvi();
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000000);
|
||||
var result1 = pvi.Update(bar1, isNew: true);
|
||||
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 105, 115, 95, 110, 1200000);
|
||||
var result2 = pvi.Update(bar2, isNew: true);
|
||||
|
||||
Assert.NotEqual(result1.Time, result2.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewFalse_UpdatesCurrentBar()
|
||||
{
|
||||
var pvi = new Pvi();
|
||||
var gbm = new GBM(seed: 42);
|
||||
|
||||
// Build up history
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
pvi.Update(gbm.Next(), isNew: true);
|
||||
}
|
||||
|
||||
// Get a new bar
|
||||
var bar1 = gbm.Next();
|
||||
var result1 = pvi.Update(bar1, isNew: true);
|
||||
|
||||
// Create a correction with different volume (higher to trigger PVI change)
|
||||
var bar2 = new TBar(bar1.Time, bar1.Open, bar1.High, bar1.Low, bar1.Close * 1.1, bar1.Volume * 1.5);
|
||||
var result2 = pvi.Update(bar2, isNew: false);
|
||||
|
||||
Assert.Equal(result1.Time, result2.Time);
|
||||
// Values may or may not differ depending on volume comparison
|
||||
Assert.True(double.IsFinite(result2.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IterativeCorrections_RestoresState()
|
||||
{
|
||||
var pvi = new Pvi();
|
||||
var gbm = new GBM(seed: 123);
|
||||
|
||||
// Build up history
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
pvi.Update(gbm.Next(), isNew: true);
|
||||
}
|
||||
|
||||
_ = pvi.Last.Value; // Capture state before new bar
|
||||
|
||||
// New bar
|
||||
var originalBar = gbm.Next();
|
||||
pvi.Update(originalBar, isNew: true);
|
||||
|
||||
// Correction with same values should restore similar state
|
||||
var correctionBar = originalBar;
|
||||
var correctedResult = pvi.Update(correctionBar, isNew: false);
|
||||
|
||||
Assert.True(double.IsFinite(correctedResult.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WarmupPeriod_IsHotBecomesTrueAfterWarmup()
|
||||
{
|
||||
var pvi = new Pvi();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
Assert.False(pvi.IsHot);
|
||||
|
||||
pvi.Update(new TBar(time, 100, 110, 90, 105, 100000), isNew: true);
|
||||
Assert.False(pvi.IsHot);
|
||||
|
||||
pvi.Update(new TBar(time.AddMinutes(1), 105, 115, 95, 110, 120000), isNew: true);
|
||||
Assert.True(pvi.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WithNaN_UsesLastValidValue()
|
||||
{
|
||||
var pvi = new Pvi();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Process some valid bars first
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
pvi.Update(new TBar(time.AddMinutes(i), 100, 105, 95, 102, 100000 + i * 1000));
|
||||
}
|
||||
|
||||
// Process bar with NaN volume
|
||||
var nanBar = new TBar(time.AddMinutes(10), 105, 110, 100, 108, double.NaN);
|
||||
var result = pvi.Update(nanBar);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ZeroVolume_HandlesGracefully()
|
||||
{
|
||||
var pvi = new Pvi();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
pvi.Update(new TBar(time, 100, 110, 90, 105, 100000));
|
||||
var result = pvi.Update(new TBar(time.AddMinutes(1), 105, 115, 95, 110, 0));
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var pvi = new Pvi();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
pvi.Update(new TBar(time.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i, 100000 + i * 5000), isNew: true);
|
||||
}
|
||||
|
||||
Assert.True(pvi.IsHot);
|
||||
Assert.True(double.IsFinite(pvi.Last.Value));
|
||||
|
||||
pvi.Reset();
|
||||
|
||||
Assert.False(pvi.IsHot);
|
||||
Assert.Equal(default, pvi.Last);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchCalculate_MatchesStreaming()
|
||||
{
|
||||
var bars = new TBarSeries();
|
||||
var gbm = new GBM(seed: 42);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
bars.Add(gbm.Next());
|
||||
}
|
||||
|
||||
// Streaming
|
||||
var pvi = new Pvi();
|
||||
var streamingValues = new List<double>();
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
streamingValues.Add(pvi.Update(bar).Value);
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batchResult = Pvi.Batch(bars);
|
||||
|
||||
Assert.Equal(bars.Count, batchResult.Count);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamingValues[i], batchResult[i].Value, 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanCalculate_MatchesStreaming()
|
||||
{
|
||||
var bars = new TBarSeries();
|
||||
var gbm = new GBM(seed: 42);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
bars.Add(gbm.Next());
|
||||
}
|
||||
|
||||
// Streaming
|
||||
var pvi = new Pvi();
|
||||
var streamingValues = new List<double>();
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
streamingValues.Add(pvi.Update(bar).Value);
|
||||
}
|
||||
|
||||
// Span
|
||||
var close = bars.Close.Values.ToArray();
|
||||
var volume = bars.Volume.Values.ToArray();
|
||||
var output = new double[bars.Count];
|
||||
|
||||
Pvi.Batch(close, volume, output);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamingValues[i], output[i], 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanCalculate_InvalidLengths_ThrowsArgumentException()
|
||||
{
|
||||
var close = new double[100];
|
||||
var volume = new double[99]; // Different length
|
||||
var output = new double[100];
|
||||
|
||||
Assert.Throws<ArgumentException>(() => Pvi.Batch(close, volume, output));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanCalculate_InvalidStartValue_ThrowsArgumentException()
|
||||
{
|
||||
var close = new double[100];
|
||||
var volume = new double[100];
|
||||
var output = new double[100];
|
||||
|
||||
Assert.Throws<ArgumentException>(() => Pvi.Batch(close, volume, output, startValue: 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanCalculate_EmptyInput_HandlesGracefully()
|
||||
{
|
||||
var close = Array.Empty<double>();
|
||||
var volume = Array.Empty<double>();
|
||||
var output = Array.Empty<double>();
|
||||
|
||||
Pvi.Batch(close, volume, output);
|
||||
|
||||
Assert.Empty(output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Event_PubFiresOnUpdate()
|
||||
{
|
||||
var pvi = new Pvi();
|
||||
TValue? receivedValue = null;
|
||||
bool receivedIsNew = false;
|
||||
|
||||
pvi.Pub += (object? sender, in TValueEventArgs args) =>
|
||||
{
|
||||
receivedValue = args.Value;
|
||||
receivedIsNew = args.IsNew;
|
||||
};
|
||||
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000000);
|
||||
pvi.Update(bar, isNew: true);
|
||||
|
||||
Assert.NotNull(receivedValue);
|
||||
Assert.True(receivedIsNew);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CustomStartValue_AffectsResults()
|
||||
{
|
||||
var bars = new TBarSeries();
|
||||
var gbm = new GBM(seed: 42);
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
bars.Add(gbm.Next());
|
||||
}
|
||||
|
||||
var pvi100 = new Pvi(startValue: 100);
|
||||
var pvi1000 = new Pvi(startValue: 1000);
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
pvi100.Update(bar);
|
||||
pvi1000.Update(bar);
|
||||
}
|
||||
|
||||
// Different start values should produce different final values
|
||||
Assert.NotEqual(pvi100.Last.Value, pvi1000.Last.Value);
|
||||
// The ratio should be approximately 10:1 (same proportional changes)
|
||||
Assert.Equal(10.0, pvi1000.Last.Value / pvi100.Last.Value, 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LargeDataset_HandlesWithoutError()
|
||||
{
|
||||
var bars = new TBarSeries();
|
||||
var gbm = new GBM(seed: 42);
|
||||
|
||||
for (int i = 0; i < 10000; i++)
|
||||
{
|
||||
bars.Add(gbm.Next());
|
||||
}
|
||||
|
||||
var pvi = new Pvi();
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
var result = pvi.Update(bar);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.True(result.Value > 0);
|
||||
}
|
||||
|
||||
Assert.True(pvi.IsHot);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Positive Volume Index validation tests.
|
||||
/// Cross-validated against: Tulip (pvi).
|
||||
/// Skender, TA-Lib, and Ooples do not have PVI implementations.
|
||||
/// Note: Tulip PVI starts at 0, QuanTAlib starts at a configurable value (default 100).
|
||||
/// Validation compares with matching start value of 0.
|
||||
/// </summary>
|
||||
public sealed class PviValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _data;
|
||||
private readonly ITestOutputHelper _output;
|
||||
private const double DefaultStartValue = 100.0;
|
||||
|
||||
public PviValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
_data = new ValidationTestData();
|
||||
_output = output;
|
||||
}
|
||||
|
||||
public void Dispose() { /* nothing to dispose */ }
|
||||
|
||||
#region Tulip Cross Validation Tests
|
||||
|
||||
[Fact]
|
||||
public void Validate_Tulip_PVI()
|
||||
{
|
||||
// Tulip pvi: inputs={close, volume}, options={}, outputs={pvi}
|
||||
var close = _data.Bars.Close.Values.ToArray();
|
||||
var volume = _data.Bars.Volume.Values.ToArray();
|
||||
|
||||
var tulipIndicator = Tulip.Indicators.pvi;
|
||||
double[][] inputs = { close, volume };
|
||||
double[] options = Array.Empty<double>();
|
||||
double[][] outputs = { new double[close.Length] };
|
||||
|
||||
tulipIndicator.Run(inputs, options, outputs);
|
||||
double[] tResult = outputs[0];
|
||||
int lookback = tulipIndicator.Start(options);
|
||||
|
||||
// QuanTAlib PVI — starts at 100 (Tulip starts at different value)
|
||||
// Compare bar-over-bar percentage changes since absolute values differ
|
||||
var pvi = new Pvi(DefaultStartValue);
|
||||
var qValues = new double[_data.Bars.Count];
|
||||
int idx = 0;
|
||||
foreach (var bar in _data.Bars)
|
||||
{
|
||||
qValues[idx++] = pvi.Update(bar).Value;
|
||||
}
|
||||
|
||||
_output.WriteLine($"Tulip PVI lookback: {lookback}, output length: {tResult.Length}");
|
||||
_output.WriteLine($"Tulip first 5: {string.Join(", ", tResult.Take(5).Select(v => v.ToString("F4", System.Globalization.CultureInfo.InvariantCulture)))}");
|
||||
_output.WriteLine($"QuanTAlib first 5: {string.Join(", ", qValues.Take(5).Select(v => v.ToString("F4", System.Globalization.CultureInfo.InvariantCulture)))}");
|
||||
|
||||
// Compare bar-over-bar percentage changes
|
||||
int compared = 0;
|
||||
int startIdx = lookback + 5; // skip warmup
|
||||
for (int i = startIdx; i < qValues.Length - 1 && (i - lookback + 1) < tResult.Length; i++)
|
||||
{
|
||||
int ti = i - lookback;
|
||||
double qPrev = qValues[i];
|
||||
double qCurr = qValues[i + 1];
|
||||
double tPrev = tResult[ti];
|
||||
double tCurr = tResult[ti + 1];
|
||||
|
||||
// Skip if previous values are near zero
|
||||
if (Math.Abs(qPrev) < 1e-10 || Math.Abs(tPrev) < 1e-10)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
double qPctChange = (qCurr - qPrev) / Math.Abs(qPrev);
|
||||
double tPctChange = (tCurr - tPrev) / Math.Abs(tPrev);
|
||||
|
||||
double diff = Math.Abs(qPctChange - tPctChange);
|
||||
|
||||
Assert.True(diff < 1e-6,
|
||||
$"Bar {i}: QuanTAlib pct={qPctChange:F8}, Tulip pct={tPctChange:F8}, Diff={diff:F8}");
|
||||
compared++;
|
||||
}
|
||||
|
||||
_output.WriteLine($"Tulip PVI: Compared {compared} bar-over-bar percentage changes");
|
||||
Assert.True(compared > 100, $"Should compare at least 100 values, got {compared}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
[Fact]
|
||||
public void Pvi_Matches_Skender()
|
||||
{
|
||||
// Skender does not have Positive Volume Index implementation
|
||||
Assert.True(true, "Skender does not have a Positive Volume Index implementation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pvi_Matches_Talib()
|
||||
{
|
||||
// TA-Lib does not have PVI/Positive Volume Index
|
||||
Assert.True(true, "TA-Lib does not have a Positive Volume Index implementation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pvi_Streaming_Matches_Batch()
|
||||
{
|
||||
// Streaming
|
||||
var pvi = new Pvi(DefaultStartValue);
|
||||
var streamingValues = new List<double>();
|
||||
foreach (var bar in _data.Bars)
|
||||
{
|
||||
streamingValues.Add(pvi.Update(bar).Value);
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batchResult = Pvi.Batch(_data.Bars, DefaultStartValue);
|
||||
var batchValues = batchResult.Values.ToArray();
|
||||
|
||||
ValidationHelper.VerifyData(streamingValues.ToArray(), batchValues, 0, 100, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pvi_Span_Matches_Streaming()
|
||||
{
|
||||
// Streaming
|
||||
var pvi = new Pvi(DefaultStartValue);
|
||||
var streamingValues = new List<double>();
|
||||
foreach (var bar in _data.Bars)
|
||||
{
|
||||
streamingValues.Add(pvi.Update(bar).Value);
|
||||
}
|
||||
|
||||
// Span
|
||||
var close = _data.Bars.Close.Values.ToArray();
|
||||
var volume = _data.Bars.Volume.Values.ToArray();
|
||||
var spanOutput = new double[close.Length];
|
||||
|
||||
Pvi.Batch(close, volume, spanOutput, DefaultStartValue);
|
||||
|
||||
ValidationHelper.VerifyData(streamingValues.ToArray(), spanOutput, 0, 100, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pvi_Different_StartValues_ProduceDifferentResults()
|
||||
{
|
||||
// Test with default start value
|
||||
var pvi1 = new Pvi(100);
|
||||
var values1 = new List<double>();
|
||||
foreach (var bar in _data.Bars)
|
||||
{
|
||||
values1.Add(pvi1.Update(bar).Value);
|
||||
}
|
||||
|
||||
// Test with different start value
|
||||
var pvi2 = new Pvi(1000);
|
||||
var values2 = new List<double>();
|
||||
foreach (var bar in _data.Bars)
|
||||
{
|
||||
values2.Add(pvi2.Update(bar).Value);
|
||||
}
|
||||
|
||||
// Values should differ (by factor of 10)
|
||||
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 start values should produce different results");
|
||||
|
||||
// Ratio should be approximately 10:1
|
||||
double ratio = values2[^1] / values1[^1];
|
||||
Assert.Equal(10.0, ratio, 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pvi_Values_OnlyChangeOnVolumeIncrease()
|
||||
{
|
||||
var pvi = new Pvi(DefaultStartValue);
|
||||
var results = new List<(double pviValue, double volume, double prevVolume)>();
|
||||
|
||||
double? prevVolume = null;
|
||||
foreach (var bar in _data.Bars)
|
||||
{
|
||||
pvi.Update(bar);
|
||||
if (prevVolume.HasValue)
|
||||
{
|
||||
results.Add((pvi.Last.Value, bar.Volume, prevVolume.Value));
|
||||
}
|
||||
prevVolume = bar.Volume;
|
||||
}
|
||||
|
||||
// Skip first few values (warmup)
|
||||
var stableResults = results.Skip(5).ToList();
|
||||
|
||||
// Verify we have valid data with volume decreases (volume patterns exist)
|
||||
int volumeDecreaseCount = 0;
|
||||
for (int i = 1; i < stableResults.Count; i++)
|
||||
{
|
||||
if (stableResults[i].volume <= stableResults[i].prevVolume)
|
||||
{
|
||||
volumeDecreaseCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Just verify we have valid data
|
||||
Assert.True(stableResults.Count > 0, "Should have stable PVI results");
|
||||
// Verify some volume decreases occurred (data has volume variation)
|
||||
Assert.True(volumeDecreaseCount >= 0, "Should have processed volume data");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pvi_ProducesReasonableValues()
|
||||
{
|
||||
var pvi = new Pvi(DefaultStartValue);
|
||||
var values = new List<double>();
|
||||
|
||||
foreach (var bar in _data.Bars)
|
||||
{
|
||||
values.Add(pvi.Update(bar).Value);
|
||||
}
|
||||
|
||||
// PVI should be positive
|
||||
Assert.True(values.All(v => v > 0), "PVI should always be positive");
|
||||
|
||||
// PVI should not have extreme values (within reasonable range)
|
||||
// With typical market data, PVI should stay within a reasonable range of start value
|
||||
Assert.True(values.All(v => v > DefaultStartValue * 0.1 && v < DefaultStartValue * 100),
|
||||
"PVI should be within reasonable range of start value");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pvi_FormulaVerification()
|
||||
{
|
||||
// Manual verification of PVI formula with known values
|
||||
var pvi = new Pvi(1000);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Bar 1: baseline (volume = 100000, close = 100)
|
||||
pvi.Update(new TBar(time, 100, 105, 95, 100, 100000));
|
||||
Assert.Equal(1000, pvi.Last.Value); // First bar, stays at start value
|
||||
|
||||
// Bar 2: volume increased (120000 > 100000), close increased (105)
|
||||
// Expected: PVI = 1000 × (105 / 100) = 1050
|
||||
pvi.Update(new TBar(time.AddMinutes(1), 100, 110, 95, 105, 120000));
|
||||
Assert.Equal(1050, pvi.Last.Value, 6);
|
||||
|
||||
// Bar 3: volume decreased (90000 < 120000), close increased (110)
|
||||
// Expected: PVI unchanged = 1050
|
||||
pvi.Update(new TBar(time.AddMinutes(2), 105, 115, 100, 110, 90000));
|
||||
Assert.Equal(1050, pvi.Last.Value, 6);
|
||||
|
||||
// Bar 4: volume increased (150000 > 90000), close decreased (100)
|
||||
// Expected: PVI = 1050 × (100 / 110) = 954.545...
|
||||
pvi.Update(new TBar(time.AddMinutes(3), 110, 112, 98, 100, 150000));
|
||||
Assert.Equal(1050 * (100.0 / 110.0), pvi.Last.Value, 6);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user