mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-17 01:58:06 +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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user