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

603 lines
16 KiB
C#

using Xunit;
namespace QuanTAlib.Tests;
public class VoTests
{
private const double Tolerance = 1e-10;
private readonly GBM _gbm;
private readonly TBarSeries _bars;
public VoTests()
{
_gbm = new GBM(seed: 42);
_bars = _gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
}
#region Constructor Tests
[Fact]
public void Constructor_DefaultPeriods_SetsExpectedValues()
{
var vo = new Vo();
Assert.Equal("Vo(5,10,10)", vo.Name);
Assert.Equal(10, vo.WarmupPeriod);
}
[Fact]
public void Constructor_CustomPeriods_SetsExpectedValues()
{
var vo = new Vo(shortPeriod: 3, longPeriod: 7, signalPeriod: 5);
Assert.Equal("Vo(3,7,5)", vo.Name);
Assert.Equal(7, vo.WarmupPeriod);
}
[Fact]
public void Constructor_ShortPeriodLessThan1_ThrowsArgumentException()
{
var ex = Assert.Throws<ArgumentException>(() => new Vo(shortPeriod: 0));
Assert.Equal("shortPeriod", ex.ParamName);
}
[Fact]
public void Constructor_LongPeriodLessThan1_ThrowsArgumentException()
{
var ex = Assert.Throws<ArgumentException>(() => new Vo(shortPeriod: 2, longPeriod: 0));
Assert.Equal("longPeriod", ex.ParamName);
}
[Fact]
public void Constructor_ShortPeriodGreaterOrEqualLongPeriod_ThrowsArgumentException()
{
var ex = Assert.Throws<ArgumentException>(() => new Vo(shortPeriod: 10, longPeriod: 10));
Assert.Equal("shortPeriod", ex.ParamName);
ex = Assert.Throws<ArgumentException>(() => new Vo(shortPeriod: 15, longPeriod: 10));
Assert.Equal("shortPeriod", ex.ParamName);
}
[Fact]
public void Constructor_SignalPeriodLessThan1_ThrowsArgumentException()
{
var ex = Assert.Throws<ArgumentException>(() => new Vo(shortPeriod: 5, longPeriod: 10, signalPeriod: 0));
Assert.Equal("signalPeriod", ex.ParamName);
}
#endregion
#region Basic Calculation Tests
[Fact]
public void Update_ReturnsTValue()
{
var vo = new Vo();
var result = vo.Update(_bars[0]);
Assert.IsType<TValue>(result);
}
[Fact]
public void Update_AccessesLastAndSignal()
{
var vo = new Vo();
vo.Update(_bars[0]);
Assert.Equal(vo.Last.Value, vo.Update(_bars[0], isNew: false).Value);
_ = vo.Signal; // Access signal property
}
[Fact]
public void Update_SameVolumes_ReturnsZero()
{
var vo = new Vo(shortPeriod: 2, longPeriod: 4, signalPeriod: 2);
var now = DateTime.UtcNow;
// All same volumes should result in VO = 0
for (int i = 0; i < 10; i++)
{
var bar = new TBar(now.AddMinutes(i), 100, 100, 100, 100, 1000);
vo.Update(bar, isNew: true);
}
Assert.Equal(0.0, vo.Last.Value, Tolerance);
}
[Fact]
public void Update_IncreasingVolumes_ReturnsPositive()
{
var vo = new Vo(shortPeriod: 2, longPeriod: 4, signalPeriod: 2);
var now = DateTime.UtcNow;
// Create a pattern where short MA > long MA at the end
// Volumes: 100, 100, 100, 100, 500, 1000
// At bar 5 (index 5): short SMA (2) = (500+1000)/2 = 750
// long SMA (4) = (100+100+500+1000)/4 = 425
// VO = ((750 - 425) / 425) * 100 = 76.47% (positive)
double[] volumes = [100, 100, 100, 100, 500, 1000];
for (int i = 0; i < volumes.Length; i++)
{
var bar = new TBar(now.AddMinutes(i), 100, 100, 100, 100, volumes[i]);
vo.Update(bar, isNew: true);
}
Assert.True(vo.Last.Value > 0, $"Expected positive VO but got {vo.Last.Value}");
}
[Fact]
public void Update_DecreasingVolumes_ReturnsNegative()
{
var vo = new Vo(shortPeriod: 2, longPeriod: 4, signalPeriod: 2);
var now = DateTime.UtcNow;
// Create a pattern where short MA < long MA at the end
// Volumes: 1000, 1000, 1000, 1000, 500, 100
// At bar 5 (index 5): short SMA (2) = (500+100)/2 = 300
// long SMA (4) = (1000+1000+500+100)/4 = 650
// VO = ((300 - 650) / 650) * 100 = -53.85% (negative)
double[] volumes = [1000, 1000, 1000, 1000, 500, 100];
for (int i = 0; i < volumes.Length; i++)
{
var bar = new TBar(now.AddMinutes(i), 100, 100, 100, 100, volumes[i]);
vo.Update(bar, isNew: true);
}
Assert.True(vo.Last.Value < 0, $"Expected negative VO but got {vo.Last.Value}");
}
#endregion
#region State Management Tests
[Fact]
public void IsNew_True_AdvancesState()
{
var vo = new Vo(shortPeriod: 2, longPeriod: 4, signalPeriod: 2);
var now = DateTime.UtcNow;
// Feed enough bars to get past warmup with varying volumes
// to ensure state advances (index changes)
double[] volumes = [100, 200, 300, 400, 500];
for (int i = 0; i < volumes.Length; i++)
{
var bar = new TBar(now.AddMinutes(i), 100, 100, 100, 100, volumes[i]);
vo.Update(bar, isNew: true);
}
var stateBeforeNewBar = vo.Last.Value;
// Add another bar with different volume
var newBar = new TBar(now.AddMinutes(5), 100, 100, 100, 100, 1000);
vo.Update(newBar, isNew: true);
// State should have advanced (different value due to new volume in moving averages)
Assert.NotEqual(stateBeforeNewBar, vo.Last.Value);
}
[Fact]
public void IsNew_False_UpdatesCurrentBar()
{
var vo = new Vo(shortPeriod: 2, longPeriod: 4, signalPeriod: 2);
var now = DateTime.UtcNow;
var bar1 = new TBar(now, 100, 100, 100, 100, 500);
vo.Update(bar1, isNew: true);
var bar2 = new TBar(now, 100, 100, 100, 100, 600);
vo.Update(bar2, isNew: false);
var bar3 = new TBar(now, 100, 100, 100, 100, 500);
var result = vo.Update(bar3, isNew: false);
Assert.Equal(vo.Update(bar1, isNew: false).Value, result.Value, Tolerance);
}
[Fact]
public void IterativeCorrections_RestoreState()
{
var vo = new Vo(shortPeriod: 3, longPeriod: 6, signalPeriod: 3);
var now = DateTime.UtcNow;
// Add several bars
for (int i = 0; i < 10; i++)
{
var bar = new TBar(now.AddMinutes(i), 100, 100, 100, 100, 500 + i * 10);
vo.Update(bar, isNew: true);
}
var stateBeforeCorrections = vo.Last.Value;
// Apply multiple corrections
for (int j = 0; j < 5; j++)
{
var correctionBar = new TBar(now.AddMinutes(9), 100, 100, 100, 100, 700 + j * 10);
vo.Update(correctionBar, isNew: false);
}
// Restore original bar
var originalBar = new TBar(now.AddMinutes(9), 100, 100, 100, 100, 590);
var restored = vo.Update(originalBar, isNew: false);
Assert.Equal(stateBeforeCorrections, restored.Value, Tolerance);
}
[Fact]
public void Reset_ClearsState()
{
var vo = new Vo();
// Process some bars
for (int i = 0; i < 20; i++)
{
vo.Update(_bars[i], isNew: true);
}
Assert.True(vo.IsHot);
vo.Reset();
Assert.False(vo.IsHot);
Assert.Equal(default, vo.Last);
}
#endregion
#region Warmup Tests
[Fact]
public void IsHot_BeforeWarmup_ReturnsFalse()
{
var vo = new Vo(shortPeriod: 3, longPeriod: 10, signalPeriod: 5);
var now = DateTime.UtcNow;
for (int i = 0; i < 9; i++)
{
var bar = new TBar(now.AddMinutes(i), 100, 100, 100, 100, 500);
vo.Update(bar, isNew: true);
Assert.False(vo.IsHot, $"Should not be hot at index {i}");
}
}
[Fact]
public void IsHot_AfterWarmup_ReturnsTrue()
{
var vo = new Vo(shortPeriod: 3, longPeriod: 10, signalPeriod: 5);
var now = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
var bar = new TBar(now.AddMinutes(i), 100, 100, 100, 100, 500);
vo.Update(bar, isNew: true);
}
Assert.True(vo.IsHot);
}
[Fact]
public void WarmupPeriod_EqualsLongPeriod()
{
var vo = new Vo(shortPeriod: 5, longPeriod: 15, signalPeriod: 10);
Assert.Equal(15, vo.WarmupPeriod);
}
#endregion
#region Robustness Tests
[Fact]
public void Update_NaN_UsesLastValidValue()
{
var vo = new Vo(shortPeriod: 2, longPeriod: 4, signalPeriod: 2);
var now = DateTime.UtcNow;
// Add valid bars
for (int i = 0; i < 5; i++)
{
var bar = new TBar(now.AddMinutes(i), 100, 100, 100, 100, 500);
vo.Update(bar, isNew: true);
}
// Add bar with NaN volume
var nanBar = new TBar(now.AddMinutes(5), 100, 100, 100, 100, double.NaN);
var result = vo.Update(nanBar, isNew: true);
Assert.True(double.IsFinite(result.Value), "Result should be finite after NaN input");
}
[Fact]
public void Update_Infinity_UsesLastValidValue()
{
var vo = new Vo(shortPeriod: 2, longPeriod: 4, signalPeriod: 2);
var now = DateTime.UtcNow;
// Add valid bars
for (int i = 0; i < 5; i++)
{
var bar = new TBar(now.AddMinutes(i), 100, 100, 100, 100, 500);
vo.Update(bar, isNew: true);
}
// Add bar with Infinity volume
var infBar = new TBar(now.AddMinutes(5), 100, 100, 100, 100, double.PositiveInfinity);
var result = vo.Update(infBar, isNew: true);
Assert.True(double.IsFinite(result.Value), "Result should be finite after Infinity input");
}
[Fact]
public void BatchUpdate_WithNaN_Safe()
{
var vo = new Vo();
var bars = new TBarSeries();
var now = DateTime.UtcNow;
for (int i = 0; i < 20; i++)
{
double volume = i == 10 ? double.NaN : 500 + i;
bars.Add(new TBar(now.AddMinutes(i), 100, 100, 100, 100, volume));
}
var result = vo.Update(bars);
Assert.Equal(20, result.Count);
foreach (var val in result.Values)
{
Assert.True(double.IsFinite(val), "All values should be finite");
}
}
#endregion
#region Consistency Tests
[Fact]
public void BatchCalc_EqualsStreaming()
{
var vo = new Vo(shortPeriod: 5, longPeriod: 10, signalPeriod: 10);
// Streaming
var streamingResults = new List<double>();
for (int i = 0; i < _bars.Count; i++)
{
var result = vo.Update(_bars[i], isNew: true);
streamingResults.Add(result.Value);
}
// Batch
var batchResult = Vo.Batch(_bars, shortPeriod: 5, longPeriod: 10, signalPeriod: 10);
Assert.Equal(streamingResults.Count, batchResult.Count);
for (int i = 0; i < streamingResults.Count; i++)
{
Assert.Equal(streamingResults[i], batchResult.Values[i], Tolerance);
}
}
[Fact]
public void SpanCalc_EqualsStreaming()
{
var vo = new Vo(shortPeriod: 5, longPeriod: 10, signalPeriod: 10);
// Streaming
var streamingResults = new List<double>();
for (int i = 0; i < _bars.Count; i++)
{
var result = vo.Update(_bars[i], isNew: true);
streamingResults.Add(result.Value);
}
// Span - pass arrays directly (implicit span conversion)
var volume = _bars.Volume.Values.ToArray();
var output = new double[_bars.Count];
Vo.Batch(volume, output, shortPeriod: 5, longPeriod: 10);
for (int i = 0; i < streamingResults.Count; i++)
{
Assert.Equal(streamingResults[i], output[i], Tolerance);
}
}
[Fact]
public void BatchUpdate_EqualsStreaming()
{
var voStream = new Vo(shortPeriod: 5, longPeriod: 10, signalPeriod: 10);
var voBatch = new Vo(shortPeriod: 5, longPeriod: 10, signalPeriod: 10);
// Streaming
for (int i = 0; i < _bars.Count; i++)
{
voStream.Update(_bars[i], isNew: true);
}
// Batch
var batchResult = voBatch.Update(_bars);
Assert.Equal(voStream.Last.Value, batchResult.Values[^1], Tolerance);
}
#endregion
#region Span API Tests
[Fact]
public void Calculate_Span_ValidatesLengths()
{
var volume = new double[100];
var output = new double[50]; // Wrong length
ArgumentException? caught = null;
try
{
Vo.Batch(volume, output, shortPeriod: 5, longPeriod: 10);
}
catch (ArgumentException ex)
{
caught = ex;
}
Assert.NotNull(caught);
Assert.Equal("output", caught.ParamName);
}
[Fact]
public void Calculate_Span_ValidatesShortPeriod()
{
var volume = new double[100];
var output = new double[100];
ArgumentException? caught = null;
try
{
Vo.Batch(volume, output, shortPeriod: 0, longPeriod: 10);
}
catch (ArgumentException ex)
{
caught = ex;
}
Assert.NotNull(caught);
Assert.Equal("shortPeriod", caught.ParamName);
}
[Fact]
public void Calculate_Span_ValidatesLongPeriod()
{
var volume = new double[100];
var output = new double[100];
ArgumentException? caught = null;
try
{
Vo.Batch(volume, output, shortPeriod: 5, longPeriod: 0);
}
catch (ArgumentException ex)
{
caught = ex;
}
Assert.NotNull(caught);
Assert.Equal("longPeriod", caught.ParamName);
}
[Fact]
public void Calculate_Span_ValidatesShortLessThanLong()
{
var volume = new double[100];
var output = new double[100];
ArgumentException? caught = null;
try
{
Vo.Batch(volume, output, shortPeriod: 10, longPeriod: 5);
}
catch (ArgumentException ex)
{
caught = ex;
}
Assert.NotNull(caught);
Assert.Equal("shortPeriod", caught.ParamName);
}
[Fact]
public void Calculate_Span_HandlesEmpty()
{
double[] volumeArr = [];
double[] outputArr = [];
// Should not throw
Vo.Batch(volumeArr, outputArr, shortPeriod: 5, longPeriod: 10);
Assert.Empty(outputArr);
}
[Fact]
public void Calculate_Span_HandlesNaN()
{
var volume = new double[20];
var output = new double[20];
for (int i = 0; i < 20; i++)
{
volume[i] = i == 10 ? double.NaN : 500 + i;
}
Vo.Batch(volume, output, shortPeriod: 5, longPeriod: 10);
foreach (var val in output)
{
Assert.True(double.IsFinite(val), "All values should be finite");
}
}
[Fact]
public void Calculate_Span_LargeData_NoStackOverflow()
{
var volume = new double[10000];
var output = new double[10000];
for (int i = 0; i < 10000; i++)
{
volume[i] = 500 + (i % 100);
}
// Should not throw stack overflow
Vo.Batch(volume, output, shortPeriod: 50, longPeriod: 200);
Assert.True(double.IsFinite(output[^1]));
}
#endregion
#region Event Tests
[Fact]
public void Pub_FiresOnUpdate()
{
var vo = new Vo();
var eventFired = false;
vo.Pub += (object? sender, in TValueEventArgs args) => { eventFired = true; };
vo.Update(_bars[0]);
Assert.True(eventFired);
}
[Fact]
public void Pub_ChainingWorks()
{
var vo = new Vo();
var receivedValues = new List<double>();
vo.Pub += (object? sender, in TValueEventArgs args) => { receivedValues.Add(args.Value.Value); };
for (int i = 0; i < 20; i++)
{
vo.Update(_bars[i], isNew: true);
}
Assert.Equal(20, receivedValues.Count);
}
#endregion
#region TValue Input Tests
[Fact]
public void Update_TValue_PreservesLastValue()
{
var vo = new Vo();
var now = DateTime.UtcNow;
// First update with bar to set a value
var bar = new TBar(now, 100, 100, 100, 100, 500);
vo.Update(bar, isNew: true);
var lastValue = vo.Last.Value;
// TValue update should preserve last value (VO requires volume)
var tval = new TValue(now.AddMinutes(1), 200);
var result = vo.Update(tval, isNew: true);
Assert.Equal(lastValue, result.Value, Tolerance);
}
#endregion
}