Files
Miha Kralj 060649192f 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
2026-03-12 12:34:16 -07:00

521 lines
16 KiB
C#

using Xunit;
namespace QuanTAlib.Tests;
public class PvoTests
{
private const int DefaultFastPeriod = 12;
private const int DefaultSlowPeriod = 26;
private const int DefaultSignalPeriod = 9;
[Fact]
public void Constructor_DefaultParameters_CreatesValidIndicator()
{
var pvo = new Pvo();
Assert.Equal($"Pvo({DefaultFastPeriod},{DefaultSlowPeriod},{DefaultSignalPeriod})", pvo.Name);
Assert.Equal(DefaultSlowPeriod, pvo.WarmupPeriod);
Assert.False(pvo.IsHot);
}
[Fact]
public void Constructor_CustomParameters_CreatesValidIndicator()
{
var pvo = new Pvo(fastPeriod: 5, slowPeriod: 10, signalPeriod: 3);
Assert.Equal("Pvo(5,10,3)", pvo.Name);
Assert.Equal(10, pvo.WarmupPeriod);
}
[Fact]
public void Constructor_InvalidFastPeriod_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new Pvo(fastPeriod: 0));
Assert.Throws<ArgumentException>(() => new Pvo(fastPeriod: -1));
}
[Fact]
public void Constructor_InvalidSlowPeriod_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new Pvo(slowPeriod: 0));
Assert.Throws<ArgumentException>(() => new Pvo(slowPeriod: -1));
}
[Fact]
public void Constructor_InvalidSignalPeriod_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new Pvo(signalPeriod: 0));
Assert.Throws<ArgumentException>(() => new Pvo(signalPeriod: -1));
}
[Fact]
public void Constructor_FastNotLessThanSlow_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new Pvo(fastPeriod: 26, slowPeriod: 26));
Assert.Throws<ArgumentException>(() => new Pvo(fastPeriod: 30, slowPeriod: 26));
}
[Fact]
public void Update_WithTBar_ReturnsValidValue()
{
var pvo = new Pvo();
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000000);
var result = pvo.Update(bar);
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Update_WithTValue_ReturnsValidValue()
{
var pvo = new Pvo();
var value = new TValue(DateTime.UtcNow, 1000000);
var result = pvo.Update(value);
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Update_VolumeIncrease_ReturnsPositiveValue()
{
var pvo = new Pvo(fastPeriod: 3, slowPeriod: 5, signalPeriod: 3);
var time = DateTime.UtcNow;
// Constant volume first
for (int i = 0; i < 50; i++)
{
pvo.Update(new TBar(time.AddMinutes(i), 100, 105, 95, 102, 100000));
}
// Then increasing volume - fast EMA will be higher than slow
for (int i = 50; i < 100; i++)
{
pvo.Update(new TBar(time.AddMinutes(i), 100, 105, 95, 102, 100000 + (i - 50) * 50000));
}
// Fast EMA responds quicker to volume increase, should be positive
Assert.True(pvo.Last.Value > 0, "PVO should be positive when volume is increasing");
}
[Fact]
public void Update_VolumeDecrease_ReturnsNegativeValue()
{
var pvo = new Pvo(fastPeriod: 3, slowPeriod: 5, signalPeriod: 3);
var time = DateTime.UtcNow;
// High constant volume first
for (int i = 0; i < 50; i++)
{
pvo.Update(new TBar(time.AddMinutes(i), 100, 105, 95, 102, 1000000));
}
// Then decreasing volume - fast EMA will be lower than slow
for (int i = 50; i < 100; i++)
{
pvo.Update(new TBar(time.AddMinutes(i), 100, 105, 95, 102, 1000000 - (i - 50) * 15000));
}
// Fast EMA responds quicker to volume decrease, should be negative
Assert.True(pvo.Last.Value < 0, "PVO should be negative when volume is decreasing");
}
[Fact]
public void Update_IsNewTrue_AdvancesState()
{
var pvo = new Pvo();
var bar1 = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000000);
var result1 = pvo.Update(bar1, isNew: true);
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 105, 115, 95, 110, 1100000);
var result2 = pvo.Update(bar2, isNew: true);
Assert.NotEqual(result1.Time, result2.Time);
}
[Fact]
public void Update_IsNewFalse_UpdatesCurrentBar()
{
var pvo = new Pvo();
var time = DateTime.UtcNow;
var bar1 = new TBar(time, 100, 110, 90, 105, 1000000);
pvo.Update(bar1, isNew: true);
var bar2 = new TBar(time.AddMinutes(1), 105, 115, 95, 110, 1100000);
var result1 = pvo.Update(bar2, isNew: true);
// Update same bar with different volume
var bar2Updated = new TBar(time.AddMinutes(1), 105, 120, 95, 118, 2000000);
var result2 = pvo.Update(bar2Updated, isNew: false);
Assert.Equal(result1.Time, result2.Time);
Assert.NotEqual(result1.Value, result2.Value);
}
[Fact]
public void Update_IterativeCorrections_RestoresState()
{
var pvo = new Pvo(fastPeriod: 5, slowPeriod: 10, signalPeriod: 5);
var time = DateTime.UtcNow;
// Build up state
for (int i = 0; i < 15; i++)
{
pvo.Update(new TBar(time.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i, 100000 + i * 10000), isNew: true);
}
// New bar
var originalBar = new TBar(time.AddMinutes(15), 120, 130, 110, 125, 250000);
var originalResult = pvo.Update(originalBar, isNew: true);
// Correction with different volume
var correctionBar = new TBar(time.AddMinutes(15), 110, 150, 90, 140, 500000);
var correctedResult = pvo.Update(correctionBar, isNew: false);
Assert.NotEqual(originalResult.Value, correctedResult.Value);
Assert.True(double.IsFinite(correctedResult.Value));
}
[Fact]
public void Update_WarmupPeriod_IsHotBecomesTrueAfterWarmup()
{
var pvo = new Pvo(fastPeriod: 3, slowPeriod: 5, signalPeriod: 3);
var time = DateTime.UtcNow;
Assert.False(pvo.IsHot);
// Feed many bars until compensators decay below threshold (1e-10)
for (int i = 0; i < 100; i++)
{
pvo.Update(new TBar(time.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i, 100000), isNew: true);
}
Assert.True(pvo.IsHot);
}
[Fact]
public void Update_WithNaN_UsesLastValidValue()
{
var pvo = new Pvo(fastPeriod: 3, slowPeriod: 5, signalPeriod: 3);
var time = DateTime.UtcNow;
// Process some valid bars first
for (int i = 0; i < 10; i++)
{
pvo.Update(new TBar(time.AddMinutes(i), 100, 105, 95, 102, 100000));
}
// Process bar with NaN volume
var nanBar = new TBar(time.AddMinutes(10), 105, 110, 100, 108, double.NaN);
var result = pvo.Update(nanBar);
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Update_ZeroVolume_HandlesGracefully()
{
var pvo = new Pvo(fastPeriod: 3, slowPeriod: 5, signalPeriod: 3);
var time = DateTime.UtcNow;
pvo.Update(new TBar(time, 100, 110, 90, 105, 100000));
var result = pvo.Update(new TBar(time.AddMinutes(1), 105, 115, 95, 110, 0));
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Signal_CalculatedAlongsidePvo()
{
var pvo = new Pvo(fastPeriod: 3, slowPeriod: 5, signalPeriod: 3);
var time = DateTime.UtcNow;
for (int i = 0; i < 20; i++)
{
pvo.Update(new TBar(time.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i, 100000 + i * 10000));
}
Assert.True(double.IsFinite(pvo.Signal.Value));
Assert.Equal(pvo.Last.Time, pvo.Signal.Time);
}
[Fact]
public void Histogram_CalculatedCorrectly()
{
var pvo = new Pvo(fastPeriod: 3, slowPeriod: 5, signalPeriod: 3);
var time = DateTime.UtcNow;
for (int i = 0; i < 20; i++)
{
pvo.Update(new TBar(time.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i, 100000 + i * 10000));
}
Assert.True(double.IsFinite(pvo.Histogram.Value));
Assert.Equal(pvo.Last.Value - pvo.Signal.Value, pvo.Histogram.Value, 10);
}
[Fact]
public void Reset_ClearsState()
{
var pvo = new Pvo(fastPeriod: 3, slowPeriod: 5, signalPeriod: 3);
var time = DateTime.UtcNow;
// Process many bars until IsHot becomes true
for (int i = 0; i < 100; i++)
{
pvo.Update(new TBar(time.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i, 100000), isNew: true);
}
Assert.True(double.IsFinite(pvo.Last.Value));
pvo.Reset();
Assert.False(pvo.IsHot);
Assert.Equal(default, pvo.Last);
Assert.Equal(default, pvo.Signal);
Assert.Equal(default, pvo.Histogram);
}
[Fact]
public void UpdateWithSignal_ReturnsAllSeries()
{
var bars = new TBarSeries();
var gbm = new GBM(seed: 42);
for (int i = 0; i < 100; i++)
{
bars.Add(gbm.Next());
}
var pvo = new Pvo();
var (pvoSeries, signalSeries, histogramSeries) = pvo.UpdateWithSignal(bars);
Assert.Equal(bars.Count, pvoSeries.Count);
Assert.Equal(bars.Count, signalSeries.Count);
Assert.Equal(bars.Count, histogramSeries.Count);
// Verify values are finite
for (int i = 0; i < bars.Count; i++)
{
Assert.True(double.IsFinite(pvoSeries[i].Value));
Assert.True(double.IsFinite(signalSeries[i].Value));
Assert.True(double.IsFinite(histogramSeries[i].Value));
}
}
[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 pvo = new Pvo();
var streamingValues = new List<double>();
foreach (var bar in bars)
{
streamingValues.Add(pvo.Update(bar).Value);
}
// Batch
var batchResult = Pvo.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 pvo = new Pvo();
var streamingPvo = new List<double>();
var streamingSignal = new List<double>();
var streamingHistogram = new List<double>();
foreach (var bar in bars)
{
pvo.Update(bar);
streamingPvo.Add(pvo.Last.Value);
streamingSignal.Add(pvo.Signal.Value);
streamingHistogram.Add(pvo.Histogram.Value);
}
// Span
var volume = bars.Volume.Values.ToArray();
var spanPvo = new double[bars.Count];
var spanSignal = new double[bars.Count];
var spanHistogram = new double[bars.Count];
Pvo.Batch(volume, spanPvo, spanSignal, spanHistogram);
for (int i = 0; i < bars.Count; i++)
{
Assert.Equal(streamingPvo[i], spanPvo[i], 10);
Assert.Equal(streamingSignal[i], spanSignal[i], 10);
Assert.Equal(streamingHistogram[i], spanHistogram[i], 10);
}
}
[Fact]
public void SpanCalculate_InvalidLengths_ThrowsArgumentException()
{
var volume = new double[100];
var output = new double[99]; // Different length
var signal = new double[100];
var histogram = new double[100];
Assert.Throws<ArgumentException>(() => Pvo.Batch(volume, output, signal, histogram));
}
[Fact]
public void SpanCalculate_InvalidFastPeriod_ThrowsArgumentException()
{
var volume = new double[100];
var output = new double[100];
var signal = new double[100];
var histogram = new double[100];
Assert.Throws<ArgumentException>(() => Pvo.Batch(volume, output, signal, histogram, fastPeriod: 0));
}
[Fact]
public void SpanCalculate_InvalidSlowPeriod_ThrowsArgumentException()
{
var volume = new double[100];
var output = new double[100];
var signal = new double[100];
var histogram = new double[100];
Assert.Throws<ArgumentException>(() => Pvo.Batch(volume, output, signal, histogram, slowPeriod: 0));
}
[Fact]
public void SpanCalculate_InvalidSignalPeriod_ThrowsArgumentException()
{
var volume = new double[100];
var output = new double[100];
var signal = new double[100];
var histogram = new double[100];
Assert.Throws<ArgumentException>(() => Pvo.Batch(volume, output, signal, histogram, signalPeriod: 0));
}
[Fact]
public void SpanCalculate_FastNotLessThanSlow_ThrowsArgumentException()
{
var volume = new double[100];
var output = new double[100];
var signal = new double[100];
var histogram = new double[100];
Assert.Throws<ArgumentException>(() => Pvo.Batch(volume, output, signal, histogram, fastPeriod: 26, slowPeriod: 26));
}
[Fact]
public void SpanCalculate_EmptyInput_HandlesGracefully()
{
var volume = Array.Empty<double>();
var output = Array.Empty<double>();
var signal = Array.Empty<double>();
var histogram = Array.Empty<double>();
// Should not throw
Pvo.Batch(volume, output, signal, histogram);
Assert.Empty(output);
}
[Fact]
public void Event_PubFiresOnUpdate()
{
var pvo = new Pvo();
TValue? receivedValue = null;
bool receivedIsNew = false;
pvo.Pub += (object? sender, in TValueEventArgs args) =>
{
receivedValue = args.Value;
receivedIsNew = args.IsNew;
};
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000000);
pvo.Update(bar, isNew: true);
Assert.NotNull(receivedValue);
Assert.True(receivedIsNew);
}
[Fact]
public void CustomPeriods_AffectsResults()
{
var bars = new TBarSeries();
var gbm = new GBM(seed: 42);
for (int i = 0; i < 100; i++)
{
bars.Add(gbm.Next());
}
var pvo1 = new Pvo(fastPeriod: 5, slowPeriod: 10, signalPeriod: 3);
var pvo2 = new Pvo(fastPeriod: 10, slowPeriod: 20, signalPeriod: 5);
foreach (var bar in bars)
{
pvo1.Update(bar);
pvo2.Update(bar);
}
// Different periods should produce different results
Assert.NotEqual(pvo1.Last.Value, pvo2.Last.Value);
}
[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 pvo = new Pvo();
foreach (var bar in bars)
{
var result = pvo.Update(bar);
Assert.True(double.IsFinite(result.Value));
}
Assert.True(pvo.IsHot);
}
[Fact]
public void ConstantVolume_PvoIsZero()
{
var pvo = new Pvo(fastPeriod: 3, slowPeriod: 5, signalPeriod: 3);
var time = DateTime.UtcNow;
// With constant volume, fast and slow EMAs should converge to same value
// resulting in PVO = 0
for (int i = 0; i < 200; i++)
{
pvo.Update(new TBar(time.AddMinutes(i), 100, 105, 95, 102, 100000));
}
// After warmup with constant volume, PVO should be very close to 0
Assert.True(Math.Abs(pvo.Last.Value) < 0.01, $"PVO should be ~0 with constant volume, but was {pvo.Last.Value}");
}
}