feat: Add Prime method to various indicators for initializing state with historical data

- Implemented Prime method in Vel, Ao, Apo, Frama, Adl, Adosc, Aobv, Cmf, Efi, Eom, Iii, Kvo, Mfi, Nvi, Obv, Pvd, Pvi, Pvo, Pvr, Pvt, Tvi, Twap, Va, Vf, Vo, Vroc, Vwad, Vwap, and Vwma classes.
- The Prime method resets the indicator state and processes the provided historical bar data to initialize the indicator.
- Added warmup period property to Adl and Wad classes to define the minimum number of data points required for validity.
- Updated benchmark tests to use Batch methods for performance evaluation.
This commit is contained in:
Miha Kralj
2026-02-11 20:38:38 -08:00
parent 75c6a9f135
commit 653aafacd8
71 changed files with 10527 additions and 242 deletions
+285 -63
View File
@@ -4,23 +4,31 @@ namespace QuanTAlib.Tests;
public class StbandsTests
{
#region Constructor Tests
[Fact]
public void Stbands_Constructor_ValidParameters()
{
// Arrange & Act
Stbands stbands = new(period: 10, multiplier: 3.0);
// Assert
Assert.NotNull(stbands);
Assert.Equal("Stbands(10,3.0)", stbands.Name);
Assert.Equal(10, stbands.WarmupPeriod);
Assert.False(stbands.IsHot);
}
[Fact]
public void Stbands_Constructor_DefaultParameters()
{
Stbands stbands = new();
Assert.Equal("Stbands(10,3.0)", stbands.Name);
Assert.Equal(10, stbands.WarmupPeriod);
}
[Fact]
public void Stbands_Constructor_InvalidPeriod_ThrowsArgumentOutOfRangeException()
{
// Arrange, Act & Assert
ArgumentOutOfRangeException exception = Assert.Throws<ArgumentOutOfRangeException>(
() => new Stbands(period: 0));
Assert.Equal("period", exception.ParamName);
@@ -29,24 +37,30 @@ public class StbandsTests
[Fact]
public void Stbands_Constructor_InvalidMultiplier_ThrowsArgumentOutOfRangeException()
{
// Arrange, Act & Assert
ArgumentOutOfRangeException exception = Assert.Throws<ArgumentOutOfRangeException>(
() => new Stbands(period: 10, multiplier: 0.0));
Assert.Equal("multiplier", exception.ParamName);
}
[Fact]
public void Stbands_Constructor_NegativeMultiplier_ThrowsArgumentOutOfRangeException()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new Stbands(period: 10, multiplier: -1.0));
}
#endregion
#region Update TBar Tests
[Fact]
public void Stbands_Update_TBar_ReturnsValue()
{
// Arrange
Stbands stbands = new(period: 3, multiplier: 2.0);
DateTime time = DateTime.UtcNow;
// Act
TBar bar = new(time, 100, 105, 95, 102, 1000);
TValue result = stbands.Update(bar);
// Assert
Assert.True(double.IsFinite(result.Value));
Assert.True(double.IsFinite(stbands.Upper.Value));
Assert.True(double.IsFinite(stbands.Lower.Value));
@@ -55,30 +69,29 @@ public class StbandsTests
[Fact]
public void Stbands_BandCalculations_CorrectValues()
{
// Arrange
Stbands stbands = new(period: 3, multiplier: 2.0);
DateTime time = DateTime.UtcNow;
// Act - Feed some bars
stbands.Update(new TBar(time, 100, 105, 95, 102, 1000), isNew: true);
stbands.Update(new TBar(time.AddMinutes(1), 102, 108, 100, 106, 1000), isNew: true);
stbands.Update(new TBar(time.AddMinutes(2), 106, 110, 104, 108, 1000), isNew: true);
// Assert
Assert.True(stbands.Upper.Value > stbands.Lower.Value);
Assert.True(stbands.Width.Value > 0);
Assert.True(stbands.Trend.Value == 1 || stbands.Trend.Value == -1);
Assert.True(stbands.IsHot);
}
#endregion
#region Band Behavior Tests
[Fact]
public void Stbands_UpperBand_OnlyMovesDown_InDowntrend()
{
// Arrange
Stbands stbands = new(period: 3, multiplier: 1.0);
DateTime time = DateTime.UtcNow;
// Act - Create downtrend scenario
stbands.Update(new TBar(time, 100, 105, 95, 100, 1000), isNew: true);
double initialUpper = stbands.Upper.Value;
@@ -88,18 +101,15 @@ public class StbandsTests
stbands.Update(new TBar(time.AddMinutes(2), 94, 98, 90, 92, 1000), isNew: true);
_ = stbands.Upper.Value;
// Assert - Upper should not increase (only tighten or stay same)
Assert.True(secondUpper <= initialUpper || secondUpper == stbands.Upper.Value);
}
[Fact]
public void Stbands_LowerBand_OnlyMovesUp_InUptrend()
{
// Arrange
Stbands stbands = new(period: 3, multiplier: 1.0);
DateTime time = DateTime.UtcNow;
// Act - Create uptrend scenario
stbands.Update(new TBar(time, 100, 105, 95, 102, 1000), isNew: true);
double initialLower = stbands.Lower.Value;
@@ -109,94 +119,112 @@ public class StbandsTests
stbands.Update(new TBar(time.AddMinutes(2), 110, 115, 108, 114, 1000), isNew: true);
double thirdLower = stbands.Lower.Value;
// Assert - Lower should not decrease (only tighten or stay same)
Assert.True(secondLower >= initialLower || thirdLower >= secondLower);
}
[Fact]
public void Stbands_Last_EqualsTrendAppropiateBand()
{
Stbands stbands = new(period: 3, multiplier: 2.0);
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.15, seed: 42);
for (int i = 0; i < 50; i++)
{
var bar = gbm.Next(isNew: true);
stbands.Update(bar, isNew: true);
double trend = stbands.Trend.Value;
if (trend > 0)
{
Assert.Equal(stbands.Lower.Value, stbands.Last.Value, 1e-10);
}
else
{
Assert.Equal(stbands.Upper.Value, stbands.Last.Value, 1e-10);
}
}
}
#endregion
#region Trend Tests
[Fact]
public void Stbands_TrendDirection_ChangesOnBreakout()
{
// Arrange
Stbands stbands = new(period: 3, multiplier: 1.0);
DateTime time = DateTime.UtcNow;
// Act - Start with some bars
stbands.Update(new TBar(time, 100, 105, 95, 100, 1000), isNew: true);
stbands.Update(new TBar(time.AddMinutes(1), 100, 105, 95, 100, 1000), isNew: true);
stbands.Update(new TBar(time.AddMinutes(2), 100, 105, 95, 100, 1000), isNew: true);
_ = (int)stbands.Trend.Value;
// Create a large breakout above upper band
stbands.Update(new TBar(time.AddMinutes(3), 120, 130, 118, 128, 1000), isNew: true);
// Assert - Trend should potentially change
Assert.True(stbands.Trend.Value == 1 || stbands.Trend.Value == -1);
}
#endregion
#region State Management Tests
[Fact]
public void Stbands_IsNew_False_RollsBackCorrectly()
{
// Arrange
Stbands stbands = new(period: 3, multiplier: 2.0);
DateTime time = DateTime.UtcNow;
// Act
stbands.Update(new TBar(time, 100, 105, 95, 100, 1000), isNew: true);
stbands.Update(new TBar(time.AddMinutes(1), 102, 108, 100, 106, 1000), isNew: true);
stbands.Update(new TBar(time.AddMinutes(2), 106, 112, 104, 110, 1000), isNew: true);
double upperBefore = stbands.Upper.Value;
_ = stbands.Lower.Value;
// Update with different value, isNew = false
stbands.Update(new TBar(time.AddMinutes(2), 90, 95, 85, 88, 1000), isNew: false);
double upperAfter = stbands.Upper.Value;
_ = stbands.Lower.Value;
// Assert - Values should change due to bar correction
Assert.NotEqual(upperBefore, upperAfter);
}
[Fact]
public void Stbands_IsNew_False_IterativeCorrections_Restore()
{
// Arrange
Stbands stbands = new(period: 3, multiplier: 2.0);
DateTime time = DateTime.UtcNow;
// Act - Build up state
stbands.Update(new TBar(time, 100, 105, 95, 100, 1000), isNew: true);
stbands.Update(new TBar(time.AddMinutes(1), 102, 108, 100, 106, 1000), isNew: true);
stbands.Update(new TBar(time.AddMinutes(2), 106, 112, 104, 110, 1000), isNew: true);
double originalUpper = stbands.Upper.Value;
double originalLower = stbands.Lower.Value;
// Make multiple corrections
stbands.Update(new TBar(time.AddMinutes(2), 90, 95, 85, 88, 1000), isNew: false);
stbands.Update(new TBar(time.AddMinutes(2), 80, 85, 75, 78, 1000), isNew: false);
// Restore original bar
stbands.Update(new TBar(time.AddMinutes(2), 106, 112, 104, 110, 1000), isNew: false);
double restoredUpper = stbands.Upper.Value;
double restoredLower = stbands.Lower.Value;
// Assert - Should restore to original values
Assert.Equal(originalUpper, restoredUpper, precision: 10);
Assert.Equal(originalLower, restoredLower, precision: 10);
}
#endregion
#region NaN / Infinity Handling Tests
[Fact]
public void Stbands_NaN_HandledGracefully()
{
// Arrange
Stbands stbands = new(period: 3, multiplier: 2.0);
DateTime time = DateTime.UtcNow;
// Act
stbands.Update(new TBar(time, 100, 105, 95, 100, 1000), isNew: true);
stbands.Update(new TBar(time.AddMinutes(1), 102, 108, 100, 106, 1000), isNew: true);
stbands.Update(new TBar(time.AddMinutes(2), double.NaN, double.NaN, double.NaN, double.NaN, 0), isNew: true);
// Assert - Should substitute last valid values
Assert.True(double.IsFinite(stbands.Upper.Value));
Assert.True(double.IsFinite(stbands.Lower.Value));
}
@@ -204,24 +232,24 @@ public class StbandsTests
[Fact]
public void Stbands_Infinity_HandledGracefully()
{
// Arrange
Stbands stbands = new(period: 3, multiplier: 2.0);
DateTime time = DateTime.UtcNow;
// Act
stbands.Update(new TBar(time, 100, 105, 95, 100, 1000), isNew: true);
stbands.Update(new TBar(time.AddMinutes(1), 102, 108, 100, 106, 1000), isNew: true);
stbands.Update(new TBar(time.AddMinutes(2), double.PositiveInfinity, double.PositiveInfinity, double.NegativeInfinity, double.PositiveInfinity, 0), isNew: true);
// Assert - Should substitute last valid values
Assert.True(double.IsFinite(stbands.Upper.Value));
Assert.True(double.IsFinite(stbands.Lower.Value));
}
#endregion
#region Reset Tests
[Fact]
public void Stbands_Reset_ClearsState()
{
// Arrange
Stbands stbands = new(period: 3, multiplier: 2.0);
DateTime time = DateTime.UtcNow;
@@ -229,21 +257,56 @@ public class StbandsTests
stbands.Update(new TBar(time.AddMinutes(1), 102, 108, 100, 106, 1000), isNew: true);
stbands.Update(new TBar(time.AddMinutes(2), 106, 112, 104, 110, 1000), isNew: true);
// Act
stbands.Reset();
// Assert
Assert.False(stbands.IsHot);
}
[Fact]
public void Stbands_Reset_ThenReuse_ProducesSameResults()
{
Stbands stbands = new(period: 3, multiplier: 2.0);
DateTime time = DateTime.UtcNow;
var bars = new TBar[]
{
new(time, 100, 105, 95, 100, 1000),
new(time.AddMinutes(1), 102, 108, 100, 106, 1000),
new(time.AddMinutes(2), 106, 112, 104, 110, 1000),
new(time.AddMinutes(3), 108, 115, 105, 112, 1000),
new(time.AddMinutes(4), 112, 118, 110, 116, 1000),
};
// First pass
foreach (var bar in bars)
{
stbands.Update(bar, isNew: true);
}
double upperFirst = stbands.Upper.Value;
double lowerFirst = stbands.Lower.Value;
double trendFirst = stbands.Trend.Value;
// Reset and second pass
stbands.Reset();
foreach (var bar in bars)
{
stbands.Update(bar, isNew: true);
}
Assert.Equal(upperFirst, stbands.Upper.Value, 1e-10);
Assert.Equal(lowerFirst, stbands.Lower.Value, 1e-10);
Assert.Equal(trendFirst, stbands.Trend.Value, 1e-10);
}
#endregion
#region WarmupPeriod / IsHot Tests
[Fact]
public void Stbands_WarmupPeriod_IsHotTransition()
{
// Arrange
Stbands stbands = new(period: 5, multiplier: 2.0);
DateTime time = DateTime.UtcNow;
// Act & Assert
for (int i = 0; i < 4; i++)
{
stbands.Update(new TBar(time.AddMinutes(i), 100 + i, 105 + i, 95 + i, 102 + i, 1000), isNew: true);
@@ -254,41 +317,153 @@ public class StbandsTests
Assert.True(stbands.IsHot);
}
#endregion
#region Prime Tests
[Fact]
public void Stbands_Prime_SetsIndicatorToHot()
{
Stbands stbands = new(period: 5, multiplier: 2.0);
double[] prices = [100, 102, 104, 98, 96, 99, 103, 107, 105, 110];
stbands.Prime(prices.AsSpan());
Assert.True(stbands.IsHot);
Assert.True(double.IsFinite(stbands.Last.Value));
Assert.True(double.IsFinite(stbands.Upper.Value));
Assert.True(double.IsFinite(stbands.Lower.Value));
}
[Fact]
public void Stbands_Prime_EmptySpan_DoesNotThrow()
{
Stbands stbands = new(period: 5, multiplier: 2.0);
stbands.Prime(ReadOnlySpan<double>.Empty);
Assert.False(stbands.IsHot);
}
[Fact]
public void Stbands_Prime_WithCustomStep()
{
Stbands stbands = new(period: 3, multiplier: 2.0);
double[] prices = [100, 102, 104, 106, 108];
stbands.Prime(prices.AsSpan(), TimeSpan.FromMinutes(5));
Assert.True(stbands.IsHot);
Assert.True(double.IsFinite(stbands.Last.Value));
}
[Fact]
public void Stbands_Prime_ThenUpdate_ContinuesCorrectly()
{
Stbands stbands = new(period: 3, multiplier: 2.0);
double[] primeData = [100, 102, 104, 106, 108];
stbands.Prime(primeData.AsSpan());
Assert.True(stbands.IsHot);
// Continue streaming with TBar
stbands.Update(new TBar(DateTime.UtcNow, 108, 112, 106, 110, 1000), isNew: true);
Assert.True(stbands.IsHot);
Assert.True(double.IsFinite(stbands.Last.Value));
}
#endregion
#region Calculate Tests
[Fact]
public void Stbands_Calculate_ReturnsResultsAndHotIndicator()
{
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.1, seed: 42);
TBarSeries bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var (results, indicator) = Stbands.Calculate(bars, period: 5, multiplier: 2.0);
Assert.True(indicator.IsHot);
Assert.Equal(bars.Count, results.Count);
Assert.True(double.IsFinite(indicator.Last.Value));
Assert.True(double.IsFinite(indicator.Upper.Value));
Assert.True(double.IsFinite(indicator.Lower.Value));
}
#endregion
#region Update TSeries Tests
[Fact]
public void Stbands_UpdateTSeries_ReturnsValidSeries()
{
Stbands stbands = new(period: 5, multiplier: 2.0);
var series = new TSeries();
for (int i = 0; i < 20; i++)
{
series.Add(DateTime.UtcNow.AddMinutes(i), 100 + i * 0.5);
}
TSeries result = stbands.Update(series);
Assert.Equal(20, result.Count);
Assert.True(stbands.IsHot);
}
[Fact]
public void Stbands_UpdateTSeries_NullSource_ThrowsArgumentNullException()
{
Stbands stbands = new(period: 5, multiplier: 2.0);
Assert.Throws<ArgumentNullException>(() => stbands.Update((TSeries)null!));
}
[Fact]
public void Stbands_UpdateTBarSeries_NullSource_ThrowsArgumentNullException()
{
Stbands stbands = new(period: 5, multiplier: 2.0);
Assert.Throws<ArgumentNullException>(() => stbands.Update((TBarSeries)null!));
}
#endregion
#region Update TBarSeries Tests
[Fact]
public void Stbands_UpdateTBarSeries_ReturnsValidSeries()
{
// Arrange
int period = 5;
Stbands stbands = new(period, multiplier: 2.0);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
TBarSeries bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Act
TSeries result = stbands.Update(bars);
// Assert
Assert.Equal(bars.Count, result.Count);
Assert.True(stbands.IsHot);
}
#endregion
#region Batch Tests
[Fact]
public void Stbands_StaticCalculate_ReturnsValidSeries()
{
// Arrange
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
TBarSeries bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Act
TSeries result = Stbands.Batch(bars, period: 5, multiplier: 2.0);
// Assert
Assert.Equal(bars.Count, result.Count);
}
[Fact]
public void Stbands_SpanCalculate_ProducesValidOutput()
{
// Arrange
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
TBarSeries bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
int period = 10;
@@ -301,10 +476,8 @@ public class StbandsTests
double[] lower = new double[bars.Count];
double[] trend = new double[bars.Count];
// Act
Stbands.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(), upper.AsSpan(), lower.AsSpan(), trend.AsSpan(), period, multiplier);
// Assert
for (int i = 0; i < bars.Count; i++)
{
Assert.True(double.IsFinite(upper[i]));
@@ -317,7 +490,6 @@ public class StbandsTests
[Fact]
public void Stbands_SpanCalculate_InvalidLength_ThrowsArgumentException()
{
// Arrange
double[] high = new double[10];
double[] low = new double[10];
double[] close = new double[10];
@@ -325,16 +497,49 @@ public class StbandsTests
double[] lower = new double[10];
double[] trend = new double[9]; // Wrong length
// Act & Assert
ArgumentException exception = Assert.Throws<ArgumentException>(
() => Stbands.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(), upper.AsSpan(), lower.AsSpan(), trend.AsSpan()));
Assert.Equal("high", exception.ParamName);
}
[Fact]
public void Stbands_SpanBatch_InvalidPeriod_ThrowsArgumentOutOfRangeException()
{
double[] data = new double[10];
Assert.Throws<ArgumentOutOfRangeException>(() =>
Stbands.Batch(data.AsSpan(), data.AsSpan(), data.AsSpan(),
data.AsSpan(), data.AsSpan(), data.AsSpan(), period: 0));
}
[Fact]
public void Stbands_SpanBatch_InvalidMultiplier_ThrowsArgumentOutOfRangeException()
{
double[] data = new double[10];
Assert.Throws<ArgumentOutOfRangeException>(() =>
Stbands.Batch(data.AsSpan(), data.AsSpan(), data.AsSpan(),
data.AsSpan(), data.AsSpan(), data.AsSpan(), period: 10, multiplier: 0.0));
}
[Fact]
public void Stbands_SpanBatch_EmptyArrays_DoesNotThrow()
{
double[] empty = [];
Stbands.Batch(empty.AsSpan(), empty.AsSpan(), empty.AsSpan(),
empty.AsSpan(), empty.AsSpan(), empty.AsSpan(), period: 10);
Assert.Empty(empty);
}
#endregion
#region Consistency Tests
[Fact]
public void Stbands_Consistency_StreamingVsBatch()
{
// Arrange
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
TBarSeries bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
int period = 10;
@@ -350,14 +555,12 @@ public class StbandsTests
// Batch
TSeries batchResult = Stbands.Batch(bars, period, multiplier);
// Assert - Last values should match
Assert.Equal(batchResult[^1].Value, streamingStbands.Last.Value, precision: 8);
}
[Fact]
public void Stbands_Consistency_StreamingVsSpan()
{
// Arrange
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
TBarSeries bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
int period = 10;
@@ -379,45 +582,64 @@ public class StbandsTests
double[] trend = new double[bars.Count];
Stbands.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(), upper.AsSpan(), lower.AsSpan(), trend.AsSpan(), period, multiplier);
// Assert - Last values should match
Assert.Equal(upper[^1], streamingStbands.Upper.Value, precision: 8);
Assert.Equal(lower[^1], streamingStbands.Lower.Value, precision: 8);
Assert.Equal(trend[^1], streamingStbands.Trend.Value, precision: 8);
}
#endregion
#region TValue Update Tests
[Fact]
public void Stbands_TValue_Update_WorksWithSingleValue()
{
// Arrange
Stbands stbands = new(period: 3, multiplier: 2.0);
DateTime time = DateTime.UtcNow;
// Act - Using TValue (treated as H=L=C=value)
stbands.Update(new TValue(time, 100.0), isNew: true);
stbands.Update(new TValue(time.AddMinutes(1), 102.0), isNew: true);
stbands.Update(new TValue(time.AddMinutes(2), 104.0), isNew: true);
// Assert
Assert.True(stbands.IsHot);
Assert.True(double.IsFinite(stbands.Upper.Value));
Assert.True(double.IsFinite(stbands.Lower.Value));
// With H=L=C, bands should be based on ATR=0 initially, but will have width from multiplier*0
// Actually TR will be 0 when H-L=0, so bands may be tight
}
#endregion
#region Width Tests
[Fact]
public void Stbands_Width_IsUpperMinusLower()
{
// Arrange
Stbands stbands = new(period: 3, multiplier: 2.0);
DateTime time = DateTime.UtcNow;
// Act
stbands.Update(new TBar(time, 100, 110, 90, 102, 1000), isNew: true);
stbands.Update(new TBar(time.AddMinutes(1), 102, 115, 95, 108, 1000), isNew: true);
stbands.Update(new TBar(time.AddMinutes(2), 108, 120, 100, 115, 1000), isNew: true);
// Assert
Assert.Equal(stbands.Upper.Value - stbands.Lower.Value, stbands.Width.Value, precision: 10);
}
#endregion
#region Pub Event Tests
[Fact]
public void Stbands_Pub_DoesNotFireDirectly()
{
// Stbands overrides Update paths without calling PubEvent —
// Pub event is inherited from AbstractBase but not invoked.
Stbands stbands = new(period: 3, multiplier: 2.0);
bool fired = false;
stbands.Pub += (object? sender, in TValueEventArgs args) => fired = true;
stbands.Update(new TValue(DateTime.UtcNow, 100.0), isNew: true);
Assert.False(fired);
}
#endregion
}