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 NviIndicatorTests
{
[Fact]
public void NviIndicator_Constructor_SetsDefaults()
{
var indicator = new NviIndicator();
Assert.Equal("NVI - Negative 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 NviIndicator_ShortName_ReflectsStartValue()
{
var indicator = new NviIndicator { StartValue = 1000 };
Assert.Equal("NVI(1000)", indicator.ShortName);
}
[Fact]
public void NviIndicator_MinHistoryDepths_EqualsTwo()
{
var indicator = new NviIndicator();
Assert.Equal(2, indicator.MinHistoryDepths);
Assert.Equal(2, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
[Fact]
public void NviIndicator_Initialize_CreatesInternalNvi()
{
var indicator = new NviIndicator();
// Initialize should not throw
indicator.Initialize();
// After init, line series should exist
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void NviIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new NviIndicator();
indicator.Initialize();
// Add historical data
var now = DateTime.UtcNow;
for (int i = 0; i < 30; i++)
{
// Volume decreasing pattern to trigger NVI 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 NviIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new NviIndicator();
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 lower volume to trigger NVI update
indicator.HistoricalData.AddBar(now.AddMinutes(30), 130, 140, 120, 135, 80000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
Assert.Equal(2, indicator.LinesSeries[0].Count);
}
[Fact]
public void NviIndicator_Value_IsPositive()
{
var indicator = new NviIndicator();
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 NVI 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, $"NVI value {val} should be positive");
}
[Fact]
public void NviIndicator_CustomStartValue_AffectsResult()
{
var indicator1 = new NviIndicator { StartValue = 100 };
var indicator2 = new NviIndicator { 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 NviIndicator_VolumeIncrease_NviUnchanged()
{
var indicator = new NviIndicator();
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 higher volume - NVI should not change
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 110, 100, 108, 150000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
double secondVal = indicator.LinesSeries[0].GetValue(0);
Assert.Equal(firstVal, secondVal);
}
[Fact]
public void NviIndicator_VolumeDecrease_NviUpdates()
{
var indicator = new NviIndicator();
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 lower volume and higher close - NVI should increase
indicator.HistoricalData.AddBar(now.AddMinutes(1), 100, 110, 98, 108, 80000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
double secondVal = indicator.LinesSeries[0].GetValue(0);
Assert.True(secondVal > firstVal, $"NVI should increase when volume decreases and price rises: {secondVal} vs {firstVal}");
}
}
+429
View File
@@ -0,0 +1,429 @@
using Xunit;
namespace QuanTAlib.Tests;
public class NviTests
{
private const double DefaultStartValue = 100.0;
[Fact]
public void Constructor_DefaultParameters_CreatesValidIndicator()
{
var nvi = new Nvi();
Assert.Equal($"Nvi({DefaultStartValue})", nvi.Name);
Assert.Equal(2, nvi.WarmupPeriod);
Assert.False(nvi.IsHot);
}
[Fact]
public void Constructor_CustomParameters_CreatesValidIndicator()
{
var nvi = new Nvi(startValue: 1000);
Assert.Equal("Nvi(1000)", nvi.Name);
Assert.Equal(2, nvi.WarmupPeriod);
}
[Fact]
public void Constructor_InvalidStartValue_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new Nvi(startValue: 0));
Assert.Throws<ArgumentException>(() => new Nvi(startValue: -100));
}
[Fact]
public void Update_WithTBar_ReturnsValidValue()
{
var nvi = new Nvi();
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000000);
var result = nvi.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 nvi = new Nvi();
var value = new TValue(DateTime.UtcNow, 100);
var result = nvi.Update(value);
// NVI without volume data returns current NVI value
Assert.Equal(DefaultStartValue, result.Value);
}
[Fact]
public void Update_VolumeDecreases_UpdatesNvi()
{
var nvi = new Nvi();
var time = DateTime.UtcNow;
// First bar - establishes baseline
nvi.Update(new TBar(time, 100, 105, 95, 100, 100000));
// Second bar with lower volume and higher close - NVI should increase
var result = nvi.Update(new TBar(time.AddMinutes(1), 100, 108, 98, 105, 80000));
Assert.True(result.Value > DefaultStartValue, $"NVI should increase when volume decreases and price rises, was {result.Value}");
}
[Fact]
public void Update_VolumeIncreases_NviUnchanged()
{
var nvi = new Nvi();
var time = DateTime.UtcNow;
// First bar - establishes baseline
nvi.Update(new TBar(time, 100, 105, 95, 100, 100000));
var firstNvi = nvi.Last.Value;
// Second bar with higher volume - NVI should stay the same
var result = nvi.Update(new TBar(time.AddMinutes(1), 100, 108, 98, 105, 150000));
Assert.Equal(firstNvi, result.Value);
}
[Fact]
public void Update_VolumeEqual_NviUnchanged()
{
var nvi = new Nvi();
var time = DateTime.UtcNow;
// First bar
nvi.Update(new TBar(time, 100, 105, 95, 100, 100000));
var firstNvi = nvi.Last.Value;
// Second bar with equal volume
var result = nvi.Update(new TBar(time.AddMinutes(1), 100, 108, 98, 105, 100000));
Assert.Equal(firstNvi, result.Value);
}
[Fact]
public void Update_ConsistentLowVolumeBullish_NviIncreases()
{
var nvi = new Nvi(startValue: 1000);
var time = DateTime.UtcNow;
// Build up with consistently lower volume and rising prices
double volume = 100000;
double price = 100;
for (int i = 0; i < 20; i++)
{
nvi.Update(new TBar(time.AddMinutes(i), price, price + 2, price - 1, price, volume));
volume *= 0.95; // Volume decreasing each day
price *= 1.02; // Price increasing each day
}
Assert.True(nvi.Last.Value > 1000, $"NVI should be above start value after consistent bullish low-volume days, was {nvi.Last.Value}");
}
[Fact]
public void Update_ConsistentLowVolumeBearish_NviDecreases()
{
var nvi = new Nvi(startValue: 1000);
var time = DateTime.UtcNow;
// Build up with consistently lower volume and falling prices
double volume = 100000;
double price = 100;
for (int i = 0; i < 20; i++)
{
nvi.Update(new TBar(time.AddMinutes(i), price, price + 2, price - 1, price, volume));
volume *= 0.95; // Volume decreasing each day
price *= 0.98; // Price decreasing each day
}
Assert.True(nvi.Last.Value < 1000, $"NVI should be below start value after consistent bearish low-volume days, was {nvi.Last.Value}");
}
[Fact]
public void Update_IsNewTrue_AdvancesState()
{
var nvi = new Nvi();
var bar1 = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000000);
var result1 = nvi.Update(bar1, isNew: true);
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 105, 115, 95, 110, 800000);
var result2 = nvi.Update(bar2, isNew: true);
Assert.NotEqual(result1.Time, result2.Time);
}
[Fact]
public void Update_IsNewFalse_UpdatesCurrentBar()
{
var nvi = new Nvi();
var gbm = new GBM(seed: 42);
// Build up history
for (int i = 0; i < 20; i++)
{
nvi.Update(gbm.Next(), isNew: true);
}
// Get a new bar
var bar1 = gbm.Next();
var result1 = nvi.Update(bar1, isNew: true);
// Create a correction with different volume (lower to trigger NVI change)
var bar2 = new TBar(bar1.Time, bar1.Open, bar1.High, bar1.Low, bar1.Close * 1.1, bar1.Volume * 0.5);
var result2 = nvi.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 nvi = new Nvi();
var gbm = new GBM(seed: 123);
// Build up history
for (int i = 0; i < 20; i++)
{
nvi.Update(gbm.Next(), isNew: true);
}
_ = nvi.Last.Value; // Capture state before new bar
// New bar
var originalBar = gbm.Next();
nvi.Update(originalBar, isNew: true);
// Correction with same values should restore similar state
var correctionBar = originalBar;
var correctedResult = nvi.Update(correctionBar, isNew: false);
Assert.True(double.IsFinite(correctedResult.Value));
}
[Fact]
public void Update_WarmupPeriod_IsHotBecomesTrueAfterWarmup()
{
var nvi = new Nvi();
var time = DateTime.UtcNow;
Assert.False(nvi.IsHot);
nvi.Update(new TBar(time, 100, 110, 90, 105, 100000), isNew: true);
Assert.False(nvi.IsHot);
nvi.Update(new TBar(time.AddMinutes(1), 105, 115, 95, 110, 80000), isNew: true);
Assert.True(nvi.IsHot);
}
[Fact]
public void Update_WithNaN_UsesLastValidValue()
{
var nvi = new Nvi();
var time = DateTime.UtcNow;
// Process some valid bars first
for (int i = 0; i < 10; i++)
{
nvi.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 = nvi.Update(nanBar);
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Update_ZeroVolume_HandlesGracefully()
{
var nvi = new Nvi();
var time = DateTime.UtcNow;
nvi.Update(new TBar(time, 100, 110, 90, 105, 100000));
var result = nvi.Update(new TBar(time.AddMinutes(1), 105, 115, 95, 110, 0));
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Reset_ClearsState()
{
var nvi = new Nvi();
var time = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
nvi.Update(new TBar(time.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i, 100000 - i * 5000), isNew: true);
}
Assert.True(nvi.IsHot);
Assert.True(double.IsFinite(nvi.Last.Value));
nvi.Reset();
Assert.False(nvi.IsHot);
Assert.Equal(default, nvi.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 nvi = new Nvi();
var streamingValues = new List<double>();
foreach (var bar in bars)
{
streamingValues.Add(nvi.Update(bar).Value);
}
// Batch
var batchResult = Nvi.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 nvi = new Nvi();
var streamingValues = new List<double>();
foreach (var bar in bars)
{
streamingValues.Add(nvi.Update(bar).Value);
}
// Span
var close = bars.Close.Values.ToArray();
var volume = bars.Volume.Values.ToArray();
var output = new double[bars.Count];
Nvi.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>(() => Nvi.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>(() => Nvi.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>();
Nvi.Batch(close, volume, output);
Assert.Empty(output);
}
[Fact]
public void Event_PubFiresOnUpdate()
{
var nvi = new Nvi();
TValue? receivedValue = null;
bool receivedIsNew = false;
nvi.Pub += (object? sender, in TValueEventArgs args) =>
{
receivedValue = args.Value;
receivedIsNew = args.IsNew;
};
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000000);
nvi.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 nvi100 = new Nvi(startValue: 100);
var nvi1000 = new Nvi(startValue: 1000);
foreach (var bar in bars)
{
nvi100.Update(bar);
nvi1000.Update(bar);
}
// Different start values should produce different final values
Assert.NotEqual(nvi100.Last.Value, nvi1000.Last.Value);
// The ratio should be approximately 10:1 (same proportional changes)
Assert.Equal(10.0, nvi1000.Last.Value / nvi100.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 nvi = new Nvi();
foreach (var bar in bars)
{
var result = nvi.Update(bar);
Assert.True(double.IsFinite(result.Value));
Assert.True(result.Value > 0);
}
Assert.True(nvi.IsHot);
}
}
@@ -0,0 +1,263 @@
using Xunit.Abstractions;
namespace QuanTAlib.Tests;
/// <summary>
/// Negative Volume Index validation tests.
/// Cross-validated against: Tulip (nvi).
/// Skender, TA-Lib, and Ooples do not have NVI implementations.
/// Note: Tulip NVI starts at 0, QuanTAlib starts at a configurable value (default 100).
/// Validation compares bar-to-bar percentage changes rather than absolute values.
/// </summary>
public sealed class NviValidationTests : IDisposable
{
private readonly ValidationTestData _data;
private readonly ITestOutputHelper _output;
private const double DefaultStartValue = 100.0;
public NviValidationTests(ITestOutputHelper output)
{
_data = new ValidationTestData();
_output = output;
}
public void Dispose() { /* nothing to dispose */ }
#region Tulip Cross Validation Tests
[Fact]
public void Validate_Tulip_NVI()
{
// Tulip nvi: inputs={close, volume}, options={}, outputs={nvi}
var close = _data.Bars.Close.Values.ToArray();
var volume = _data.Bars.Volume.Values.ToArray();
var tulipIndicator = Tulip.Indicators.nvi;
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 NVI — starts at 100 (Tulip starts at different value)
// Compare bar-over-bar percentage changes since absolute values differ
var nvi = new Nvi(DefaultStartValue);
var qValues = new double[_data.Bars.Count];
int idx = 0;
foreach (var bar in _data.Bars)
{
qValues[idx++] = nvi.Update(bar).Value;
}
_output.WriteLine($"Tulip NVI 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 NVI: Compared {compared} bar-over-bar percentage changes");
Assert.True(compared > 100, $"Should compare at least 100 values, got {compared}");
}
#endregion
[Fact]
public void Nvi_Matches_Skender()
{
// Skender does not have Negative Volume Index implementation
Assert.True(true, "Skender does not have a Negative Volume Index implementation");
}
[Fact]
public void Nvi_Matches_Talib()
{
// TA-Lib does not have NVI/Negative Volume Index
Assert.True(true, "TA-Lib does not have a Negative Volume Index implementation");
}
[Fact]
public void Nvi_Streaming_Matches_Batch()
{
// Streaming
var nvi = new Nvi(DefaultStartValue);
var streamingValues = new List<double>();
foreach (var bar in _data.Bars)
{
streamingValues.Add(nvi.Update(bar).Value);
}
// Batch
var batchResult = Nvi.Batch(_data.Bars, DefaultStartValue);
var batchValues = batchResult.Values.ToArray();
ValidationHelper.VerifyData(streamingValues.ToArray(), batchValues, 0, 100, 1e-9);
}
[Fact]
public void Nvi_Span_Matches_Streaming()
{
// Streaming
var nvi = new Nvi(DefaultStartValue);
var streamingValues = new List<double>();
foreach (var bar in _data.Bars)
{
streamingValues.Add(nvi.Update(bar).Value);
}
// Span
var close = _data.Bars.Close.Values.ToArray();
var volume = _data.Bars.Volume.Values.ToArray();
var spanOutput = new double[close.Length];
Nvi.Batch(close, volume, spanOutput, DefaultStartValue);
ValidationHelper.VerifyData(streamingValues.ToArray(), spanOutput, 0, 100, 1e-9);
}
[Fact]
public void Nvi_Different_StartValues_ProduceDifferentResults()
{
// Test with default start value
var nvi1 = new Nvi(100);
var values1 = new List<double>();
foreach (var bar in _data.Bars)
{
values1.Add(nvi1.Update(bar).Value);
}
// Test with different start value
var nvi2 = new Nvi(1000);
var values2 = new List<double>();
foreach (var bar in _data.Bars)
{
values2.Add(nvi2.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 Nvi_Values_OnlyChangeOnVolumeDecrease()
{
var nvi = new Nvi(DefaultStartValue);
var results = new List<(double nviValue, double volume, double prevVolume)>();
double? prevVolume = null;
foreach (var bar in _data.Bars)
{
nvi.Update(bar);
if (prevVolume.HasValue)
{
results.Add((nvi.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 increases (volume patterns exist)
int volumeIncreaseCount = 0;
for (int i = 1; i < stableResults.Count; i++)
{
if (stableResults[i].volume >= stableResults[i].prevVolume)
{
volumeIncreaseCount++;
}
}
// Just verify we have valid data
Assert.True(stableResults.Count > 0, "Should have stable NVI results");
// Verify some volume increases occurred (data has volume variation)
Assert.True(volumeIncreaseCount >= 0, "Should have processed volume data");
}
[Fact]
public void Nvi_ProducesReasonableValues()
{
var nvi = new Nvi(DefaultStartValue);
var values = new List<double>();
foreach (var bar in _data.Bars)
{
values.Add(nvi.Update(bar).Value);
}
// NVI should be positive
Assert.True(values.All(v => v > 0), "NVI should always be positive");
// NVI should not have extreme values (within reasonable range)
// With typical market data, NVI should stay within a reasonable range of start value
Assert.True(values.All(v => v > DefaultStartValue * 0.1 && v < DefaultStartValue * 100),
"NVI should be within reasonable range of start value");
}
[Fact]
public void Nvi_FormulaVerification()
{
// Manual verification of NVI formula with known values
var nvi = new Nvi(1000);
var time = DateTime.UtcNow;
// Bar 1: baseline (volume = 100000, close = 100)
nvi.Update(new TBar(time, 100, 105, 95, 100, 100000));
Assert.Equal(1000, nvi.Last.Value); // First bar, stays at start value
// Bar 2: volume decreased (80000 < 100000), close increased (105)
// Expected: NVI = 1000 × (105 / 100) = 1050
nvi.Update(new TBar(time.AddMinutes(1), 100, 110, 95, 105, 80000));
Assert.Equal(1050, nvi.Last.Value, 6);
// Bar 3: volume increased (90000 > 80000), close increased (110)
// Expected: NVI unchanged = 1050
nvi.Update(new TBar(time.AddMinutes(2), 105, 115, 100, 110, 90000));
Assert.Equal(1050, nvi.Last.Value, 6);
// Bar 4: volume decreased (70000 < 90000), close decreased (100)
// Expected: NVI = 1050 × (100 / 110) = 954.545...
nvi.Update(new TBar(time.AddMinutes(3), 110, 112, 98, 100, 70000));
Assert.Equal(1050 * (100.0 / 110.0), nvi.Last.Value, 6);
}
}