mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-23 21:18:04 +00:00
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user