Add validation tests for various volume and momentum indicators

- Introduced Massi validation tests to ensure mathematical properties hold for the Mass Index indicator.
- Added Va validation tests for Volume Accumulation, checking for finite outputs and correct accumulation behavior.
- Implemented Vf validation tests for Volume Force, verifying outputs for rising and falling prices, and ensuring batch and streaming results match.
- Created Vo validation tests for Volume Oscillator, confirming behavior with constant, increasing, and decreasing volumes.
- Developed Vroc validation tests for Volume Rate of Change, validating outputs for constant volume and changes in volume.
- Updated project file to include new momentum indicators (MACD and RSI) in the compilation.
This commit is contained in:
Miha Kralj
2026-02-12 19:43:09 -08:00
parent 92709ef2ed
commit 951842acca
56 changed files with 12350 additions and 359 deletions
+234
View File
@@ -115,4 +115,238 @@ public class HtPhasorTests
var ex = Assert.Throws<ArgumentException>(() => HtPhasor.Batch(source, inPhase, quad));
Assert.Equal("quadrature", ex.ParamName);
}
#region Coverage Gap Tests
[Fact]
public void ChainedConstructor_ReceivesUpdates()
{
var source = new TSeries();
var phasor = new HtPhasor(source);
for (int i = 0; i < 50; i++)
{
source.Add(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + Math.Sin(i * 0.2) * 10));
}
Assert.True(phasor.IsHot);
Assert.True(double.IsFinite(phasor.Last.Value));
}
[Fact]
public void Update_IsNewFalse_RollsBackState()
{
var phasor = new HtPhasor();
for (int i = 0; i < 50; i++)
{
phasor.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + i), isNew: true);
}
double valueAfterNew = phasor.Last.Value;
phasor.Update(new TValue(DateTime.UtcNow.AddSeconds(50), 999.0), isNew: false);
double valueAfterCorrection = phasor.Last.Value;
Assert.Equal(valueAfterNew, valueAfterCorrection, Tolerance);
}
[Fact]
public void Update_IsNewFalse_AtStart_CoversWmaPath()
{
var phasor = new HtPhasor();
phasor.Update(new TValue(DateTime.UtcNow, 100.0), isNew: true);
var result = phasor.Update(new TValue(DateTime.UtcNow, 105.0), isNew: false);
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Update_NaNAsFirstInput_ReturnsNaN()
{
var phasor = new HtPhasor();
var result = phasor.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.True(double.IsNaN(result.Value));
}
[Fact]
public void Update_NaNAfterValid_SubstitutesLastValid()
{
var phasor = new HtPhasor();
for (int i = 0; i < 50; i++)
{
phasor.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + i));
}
var result = phasor.Update(new TValue(DateTime.UtcNow.AddSeconds(50), double.NaN));
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Update_InfinityAfterValid_SubstitutesLastValid()
{
var phasor = new HtPhasor();
for (int i = 0; i < 50; i++)
{
phasor.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + i));
}
var result = phasor.Update(new TValue(DateTime.UtcNow.AddSeconds(50), double.PositiveInfinity));
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void UpdateTSeries_ProcessesAllBars()
{
var phasor = new HtPhasor();
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var tSeries = new TSeries();
foreach (var bar in bars)
{
tSeries.Add(new TValue(bar.Time, bar.Close));
}
TSeries result = phasor.Update(tSeries);
Assert.Equal(100, result.Count);
Assert.True(phasor.IsHot);
}
[Fact]
public void UpdateTSeries_EmptySource_ReturnsEmpty()
{
var phasor = new HtPhasor();
var emptySource = new TSeries();
TSeries result = phasor.Update(emptySource);
Assert.Empty(result);
}
[Fact]
public void Prime_InitializesState()
{
var phasor1 = new HtPhasor();
var phasor2 = new HtPhasor();
double[] data = new double[50];
for (int i = 0; i < 50; i++)
{
data[i] = 100.0 + Math.Sin(i * 0.3) * 10;
}
phasor1.Prime(data);
foreach (double val in data)
{
phasor2.Update(new TValue(DateTime.UtcNow, val));
}
Assert.Equal(phasor2.Last.Value, phasor1.Last.Value, Tolerance);
}
[Fact]
public void BatchTSeries_ReturnsResults()
{
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var tSeries = new TSeries();
foreach (var bar in bars)
{
tSeries.Add(new TValue(bar.Time, bar.Close));
}
TSeries result = HtPhasor.Batch(tSeries);
Assert.Equal(100, result.Count);
}
[Fact]
public void BatchSpan_EmptyInput_ReturnsWithoutError()
{
double[] source = [];
double[] inPhase = [];
double[] quad = [];
var ex = Record.Exception(() => HtPhasor.Batch(source, inPhase, quad));
Assert.Null(ex);
}
[Fact]
public void Calculate_ReturnsTupleWithResultsAndIndicator()
{
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var tSeries = new TSeries();
foreach (var bar in bars)
{
tSeries.Add(new TValue(bar.Time, bar.Close));
}
var (results, indicator) = HtPhasor.Calculate(tSeries);
Assert.Equal(100, results.Count);
Assert.True(indicator.IsHot);
Assert.True(double.IsFinite(results.Last.Value));
}
[Fact]
public void Reset_ClearsState()
{
var phasor = new HtPhasor();
for (int i = 0; i < 50; i++)
{
phasor.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + i));
}
Assert.True(phasor.IsHot);
phasor.Reset();
Assert.False(phasor.IsHot);
Assert.Equal(default, phasor.Last);
Assert.Equal(0.0, phasor.Quadrature);
}
[Fact]
public void IterativeCorrections_RestoreState()
{
var phasor = new HtPhasor();
for (int i = 0; i < 50; i++)
{
phasor.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + i));
}
phasor.Update(new TValue(DateTime.UtcNow.AddSeconds(50), 200.0), isNew: true);
double afterNew = phasor.Last.Value;
phasor.Update(new TValue(DateTime.UtcNow.AddSeconds(50), 250.0), isNew: false);
phasor.Update(new TValue(DateTime.UtcNow.AddSeconds(50), 300.0), isNew: false);
phasor.Update(new TValue(DateTime.UtcNow.AddSeconds(50), 200.0), isNew: false);
Assert.Equal(afterNew, phasor.Last.Value, Tolerance);
}
[Fact]
public void Dispose_DoesNotThrow()
{
var phasor = new HtPhasor();
for (int i = 0; i < 50; i++)
{
phasor.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + i));
}
var ex = Record.Exception(() => phasor.Dispose());
Assert.Null(ex);
}
#endregion
}
+6 -25
View File
@@ -205,31 +205,15 @@ public sealed class HtPhasor : AbstractBase
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double UpdateWma(ref State s, double price, double[] priceHistory, bool isNew)
private static double UpdateWma(ref State s, double price, double[] priceHistory)
{
int historyIdx;
if (isNew)
{
historyIdx = s.Today % PRICE_HISTORY_SIZE;
}
else if (s.Today == 0)
{
historyIdx = 0;
}
else
{
historyIdx = (s.Today - 1 + PRICE_HISTORY_SIZE) % PRICE_HISTORY_SIZE;
}
int historyIdx = s.Today % PRICE_HISTORY_SIZE;
priceHistory[historyIdx] = price;
int processed = s.Today + (isNew ? 1 : 0);
int processed = s.Today + 1;
if (processed <= 3)
{
if (isNew)
{
s.Today++;
}
s.Today++;
return 0.0;
}
@@ -250,10 +234,7 @@ public sealed class HtPhasor : AbstractBase
s.PeriodWMASum = smoothedValue * 10.0;
s.TrailingWMAValue = p3;
if (isNew)
{
s.Today++;
}
s.Today++;
return smoothedValue;
}
@@ -299,7 +280,7 @@ public sealed class HtPhasor : AbstractBase
}
// WMA init and smoothing (updates day counter only when isNew)
double smoothedValue = UpdateWma(ref s, price, _priceHistory, isNew);
double smoothedValue = UpdateWma(ref s, price, _priceHistory);
// Still initializing WMA until day 3; smoothedValue only valid from day >=3
if (s.Today <= 3)