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:
Miha Kralj
2026-03-12 12:34:16 -07:00
parent 8937b0c0fa
commit 060649192f
1149 changed files with 1780 additions and 3316 deletions
+187
View File
@@ -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}");
}
}
+429
View File
@@ -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);
}
}