mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-16 09:38:05 +00:00
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:
@@ -318,4 +318,451 @@ public class BbandsTests
|
||||
Assert.Equal(batchResult[^1].Value, streamingBbands.Middle.Value, precision: 8);
|
||||
Assert.Equal(middleArray[^1], streamingBbands.Middle.Value, precision: 8);
|
||||
}
|
||||
|
||||
#region Default Constructor
|
||||
|
||||
[Fact]
|
||||
public void Bbands_Constructor_DefaultParameters()
|
||||
{
|
||||
// Default: period=20, multiplier=2.0
|
||||
Bbands bbands = new();
|
||||
|
||||
Assert.Equal("Bbands(20,2.0)", bbands.Name);
|
||||
Assert.Equal(20, bbands.WarmupPeriod);
|
||||
Assert.False(bbands.IsHot);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Prime Tests
|
||||
|
||||
[Fact]
|
||||
public void Bbands_Prime_SetsIndicatorToHot()
|
||||
{
|
||||
Bbands bbands = new(period: 5, multiplier: 2.0);
|
||||
|
||||
double[] data = [100, 101, 102, 103, 104, 105, 106, 107, 108, 109];
|
||||
|
||||
Assert.False(bbands.IsHot);
|
||||
|
||||
bbands.Prime(data);
|
||||
|
||||
Assert.True(bbands.IsHot);
|
||||
Assert.True(double.IsFinite(bbands.Middle.Value));
|
||||
Assert.True(double.IsFinite(bbands.Upper.Value));
|
||||
Assert.True(double.IsFinite(bbands.Lower.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bbands_Prime_EmptySpan_DoesNotThrow()
|
||||
{
|
||||
Bbands bbands = new(period: 5, multiplier: 2.0);
|
||||
var ex = Record.Exception(() => bbands.Prime(ReadOnlySpan<double>.Empty));
|
||||
Assert.Null(ex);
|
||||
Assert.False(bbands.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bbands_Prime_WithStep_UsesCorrectSpacing()
|
||||
{
|
||||
Bbands bbands = new(period: 3, multiplier: 2.0);
|
||||
double[] data = [100, 102, 104, 106, 108];
|
||||
var step = TimeSpan.FromHours(1);
|
||||
|
||||
bbands.Prime(data, step);
|
||||
|
||||
Assert.True(bbands.IsHot);
|
||||
Assert.True(double.IsFinite(bbands.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bbands_Prime_ThenUpdate_ContinuesCorrectly()
|
||||
{
|
||||
Bbands bbands = new(period: 5, multiplier: 2.0);
|
||||
|
||||
double[] primeData = [100, 101, 102, 103, 104, 105, 106, 107, 108, 109];
|
||||
bbands.Prime(primeData);
|
||||
Assert.True(bbands.IsHot);
|
||||
|
||||
double middleAfterPrime = bbands.Middle.Value;
|
||||
|
||||
// Continue with streaming
|
||||
bbands.Update(new TValue(DateTime.UtcNow, 120.0), isNew: true);
|
||||
|
||||
Assert.NotEqual(middleAfterPrime, bbands.Middle.Value);
|
||||
Assert.True(double.IsFinite(bbands.Middle.Value));
|
||||
Assert.True(double.IsFinite(bbands.Upper.Value));
|
||||
Assert.True(double.IsFinite(bbands.Lower.Value));
|
||||
Assert.True(bbands.Upper.Value > bbands.Middle.Value);
|
||||
Assert.True(bbands.Lower.Value < bbands.Middle.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bbands_Prime_MatchesStreamingResults()
|
||||
{
|
||||
double[] data = [100, 102, 98, 105, 103, 107, 101, 99, 106, 104];
|
||||
|
||||
// Via Prime
|
||||
Bbands primedBbands = new(period: 5, multiplier: 2.0);
|
||||
primedBbands.Prime(data);
|
||||
|
||||
// Via streaming Update
|
||||
Bbands streamBbands = new(period: 5, multiplier: 2.0);
|
||||
DateTime startTime = DateTime.UtcNow;
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
streamBbands.Update(new TValue(startTime + i * TimeSpan.FromSeconds(1), data[i]), isNew: true);
|
||||
}
|
||||
|
||||
Assert.Equal(streamBbands.Middle.Value, primedBbands.Middle.Value, precision: 10);
|
||||
Assert.Equal(streamBbands.Upper.Value, primedBbands.Upper.Value, precision: 10);
|
||||
Assert.Equal(streamBbands.Lower.Value, primedBbands.Lower.Value, precision: 10);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Calculate Tests
|
||||
|
||||
[Fact]
|
||||
public void Bbands_Calculate_ReturnsResultsAndHotIndicator()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
TSeries source = bars.Close;
|
||||
|
||||
var (results, indicator) = Bbands.Calculate(source, period: 5, multiplier: 2.0);
|
||||
|
||||
// Check results
|
||||
Assert.Equal(50, results.Count);
|
||||
|
||||
// Check indicator is hot and has valid state
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.True(double.IsFinite(indicator.Middle.Value));
|
||||
Assert.True(double.IsFinite(indicator.Upper.Value));
|
||||
Assert.True(double.IsFinite(indicator.Lower.Value));
|
||||
|
||||
// Verify indicator can continue streaming
|
||||
indicator.Update(new TValue(DateTime.UtcNow, 110.0), isNew: true);
|
||||
Assert.True(double.IsFinite(indicator.Middle.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bbands_Calculate_DefaultParameters()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
var bars = gbm.Fetch(30, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
TSeries source = bars.Close;
|
||||
|
||||
var (results, indicator) = Bbands.Calculate(source);
|
||||
|
||||
Assert.Equal(30, results.Count);
|
||||
Assert.Equal(20, indicator.WarmupPeriod);
|
||||
Assert.True(indicator.IsHot); // 30 > 20
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Update(TSeries) Edge Cases
|
||||
|
||||
[Fact]
|
||||
public void Bbands_UpdateTSeries_NullSource_ThrowsArgumentNullException()
|
||||
{
|
||||
Bbands bbands = new(period: 5, multiplier: 2.0);
|
||||
|
||||
Assert.Throws<ArgumentNullException>(() => bbands.Update((TSeries)null!));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Infinity Handling
|
||||
|
||||
[Fact]
|
||||
public void Bbands_Infinity_HandledGracefully()
|
||||
{
|
||||
Bbands bbands = new(period: 3, multiplier: 2.0);
|
||||
DateTime time = DateTime.UtcNow;
|
||||
|
||||
bbands.Update(new TValue(time, 10.0), isNew: true);
|
||||
bbands.Update(new TValue(time.AddSeconds(1), 12.0), isNew: true);
|
||||
|
||||
// PositiveInfinity should use last valid value
|
||||
bbands.Update(new TValue(time.AddSeconds(2), double.PositiveInfinity), isNew: true);
|
||||
Assert.True(double.IsFinite(bbands.Middle.Value));
|
||||
Assert.True(double.IsFinite(bbands.Upper.Value));
|
||||
Assert.True(double.IsFinite(bbands.Lower.Value));
|
||||
|
||||
// NegativeInfinity should also be handled
|
||||
bbands.Update(new TValue(time.AddSeconds(3), double.NegativeInfinity), isNew: true);
|
||||
Assert.True(double.IsFinite(bbands.Middle.Value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PercentB Edge Cases
|
||||
|
||||
[Fact]
|
||||
public void Bbands_PercentB_ZeroWidth_ReturnsZero()
|
||||
{
|
||||
// When all values are the same, stddev = 0, width = 0, percentB should be 0
|
||||
Bbands bbands = new(period: 3, multiplier: 2.0);
|
||||
DateTime time = DateTime.UtcNow;
|
||||
|
||||
bbands.Update(new TValue(time, 100.0), isNew: true);
|
||||
bbands.Update(new TValue(time.AddSeconds(1), 100.0), isNew: true);
|
||||
bbands.Update(new TValue(time.AddSeconds(2), 100.0), isNew: true);
|
||||
|
||||
Assert.Equal(0.0, bbands.Width.Value, precision: 10);
|
||||
Assert.Equal(0.0, bbands.PercentB.Value, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bbands_PercentB_AtMiddle_IsFiftyPercent()
|
||||
{
|
||||
// When price equals the middle band, %B should be ≈ 0.5
|
||||
Bbands bbands = new(period: 3, multiplier: 2.0);
|
||||
DateTime time = DateTime.UtcNow;
|
||||
|
||||
bbands.Update(new TValue(time, 10.0), isNew: true);
|
||||
bbands.Update(new TValue(time.AddSeconds(1), 12.0), isNew: true);
|
||||
bbands.Update(new TValue(time.AddSeconds(2), 14.0), isNew: true);
|
||||
|
||||
// SMA = 12.0, feeding 12.0 next — it becomes middle of [12, 14, 12] = SMA ≈ 12.67
|
||||
// Need to check the actual calculation rather than assume
|
||||
// The point: when price = middle, %B = (price - lower) / (upper - lower)
|
||||
// which would be 0.5 since middle is equidistant from upper and lower
|
||||
bbands.Update(new TValue(time.AddSeconds(3), bbands.Middle.Value), isNew: true);
|
||||
// After this update, the SMA shifts, but %B should be ≈ 0.5
|
||||
Assert.True(bbands.PercentB.Value > 0.3 && bbands.PercentB.Value < 0.7);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reset State Tests
|
||||
|
||||
[Fact]
|
||||
public void Bbands_Reset_ClearsAllProperties()
|
||||
{
|
||||
Bbands bbands = new(period: 3, multiplier: 2.0);
|
||||
DateTime time = DateTime.UtcNow;
|
||||
|
||||
bbands.Update(new TValue(time, 10.0));
|
||||
bbands.Update(new TValue(time.AddSeconds(1), 12.0));
|
||||
bbands.Update(new TValue(time.AddSeconds(2), 14.0));
|
||||
|
||||
Assert.True(bbands.IsHot);
|
||||
Assert.NotEqual(0, bbands.Middle.Value);
|
||||
Assert.NotEqual(0, bbands.Upper.Value);
|
||||
|
||||
bbands.Reset();
|
||||
|
||||
Assert.False(bbands.IsHot);
|
||||
Assert.Equal(0, bbands.Middle.Value);
|
||||
Assert.Equal(0, bbands.Upper.Value);
|
||||
Assert.Equal(0, bbands.Lower.Value);
|
||||
Assert.Equal(0, bbands.Width.Value);
|
||||
Assert.Equal(0, bbands.PercentB.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bbands_Reset_ThenReuse_ProducesSameResults()
|
||||
{
|
||||
Bbands bbands = new(period: 3, multiplier: 2.0);
|
||||
DateTime time = DateTime.UtcNow;
|
||||
double[] prices = [10.0, 12.0, 14.0];
|
||||
|
||||
// First pass
|
||||
for (int i = 0; i < prices.Length; i++)
|
||||
{
|
||||
bbands.Update(new TValue(time.AddSeconds(i), prices[i]));
|
||||
}
|
||||
double firstMiddle = bbands.Middle.Value;
|
||||
double firstUpper = bbands.Upper.Value;
|
||||
double firstLower = bbands.Lower.Value;
|
||||
|
||||
// Reset and second pass with same data
|
||||
bbands.Reset();
|
||||
for (int i = 0; i < prices.Length; i++)
|
||||
{
|
||||
bbands.Update(new TValue(time.AddSeconds(i), prices[i]));
|
||||
}
|
||||
|
||||
Assert.Equal(firstMiddle, bbands.Middle.Value, precision: 10);
|
||||
Assert.Equal(firstUpper, bbands.Upper.Value, precision: 10);
|
||||
Assert.Equal(firstLower, bbands.Lower.Value, precision: 10);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Span Batch Edge Cases
|
||||
|
||||
[Fact]
|
||||
public void Bbands_SpanBatch_EmptyArrays_DoesNotThrow()
|
||||
{
|
||||
double[] source = [];
|
||||
double[] middle = [];
|
||||
double[] upper = [];
|
||||
double[] lower = [];
|
||||
|
||||
var ex = Record.Exception(() => Bbands.Batch(
|
||||
source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan()));
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bbands_SpanBatch_InvalidPeriod_ThrowsArgumentOutOfRangeException()
|
||||
{
|
||||
double[] source = new double[10];
|
||||
double[] middle = new double[10];
|
||||
double[] upper = new double[10];
|
||||
double[] lower = new double[10];
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||
Bbands.Batch(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(),
|
||||
period: 1, multiplier: 2.0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bbands_SpanBatch_InvalidMultiplier_ThrowsArgumentOutOfRangeException()
|
||||
{
|
||||
double[] source = new double[10];
|
||||
double[] middle = new double[10];
|
||||
double[] upper = new double[10];
|
||||
double[] lower = new double[10];
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||
Bbands.Batch(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(),
|
||||
period: 5, multiplier: 0.05));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bbands_SpanBatch_ShorterThanPeriod_SetsNaN()
|
||||
{
|
||||
// Source shorter than period — all upper/lower should be NaN
|
||||
double[] source = [100, 101, 102];
|
||||
double[] middle = new double[3];
|
||||
double[] upper = new double[3];
|
||||
double[] lower = new double[3];
|
||||
|
||||
Bbands.Batch(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(),
|
||||
period: 5, multiplier: 2.0);
|
||||
|
||||
// First (period-1) values should be NaN for upper/lower
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
Assert.True(double.IsNaN(upper[i]));
|
||||
Assert.True(double.IsNaN(lower[i]));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bbands_SpanBatch_NaN_InWindow_EmitsNaN()
|
||||
{
|
||||
double[] source = [100, 101, double.NaN, 103, 104, 105, 106, 107, 108, 109];
|
||||
double[] middle = new double[10];
|
||||
double[] upper = new double[10];
|
||||
double[] lower = new double[10];
|
||||
|
||||
Bbands.Batch(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(),
|
||||
period: 5, multiplier: 2.0);
|
||||
|
||||
// Index 4 (first complete window [100,101,NaN,103,104]) contains NaN
|
||||
// So upper/lower at index 4 should be NaN
|
||||
Assert.True(double.IsNaN(upper[4]));
|
||||
Assert.True(double.IsNaN(lower[4]));
|
||||
|
||||
// Once NaN exits the window, values should become finite again
|
||||
// Window at index 7: [103, 104, 105, 106, 107] — all finite
|
||||
Assert.True(double.IsFinite(upper[7]));
|
||||
Assert.True(double.IsFinite(lower[7]));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Band Relationship Tests
|
||||
|
||||
[Fact]
|
||||
public void Bbands_UpperAlwaysAboveLower()
|
||||
{
|
||||
Bbands bbands = new(period: 5, multiplier: 2.0);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
DateTime time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
bbands.Update(new TValue(time.AddMinutes(i), bar.Close), isNew: true);
|
||||
|
||||
if (bbands.IsHot)
|
||||
{
|
||||
Assert.True(bbands.Upper.Value >= bbands.Lower.Value,
|
||||
$"Upper ({bbands.Upper.Value}) should be >= Lower ({bbands.Lower.Value}) at step {i}");
|
||||
Assert.True(bbands.Width.Value >= 0,
|
||||
$"Width ({bbands.Width.Value}) should be >= 0 at step {i}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bbands_MiddleIsBetweenBands()
|
||||
{
|
||||
Bbands bbands = new(period: 5, multiplier: 2.0);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
DateTime time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
bbands.Update(new TValue(time.AddMinutes(i), bar.Close), isNew: true);
|
||||
|
||||
if (bbands.IsHot)
|
||||
{
|
||||
Assert.True(bbands.Middle.Value >= bbands.Lower.Value,
|
||||
$"Middle ({bbands.Middle.Value}) should be >= Lower ({bbands.Lower.Value})");
|
||||
Assert.True(bbands.Middle.Value <= bbands.Upper.Value,
|
||||
$"Middle ({bbands.Middle.Value}) should be <= Upper ({bbands.Upper.Value})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bbands_MultiplierAffectsBandWidth()
|
||||
{
|
||||
DateTime time = DateTime.UtcNow;
|
||||
double[] prices = [100, 102, 98, 105, 103, 107, 101, 99, 106, 104];
|
||||
|
||||
Bbands narrow = new(period: 5, multiplier: 1.0);
|
||||
Bbands wide = new(period: 5, multiplier: 3.0);
|
||||
|
||||
for (int i = 0; i < prices.Length; i++)
|
||||
{
|
||||
narrow.Update(new TValue(time.AddSeconds(i), prices[i]), isNew: true);
|
||||
wide.Update(new TValue(time.AddSeconds(i), prices[i]), isNew: true);
|
||||
}
|
||||
|
||||
// Wider multiplier should produce wider bands
|
||||
Assert.True(wide.Width.Value > narrow.Width.Value);
|
||||
// Middle should be the same (same SMA)
|
||||
Assert.Equal(narrow.Middle.Value, wide.Middle.Value, precision: 10);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Last Property
|
||||
|
||||
[Fact]
|
||||
public void Bbands_Last_EqualsMiddle()
|
||||
{
|
||||
Bbands bbands = new(period: 3, multiplier: 2.0);
|
||||
DateTime time = DateTime.UtcNow;
|
||||
|
||||
bbands.Update(new TValue(time, 10.0));
|
||||
bbands.Update(new TValue(time.AddSeconds(1), 12.0));
|
||||
bbands.Update(new TValue(time.AddSeconds(2), 14.0));
|
||||
|
||||
// Last should be the Middle band value
|
||||
Assert.Equal(bbands.Middle.Value, bbands.Last.Value, precision: 10);
|
||||
Assert.Equal(bbands.Middle.Time, bbands.Last.Time);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user