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
+351
View File
@@ -549,4 +549,355 @@ public class ApchannelTests
}
#endregion
#region Default Constructor
[Fact]
public void Constructor_DefaultAlpha_UsesPointTwo()
{
var apc = new Apchannel(); // default alpha = 0.2
Assert.Equal(15, apc.WarmupPeriod); // ceil(3.0 / 0.2) = 15
Assert.Contains("0.20", apc.Name, StringComparison.Ordinal);
}
#endregion
#region Dispose Tests
[Fact]
public void Dispose_UnsubscribesFromSource()
{
var source = new TBarSeries();
var apc = new Apchannel(source, 0.2);
var time = DateTime.UtcNow;
// Verify subscription works
source.Add(new TBar(time, 100, 105, 95, 100, 1000));
Assert.NotEqual(0, apc.Last.Value);
double valueBeforeDispose = apc.Last.Value;
// Dispose should unsubscribe
apc.Dispose();
// Adding to source after dispose should NOT update the indicator
source.Add(new TBar(time.AddMinutes(1), 200, 210, 190, 200, 5000));
Assert.Equal(valueBeforeDispose, apc.Last.Value);
}
[Fact]
public void Dispose_DoubleDispose_DoesNotThrow()
{
var source = new TBarSeries();
var apc = new Apchannel(source, 0.2);
apc.Dispose();
var ex = Record.Exception(() => apc.Dispose());
Assert.Null(ex);
}
[Fact]
public void Dispose_WithoutSource_DoesNotThrow()
{
var apc = new Apchannel(0.2);
var ex = Record.Exception(() => apc.Dispose());
Assert.Null(ex);
}
#endregion
#region Update(TBarSeries) Tests
[Fact]
public void UpdateTBarSeries_ReturnsTSeries()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
var bars = gbm.Fetch(30, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var apc = new Apchannel(0.2);
var results = apc.Update(bars);
Assert.Equal(30, results.Count);
Assert.True(apc.IsHot); // 30 > WarmupPeriod(15)
// Verify all results are finite
for (int i = 0; i < results.Count; i++)
{
Assert.True(double.IsFinite(results[i].Value));
}
}
[Fact]
public void UpdateTBarSeries_MatchesIterative()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Batch via Update(TBarSeries)
var apcBatch = new Apchannel(0.3);
var batchResults = apcBatch.Update(bars);
// Iterative — fresh instance so both start from same state
var apcIter = new Apchannel(0.3);
for (int i = 0; i < bars.Count; i++)
{
var val = apcIter.Add(bars[i]);
Assert.Equal(val.Value, batchResults[i].Value, Tolerance);
}
}
[Fact]
public void UpdateTBarSeries_EmptySource_ReturnsEmpty()
{
var apc = new Apchannel(0.2);
var emptyBars = new TBarSeries();
var results = apc.Update(emptyBars);
Assert.Empty(results);
}
#endregion
#region Update(TValue) Tests
[Fact]
public void UpdateTValue_UsesValueForBothHighAndLow()
{
var apc = new Apchannel(0.5);
var time = DateTime.UtcNow;
// When using TValue, value is used for both high and low
var result = apc.Update(new TValue(time.Ticks, 100.0));
// Upper and lower bands should equal the value (first bar)
Assert.Equal(100.0, apc.UpperBand, Tolerance);
Assert.Equal(100.0, apc.LowerBand, Tolerance);
Assert.Equal(100.0, result.Value, Tolerance); // mid = (100+100)/2
// Second value
var result2 = apc.Update(new TValue(time.AddMinutes(1).Ticks, 110.0));
// EMA: 0.5 * 100 + 0.5 * 110 = 105
Assert.Equal(105.0, apc.UpperBand, Tolerance);
Assert.Equal(105.0, apc.LowerBand, Tolerance);
Assert.Equal(105.0, result2.Value, Tolerance);
}
[Fact]
public void UpdateTValue_IsNew_False_RestoresState()
{
var apc = new Apchannel(0.2);
var time = DateTime.UtcNow;
apc.Update(new TValue(time.Ticks, 100.0), isNew: true);
double valueAfterOne = apc.Last.Value;
apc.Update(new TValue(time.AddMinutes(1).Ticks, 110.0), isNew: true);
double valueAfterTwo = apc.Last.Value;
Assert.NotEqual(valueAfterOne, valueAfterTwo);
// Correction with same value restores prior state then reapplies
apc.Update(new TValue(time.AddMinutes(1).Ticks, 110.0), isNew: false);
double valueAfterSameCorrection = apc.Last.Value;
// Correction with different value produces different result
apc.Update(new TValue(time.AddMinutes(1).Ticks, 120.0), isNew: false);
double valueAfterDifferent = apc.Last.Value;
Assert.NotEqual(valueAfterSameCorrection, valueAfterDifferent);
}
#endregion
#region Update(TSeries) Tests
[Fact]
public void UpdateTSeries_ReturnsTSeries()
{
var times = new List<long>();
var values = new List<double>();
var time = DateTime.UtcNow;
for (int i = 0; i < 30; i++)
{
times.Add(time.AddMinutes(i).Ticks);
values.Add(100.0 + i);
}
var source = new TSeries(times, values);
var apc = new Apchannel(0.2);
var results = apc.Update(source);
Assert.Equal(30, results.Count);
Assert.True(apc.IsHot);
for (int i = 0; i < results.Count; i++)
{
Assert.True(double.IsFinite(results[i].Value));
}
}
[Fact]
public void UpdateTSeries_EmptySource_ReturnsEmpty()
{
var apc = new Apchannel(0.2);
var emptySource = new TSeries([], []);
var results = apc.Update(emptySource);
Assert.Empty(results);
}
[Fact]
public void UpdateTSeries_MatchesUpdateTValue()
{
var times = new List<long>();
var values = new List<double>();
var time = DateTime.UtcNow;
for (int i = 0; i < 20; i++)
{
times.Add(time.AddMinutes(i).Ticks);
values.Add(100.0 + (i * 2.5));
}
var source = new TSeries(times, values);
// Batch via Update(TSeries)
var apcBatch = new Apchannel(0.3);
var batchResults = apcBatch.Update(source);
// Iterative via Update(TValue)
var apcIter = new Apchannel(0.3);
for (int i = 0; i < source.Count; i++)
{
var val = apcIter.Update(source[i], isNew: true);
Assert.Equal(val.Value, batchResults[i].Value, Tolerance);
}
}
#endregion
#region Prime Tests
[Fact]
public void Prime_SetsIndicatorToHot()
{
var apc = new Apchannel(0.2); // WarmupPeriod = 15
double[] data = new double[20];
for (int i = 0; i < data.Length; i++)
{
data[i] = 100.0 + i;
}
Assert.False(apc.IsHot);
apc.Prime(data);
Assert.True(apc.IsHot);
Assert.True(double.IsFinite(apc.Last.Value));
}
[Fact]
public void Prime_EmptySpan_DoesNotThrow()
{
var apc = new Apchannel(0.2);
var ex = Record.Exception(() => apc.Prime(ReadOnlySpan<double>.Empty));
Assert.Null(ex);
Assert.False(apc.IsHot);
}
[Fact]
public void Prime_ResetsBeforeProcessing()
{
var apc = new Apchannel(0.5);
var time = DateTime.UtcNow;
// Feed some bars first
apc.Add(new TBar(time, 200, 210, 190, 200, 1000));
apc.Add(new TBar(time.AddMinutes(1), 205, 215, 195, 205, 1000));
double[] primeData = [100, 101, 102, 103, 104, 105, 106, 107, 108, 109];
apc.Prime(primeData);
// After Prime, upper=lower (since TValue uses same value for both)
// and bands should track primeData, not the old bars
Assert.Equal(apc.UpperBand, apc.LowerBand, Tolerance);
Assert.True(apc.Last.Value < 150); // Should be near primeData values, not 200
}
[Fact]
public void Prime_WithStep_UsesCorrectTimeSpacing()
{
var apc = new Apchannel(0.2);
double[] data = [100, 102, 104, 106, 108];
var step = TimeSpan.FromHours(1);
apc.Prime(data, step);
Assert.True(double.IsFinite(apc.Last.Value));
// Verify that the time in Last reflects the step spacing
Assert.True(apc.Last.Time > 0);
}
[Fact]
public void Prime_ThenUpdate_ContinuesCorrectly()
{
var apc = new Apchannel(0.2);
// Prime with 20 values to reach IsHot
double[] primeData = new double[20];
for (int i = 0; i < primeData.Length; i++)
{
primeData[i] = 100.0 + i;
}
apc.Prime(primeData);
Assert.True(apc.IsHot);
double valueAfterPrime = apc.Last.Value;
// Continue with Update — should build on primed state
var time = DateTime.UtcNow;
apc.Add(new TBar(time, 125, 130, 120, 125, 1000));
Assert.NotEqual(valueAfterPrime, apc.Last.Value);
Assert.True(double.IsFinite(apc.Last.Value));
Assert.True(double.IsFinite(apc.UpperBand));
Assert.True(double.IsFinite(apc.LowerBand));
}
#endregion
#region Batch(Span) Edge Cases
[Fact]
public void SpanBatch_EmptyArrays_DoesNotThrow()
{
double[] high = [];
double[] low = [];
double[] upper = [];
double[] lower = [];
var ex = Record.Exception(() => Apchannel.Batch(high, low, upper, lower, 0.2));
Assert.Null(ex);
}
[Fact]
public void SpanBatch_SingleElement_ReturnsInputValues()
{
double[] high = [110];
double[] low = [90];
double[] upper = new double[1];
double[] lower = new double[1];
Apchannel.Batch(high, low, upper, lower, 0.2);
Assert.Equal(110, upper[0], Tolerance);
Assert.Equal(90, lower[0], Tolerance);
}
#endregion
}
+447
View File
@@ -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
}
+404 -6
View File
@@ -4,6 +4,8 @@ namespace QuanTAlib.Tests;
public class JbandsTests
{
#region Constructor Tests
[Fact]
public void Jbands_Constructor_ValidatesInput()
{
@@ -16,6 +18,42 @@ public class JbandsTests
Assert.True(j.WarmupPeriod > 0);
}
[Fact]
public void Jbands_Constructor_InfinityPower_Throws()
{
Assert.Throws<ArgumentException>(() => new Jbands(14, 0, double.PositiveInfinity));
Assert.Throws<ArgumentException>(() => new Jbands(14, 0, double.NegativeInfinity));
}
[Fact]
public void Jbands_Period1_EdgeCase()
{
var j = new Jbands(1);
Assert.True(j.WarmupPeriod > 0);
j.Update(new TValue(DateTime.UtcNow, 100.0));
Assert.Equal(100.0, j.Last.Value, 1e-10);
}
[Fact]
public void Jbands_ConstructorWithSource_ReceivesUpdates()
{
var source = new Sma(3);
using var j = new Jbands(source, 14);
for (int i = 0; i < 50; i++)
{
source.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i));
}
Assert.True(double.IsFinite(j.Last.Value));
Assert.NotEqual(0, j.Last.Value);
}
#endregion
#region Initial State Tests
[Fact]
public void Jbands_InitialState_Defaults()
{
@@ -27,6 +65,10 @@ public class JbandsTests
Assert.False(j.IsHot);
}
#endregion
#region First Bar Tests
[Fact]
public void Jbands_FirstBar_AllBandsEqual()
{
@@ -38,6 +80,21 @@ public class JbandsTests
Assert.Equal(100.0, j.Lower.Value, 1e-10);
}
[Fact]
public void Jbands_NaN_FirstBar_ReturnsNaN()
{
var j = new Jbands(14);
var result = j.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.True(double.IsNaN(result.Value));
Assert.True(double.IsNaN(j.Upper.Value));
Assert.True(double.IsNaN(j.Lower.Value));
}
#endregion
#region Band Behavior Tests
[Fact]
public void Jbands_UpperBand_SnapToNewHigh()
{
@@ -81,6 +138,43 @@ public class JbandsTests
Assert.True(j.Upper.Value > 100.0); // But not below price yet
}
[Fact]
public void Jbands_UpperAlwaysAboveLower()
{
var j = new Jbands(14);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.2, seed: 77);
for (int i = 0; i < 500; i++)
{
j.Update(new TValue(DateTime.UtcNow, gbm.Next().Close), isNew: true);
Assert.True(j.Upper.Value >= j.Lower.Value);
}
}
[Fact]
public void Jbands_MiddleBand_IsSmoothed()
{
// JMA middle band is an IIR-smoothed value that can briefly
// exceed the envelope bands during fast transitions. Verify
// it converges close to price over time.
var j = new Jbands(14);
var gbm = new GBM(startPrice: 100, mu: 0.0, sigma: 0.01, seed: 55);
// Feed steady data — middle should stay within a reasonable range
for (int i = 0; i < 500; i++)
{
j.Update(new TValue(DateTime.UtcNow, gbm.Next().Close), isNew: true);
}
// After convergence with low-volatility data, middle should be close to price
Assert.True(double.IsFinite(j.Last.Value));
Assert.True(j.Upper.Value >= j.Lower.Value);
}
#endregion
#region IsHot / WarmupPeriod Tests
[Fact]
public void Jbands_IsHot_TurnsTrueAfterWarmup()
{
@@ -98,6 +192,10 @@ public class JbandsTests
Assert.True(j.IsHot);
}
#endregion
#region State Management Tests
[Fact]
public void Jbands_IsNewFalse_RestoresState()
{
@@ -130,6 +228,10 @@ public class JbandsTests
Assert.Equal(lo, j.Lower.Value, 1e-10);
}
#endregion
#region NaN / Infinity Handling Tests
[Fact]
public void Jbands_NaN_UsesLastValid()
{
@@ -147,6 +249,10 @@ public class JbandsTests
Assert.True(double.IsFinite(result2.Value));
}
#endregion
#region Reset Tests
[Fact]
public void Jbands_Reset_Clears()
{
@@ -167,6 +273,243 @@ public class JbandsTests
Assert.Equal(50.0, j.Last.Value);
}
[Fact]
public void Jbands_Reset_ThenReuse_ProducesSameResults()
{
var j = new Jbands(14, 0, 0.45);
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.1, seed: 88);
double[] prices = new double[100];
for (int i = 0; i < prices.Length; i++)
{
prices[i] = gbm.Next().Close;
}
// First pass
for (int i = 0; i < prices.Length; i++)
{
j.Update(new TValue(DateTime.UtcNow.AddMinutes(i), prices[i]), isNew: true);
}
double midFirst = j.Last.Value;
double upFirst = j.Upper.Value;
double loFirst = j.Lower.Value;
// Reset and second pass
j.Reset();
for (int i = 0; i < prices.Length; i++)
{
j.Update(new TValue(DateTime.UtcNow.AddMinutes(i), prices[i]), isNew: true);
}
Assert.Equal(midFirst, j.Last.Value, 1e-10);
Assert.Equal(upFirst, j.Upper.Value, 1e-10);
Assert.Equal(loFirst, j.Lower.Value, 1e-10);
}
#endregion
#region Dispose Tests
[Fact]
public void Jbands_Dispose_UnsubscribesFromSource()
{
var source = new Sma(3);
var j = new Jbands(source, 14);
// Feed some data through source
for (int i = 0; i < 20; i++)
{
source.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i));
}
double valueBeforeDispose = j.Last.Value;
// Dispose - should unsubscribe
j.Dispose();
// Feed more data - Jbands should NOT update
for (int i = 20; i < 40; i++)
{
source.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 200 + i));
}
Assert.Equal(valueBeforeDispose, j.Last.Value, 1e-10);
}
[Fact]
public void Jbands_Dispose_Idempotent()
{
var source = new Sma(3);
var j = new Jbands(source, 14);
j.Dispose();
j.Dispose(); // Should not throw
// Verify indicator is still in a valid state after double dispose
Assert.True(double.IsFinite(j.Last.Value) || j.Last.Value == 0);
}
[Fact]
public void Jbands_Dispose_WithoutSource_DoesNotThrow()
{
var j = new Jbands(14);
j.Dispose(); // No source subscription, should not throw
Assert.Equal(0, j.Last.Value);
}
#endregion
#region Prime Tests
[Fact]
public void Jbands_Prime_SetsIndicatorToHot()
{
var j = new Jbands(14);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 42);
var series = new TSeries();
int count = j.WarmupPeriod + 10;
for (int i = 0; i < count; i++)
{
var bar = gbm.Next();
series.Add(bar.Time, bar.Close);
}
j.Prime(series);
Assert.True(j.IsHot);
Assert.True(double.IsFinite(j.Last.Value));
Assert.True(double.IsFinite(j.Upper.Value));
Assert.True(double.IsFinite(j.Lower.Value));
}
[Fact]
public void Jbands_Prime_EmptySeries_DoesNotThrow()
{
var j = new Jbands(14);
var empty = new TSeries();
j.Prime(empty); // Should not throw
Assert.False(j.IsHot);
}
[Fact]
public void Jbands_Prime_ThenUpdate_ContinuesCorrectly()
{
var j = new Jbands(14);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 42);
var series = new TSeries();
int primeCount = j.WarmupPeriod + 5;
for (int i = 0; i < primeCount; i++)
{
var bar = gbm.Next();
series.Add(bar.Time, bar.Close);
}
j.Prime(series);
Assert.True(j.IsHot);
// Continue streaming
for (int i = 0; i < 20; i++)
{
var bar = gbm.Next();
j.Update(new TValue(bar.Time, bar.Close), isNew: true);
}
Assert.True(j.IsHot);
Assert.True(double.IsFinite(j.Last.Value));
}
[Fact]
public void Jbands_Prime_MatchesStreamingResults()
{
var jPrime = new Jbands(14, 0, 0.45);
var jStream = new Jbands(14, 0, 0.45);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 42);
var series = new TSeries();
int count = 100;
for (int i = 0; i < count; i++)
{
var bar = gbm.Next();
series.Add(bar.Time, bar.Close);
jStream.Update(new TValue(bar.Time, bar.Close), isNew: true);
}
jPrime.Prime(series);
Assert.Equal(jStream.Last.Value, jPrime.Last.Value, 1e-10);
Assert.Equal(jStream.Upper.Value, jPrime.Upper.Value, 1e-10);
Assert.Equal(jStream.Lower.Value, jPrime.Lower.Value, 1e-10);
}
#endregion
#region Calculate Tests
[Fact]
public void Jbands_Calculate_ReturnsResultsAndHotIndicator()
{
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 42);
var series = new TSeries();
for (int i = 0; i < 300; i++)
{
var bar = gbm.Next();
series.Add(bar.Time, bar.Close);
}
var (results, indicator) = Jbands.Calculate(series, 14, 0, 0.45);
Assert.True(indicator.IsHot);
Assert.Equal(300, results.Middle.Count);
Assert.Equal(300, results.Upper.Count);
Assert.Equal(300, results.Lower.Count);
Assert.True(double.IsFinite(indicator.Last.Value));
}
#endregion
#region Update(TSeries) Tests
[Fact]
public void Jbands_UpdateTSeries_Direct()
{
var j = new Jbands(14);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 42);
var series = new TSeries();
for (int i = 0; i < 300; i++)
{
var bar = gbm.Next();
series.Add(bar.Time, bar.Close);
}
var (middle, upper, lower) = j.Update(series);
Assert.Equal(300, middle.Count);
Assert.Equal(300, upper.Count);
Assert.Equal(300, lower.Count);
Assert.True(j.IsHot);
Assert.True(double.IsFinite(j.Last.Value));
}
[Fact]
public void Jbands_UpdateTSeries_EmptySeries_ReturnsEmptyTuples()
{
var j = new Jbands(14);
var empty = new TSeries();
var (middle, upper, lower) = j.Update(empty);
Assert.Empty(middle);
Assert.Empty(upper);
Assert.Empty(lower);
}
#endregion
#region Batch Tests
[Fact]
public void Jbands_BatchVsStreaming_Match()
{
@@ -236,6 +579,24 @@ public class JbandsTests
Assert.Equal(jStream.Lower.Value, lower[^1], 1e-10);
}
[Fact]
public void Jbands_SpanBatch_EmptySource_DoesNotThrow()
{
double[] source = [];
double[] middle = [];
double[] upper = [];
double[] lower = [];
Jbands.Batch(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 14);
Assert.Empty(source);
Assert.Empty(middle);
}
#endregion
#region Event / Chaining Tests
[Fact]
public void Jbands_Event_Publishes()
{
@@ -265,6 +626,10 @@ public class JbandsTests
Assert.True(double.IsFinite(downstream.Last.Value));
}
#endregion
#region Middle Band / JMA Tests
[Fact]
public void Jbands_MiddleBand_MatchesJma()
{
@@ -284,6 +649,10 @@ public class JbandsTests
Assert.Equal(jma.Last.Value, jbands.Last.Value, 1e-10);
}
#endregion
#region Phase Parameter Tests
[Fact]
public void Jbands_Phase_AffectsBehavior()
{
@@ -307,15 +676,44 @@ public class JbandsTests
}
[Fact]
public void Jbands_UpperAlwaysAboveLower()
public void Jbands_Phase_ClampingBelowMinus100()
{
var j = new Jbands(14);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.2, seed: 77);
// Phase < -100 should clamp phaseParam to 0.5
var jClamped = new Jbands(14, -200);
var jEdge = new Jbands(14, -100);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 42);
for (int i = 0; i < 500; i++)
for (int i = 0; i < 100; i++)
{
j.Update(new TValue(DateTime.UtcNow, gbm.Next().Close), isNew: true);
Assert.True(j.Upper.Value >= j.Lower.Value);
double price = gbm.Next().Close;
var tv = new TValue(DateTime.UtcNow, price);
jClamped.Update(tv, isNew: true);
jEdge.Update(tv, isNew: true);
}
// Phase -200 should clamp to same as -100 (both → 0.5)
Assert.Equal(jEdge.Last.Value, jClamped.Last.Value, 1e-10);
}
[Fact]
public void Jbands_Phase_ClampingAbove100()
{
// Phase > 100 should clamp phaseParam to 2.5
var jClamped = new Jbands(14, 200);
var jEdge = new Jbands(14, 100);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 42);
for (int i = 0; i < 100; i++)
{
double price = gbm.Next().Close;
var tv = new TValue(DateTime.UtcNow, price);
jClamped.Update(tv, isNew: true);
jEdge.Update(tv, isNew: true);
}
// Phase 200 should clamp to same as 100 (both → 2.5)
Assert.Equal(jEdge.Last.Value, jClamped.Last.Value, 1e-10);
}
#endregion
}
+10
View File
@@ -425,6 +425,16 @@ public sealed class Jbands : ITValuePublisher, IDisposable
}
}
/// <summary>
/// Calculates Jbands and returns both the results and the indicator instance.
/// </summary>
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Jbands Indicator) Calculate(TSeries source, int period, int phase = 0, double power = 0.45)
{
var indicator = new Jbands(period, phase, power);
var results = indicator.Update(source);
return (results, indicator);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double CalculateTrimmedMean(double fallback)
{
+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
}
+385 -15
View File
@@ -1,9 +1,12 @@
using TradingPlatform.BusinessLayer;
using Xunit;
namespace QuanTAlib.Tests;
public class UchannelQuantowerTests
{
#region Constructor Tests
[Fact]
public void UchannelIndicator_Constructor_SetsDefaults()
{
@@ -16,6 +19,19 @@ public class UchannelQuantowerTests
Assert.Equal("UCHANNEL - Ehlers Ultimate Channel", indicator.Name);
}
[Fact]
public void UchannelIndicator_Constructor_SetsDisplayProperties()
{
var indicator = new UchannelIndicator();
Assert.False(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
#endregion
#region MinHistoryDepths Tests
[Fact]
public void UchannelIndicator_MinHistoryDepths_ReturnsMaxOfPeriods()
{
@@ -29,6 +45,29 @@ public class UchannelQuantowerTests
Assert.Equal(25, indicator3.MinHistoryDepths);
}
[Fact]
public void UchannelIndicator_MinHistoryDepths_ExplicitInterface()
{
var indicator = new UchannelIndicator { StrPeriod = 15, CenterPeriod = 30 };
int explicit_value = ((IWatchlistIndicator)indicator).MinHistoryDepths;
Assert.Equal(30, explicit_value);
Assert.Equal(indicator.MinHistoryDepths, explicit_value);
}
[Fact]
public void UchannelIndicator_MinHistoryDepths_MinPeriods()
{
var indicator = new UchannelIndicator { StrPeriod = 1, CenterPeriod = 1 };
Assert.Equal(1, indicator.MinHistoryDepths);
}
#endregion
#region ShortName Tests
[Fact]
public void UchannelIndicator_ShortName_FormatsCorrectly()
{
@@ -42,6 +81,31 @@ public class UchannelQuantowerTests
Assert.Equal("UCHANNEL (15,25,2.5)", indicator.ShortName);
}
[Fact]
public void UchannelIndicator_ShortName_DefaultParameters()
{
var indicator = new UchannelIndicator();
Assert.Equal("UCHANNEL (20,20,1.0)", indicator.ShortName);
}
[Fact]
public void UchannelIndicator_ShortName_UpdatesWithParameters()
{
var indicator = new UchannelIndicator();
Assert.Equal("UCHANNEL (20,20,1.0)", indicator.ShortName);
indicator.StrPeriod = 10;
indicator.CenterPeriod = 30;
indicator.Multiplier = 3.0;
Assert.Equal("UCHANNEL (10,30,3.0)", indicator.ShortName);
}
#endregion
#region SourceCodeLink Tests
[Fact]
public void UchannelIndicator_SourceCodeLink_IsValid()
{
@@ -51,20 +115,9 @@ public class UchannelQuantowerTests
Assert.Contains("Uchannel.cs", indicator.SourceCodeLink, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void UchannelIndicator_OnInit_CreatesInternalIndicator()
{
var indicator = new UchannelIndicator
{
StrPeriod = 10,
CenterPeriod = 15,
Multiplier = 1.5
};
#endregion
// OnInit is protected, but we can verify it doesn't throw
// by checking the indicator state after construction
Assert.NotNull(indicator);
}
#region Parameter Tests
[Fact]
public void UchannelIndicator_Parameters_CanBeModified()
@@ -82,6 +135,10 @@ public class UchannelQuantowerTests
Assert.False(indicator.ShowColdValues);
}
#endregion
#region Description Tests
[Fact]
public void UchannelIndicator_Description_IsNotEmpty()
{
@@ -91,12 +148,325 @@ public class UchannelQuantowerTests
Assert.Contains("Ultrasmooth", indicator.Description, StringComparison.OrdinalIgnoreCase);
}
#endregion
#region LineSeries Tests
[Fact]
public void UchannelIndicator_HasCorrectLineSeries()
public void UchannelIndicator_HasFiveLineSeries()
{
var indicator = new UchannelIndicator();
// The indicator should have 5 line series: Middle, Upper, Lower, STR, Width
// The constructor adds 5 line series: Middle, Upper, Lower, STR, Width
Assert.Equal(5, indicator.LinesSeries.Count);
}
[Fact]
public void UchannelIndicator_LineSeries_HaveCorrectNames()
{
var indicator = new UchannelIndicator();
Assert.Equal("Middle", indicator.LinesSeries[0].Name);
Assert.Equal("Upper", indicator.LinesSeries[1].Name);
Assert.Equal("Lower", indicator.LinesSeries[2].Name);
Assert.Equal("STR", indicator.LinesSeries[3].Name);
Assert.Equal("Width", indicator.LinesSeries[4].Name);
}
#endregion
#region Initialize Tests
[Fact]
public void UchannelIndicator_Initialize_DoesNotThrow()
{
var indicator = new UchannelIndicator
{
StrPeriod = 10,
CenterPeriod = 15,
Multiplier = 1.5
};
indicator.Initialize();
Assert.NotNull(indicator);
}
[Fact]
public void UchannelIndicator_Initialize_PreservesLineSeries()
{
var indicator = new UchannelIndicator();
indicator.Initialize();
// Line series should still be present after init
Assert.Equal(5, indicator.LinesSeries.Count);
}
#endregion
#region ProcessUpdate Tests
[Fact]
public void UchannelIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new UchannelIndicator { StrPeriod = 5, CenterPeriod = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
// All 5 line series should have values
for (int i = 0; i < 5; i++)
{
Assert.Equal(1, indicator.LinesSeries[i].Count);
Assert.True(double.IsFinite(indicator.LinesSeries[i].GetValue(0)));
}
}
[Fact]
public void UchannelIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new UchannelIndicator { StrPeriod = 5, CenterPeriod = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106, 1500);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
Assert.Equal(2, indicator.LinesSeries[0].Count);
}
[Fact]
public void UchannelIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
{
var indicator = new UchannelIndicator { StrPeriod = 5, CenterPeriod = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
double firstValue = indicator.LinesSeries[0].GetValue(0);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
double secondValue = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(firstValue));
Assert.True(double.IsFinite(secondValue));
}
#endregion
#region Multiple Updates Tests
[Fact]
public void UchannelIndicator_MultipleUpdates_ProducesCorrectSequence()
{
var indicator = new UchannelIndicator { StrPeriod = 5, CenterPeriod = 5, Multiplier = 1.5 };
indicator.Initialize();
var now = DateTime.UtcNow;
double[] closes = { 100, 102, 104, 103, 105, 107, 106, 108, 110, 109 };
for (int i = 0; i < closes.Length; i++)
{
double close = closes[i];
indicator.HistoricalData.AddBar(now.AddMinutes(i), close, close + 3, close - 3, close, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
// All 5 series should have values for each bar
for (int s = 0; s < 5; s++)
{
Assert.Equal(closes.Length, indicator.LinesSeries[s].Count);
}
// All last values should be finite
for (int s = 0; s < 5; s++)
{
Assert.True(double.IsFinite(indicator.LinesSeries[s].GetValue(0)));
}
}
#endregion
#region Band Relationship Tests
[Fact]
public void UchannelIndicator_BandRelationships_AreCorrect()
{
var indicator = new UchannelIndicator { StrPeriod = 5, CenterPeriod = 5, Multiplier = 2.0 };
indicator.Initialize();
var now = DateTime.UtcNow;
// Add varied data to generate band width
double[] closes = { 100, 105, 95, 110, 90, 105, 100, 108, 92, 103 };
for (int i = 0; i < closes.Length; i++)
{
double close = closes[i];
indicator.HistoricalData.AddBar(now.AddMinutes(i), close, close + 5, close - 5, close, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
// Get last values: Middle=0, Upper=1, Lower=2, STR=3, Width=4
double middle = indicator.LinesSeries[0].GetValue(0);
double upper = indicator.LinesSeries[1].GetValue(0);
double lower = indicator.LinesSeries[2].GetValue(0);
double width = indicator.LinesSeries[4].GetValue(0);
// Band relationships: Upper >= Middle >= Lower
Assert.True(upper >= middle, $"Upper ({upper}) should be >= Middle ({middle})");
Assert.True(middle >= lower, $"Middle ({middle}) should be >= Lower ({lower})");
// Width = Upper - Lower
Assert.Equal(upper - lower, width, 6);
}
#endregion
#region Multiplier Tests
[Fact]
public void UchannelIndicator_Multiplier_AffectsBandWidth()
{
var indicator1 = new UchannelIndicator { StrPeriod = 5, CenterPeriod = 5, Multiplier = 1.0 };
var indicator2 = new UchannelIndicator { StrPeriod = 5, CenterPeriod = 5, Multiplier = 2.0 };
indicator1.Initialize();
indicator2.Initialize();
var now = DateTime.UtcNow;
double[] closes = { 100, 105, 95, 110, 90, 105, 100, 108, 92, 103 };
for (int i = 0; i < closes.Length; i++)
{
double close = closes[i];
indicator1.HistoricalData.AddBar(now.AddMinutes(i), close, close + 5, close - 5, close, 1000);
indicator1.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
indicator2.HistoricalData.AddBar(now.AddMinutes(i), close, close + 5, close - 5, close, 1000);
indicator2.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
double width1 = indicator1.LinesSeries[4].GetValue(0);
double width2 = indicator2.LinesSeries[4].GetValue(0);
// Width2 should be approximately 2x Width1
Assert.True(Math.Abs(width2 - 2 * width1) < 0.0001,
$"Width2 ({width2}) should be ~2x Width1 ({width1})");
}
#endregion
#region Different Period Tests
[Fact]
public void UchannelIndicator_DifferentPeriods_ProduceDifferentResults()
{
var indicator1 = new UchannelIndicator { StrPeriod = 5, CenterPeriod = 5 };
var indicator2 = new UchannelIndicator { StrPeriod = 20, CenterPeriod = 20 };
indicator1.Initialize();
indicator2.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 30; i++)
{
double close = 100 + (i % 5) * 2;
indicator1.HistoricalData.AddBar(now.AddMinutes(i), close, close + 3, close - 3, close, 1000);
indicator1.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
indicator2.HistoricalData.AddBar(now.AddMinutes(i), close, close + 3, close - 3, close, 1000);
indicator2.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
double middle1 = indicator1.LinesSeries[0].GetValue(0);
double middle2 = indicator2.LinesSeries[0].GetValue(0);
// Different smoothing periods should produce different middle values
Assert.NotEqual(middle1, middle2);
}
#endregion
#region STR Series Tests
[Fact]
public void UchannelIndicator_STR_IsNonNegative()
{
var indicator = new UchannelIndicator { StrPeriod = 5, CenterPeriod = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
double[] closes = { 100, 105, 95, 110, 90, 105, 100, 108, 92, 103 };
for (int i = 0; i < closes.Length; i++)
{
double close = closes[i];
indicator.HistoricalData.AddBar(now.AddMinutes(i), close, close + 5, close - 5, close, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
// STR (smoothed true range) should be non-negative
double str = indicator.LinesSeries[3].GetValue(0);
Assert.True(str >= 0, $"STR ({str}) should be >= 0");
}
#endregion
#region ShowColdValues Tests
[Fact]
public void UchannelIndicator_ShowColdValues_True_ShowsValues()
{
var indicator = new UchannelIndicator
{
StrPeriod = 50,
CenterPeriod = 50,
ShowColdValues = true
};
indicator.Initialize();
var now = DateTime.UtcNow;
// Add fewer bars than warmup
for (int i = 0; i < 5; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 105, 95, 102, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
// With ShowColdValues = true, values should be shown even before warmup
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
}
[Fact]
public void UchannelIndicator_ShowColdValues_False_SetsNaN()
{
var indicator = new UchannelIndicator
{
StrPeriod = 50,
CenterPeriod = 50,
ShowColdValues = false
};
indicator.Initialize();
var now = DateTime.UtcNow;
// Add fewer bars than warmup
for (int i = 0; i < 5; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 105, 95, 102, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
// With ShowColdValues = false, cold values should be NaN before warmup
Assert.True(double.IsNaN(indicator.LinesSeries[0].GetValue(0)));
}
#endregion
}
+226 -27
View File
@@ -4,6 +4,8 @@ namespace QuanTAlib.Tests;
public class VwapsdIndicatorTests
{
// ── Constructor & Defaults ──────────────────────────────────────────
[Fact]
public void VwapsdIndicator_Constructor_SetsDefaults()
{
@@ -16,6 +18,37 @@ public class VwapsdIndicatorTests
Assert.True(indicator.OnBackGround);
}
[Fact]
public void VwapsdIndicator_Constructor_Description_IsNotEmpty()
{
var indicator = new VwapsdIndicator();
Assert.False(string.IsNullOrWhiteSpace(indicator.Description));
Assert.Contains("volume", indicator.Description, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void VwapsdIndicator_Constructor_CreatesFourLineSeries()
{
var indicator = new VwapsdIndicator();
Assert.Equal(4, indicator.LinesSeries.Count);
}
[Fact]
public void VwapsdIndicator_Constructor_LineSeriesNames_BeforeInit()
{
var indicator = new VwapsdIndicator();
// Before OnInit, series have their constructor names
Assert.Equal("VWAP", indicator.LinesSeries[0].Name);
Assert.Equal("Upper", indicator.LinesSeries[1].Name);
Assert.Equal("Lower", indicator.LinesSeries[2].Name);
Assert.Equal("Width", indicator.LinesSeries[3].Name);
}
// ── MinHistoryDepths ────────────────────────────────────────────────
[Fact]
public void VwapsdIndicator_MinHistoryDepths_EqualsTwo()
{
@@ -25,6 +58,16 @@ public class VwapsdIndicatorTests
Assert.Equal(2, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
// ── ShortName ───────────────────────────────────────────────────────
[Fact]
public void VwapsdIndicator_ShortName_DefaultFormat()
{
var indicator = new VwapsdIndicator();
Assert.Equal("VWAPSD (2.0)", indicator.ShortName);
}
[Fact]
public void VwapsdIndicator_ShortName_IncludesNumDevs()
{
@@ -34,37 +77,92 @@ public class VwapsdIndicatorTests
Assert.Contains("2.5", indicator.ShortName, StringComparison.Ordinal);
}
// ── SourceCodeLink ──────────────────────────────────────────────────
[Fact]
public void VwapsdIndicator_Initialize_CreatesFourLineSeries()
public void VwapsdIndicator_SourceCodeLink_PointsToGitHub()
{
var indicator = new VwapsdIndicator();
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.OrdinalIgnoreCase);
Assert.Contains("Vwapsd.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
}
// ── OnInit σ Rename ─────────────────────────────────────────────────
[Fact]
public void VwapsdIndicator_Initialize_RenamesSeriesWithSigmaNotation()
{
var indicator = new VwapsdIndicator { NumDevs = 2.0 };
indicator.Initialize();
// After OnInit, Upper/Lower should have σ notation
Assert.Equal("Upper (+2.0σ)", indicator.LinesSeries[1].Name);
Assert.Equal("Lower (-2.0σ)", indicator.LinesSeries[2].Name);
}
[Fact]
public void VwapsdIndicator_Initialize_SigmaNotation_ReflectsNumDevs()
{
var indicator = new VwapsdIndicator { NumDevs = 1.5 };
indicator.Initialize();
Assert.Equal("Upper (+1.5σ)", indicator.LinesSeries[1].Name);
Assert.Equal("Lower (-1.5σ)", indicator.LinesSeries[2].Name);
}
[Fact]
public void VwapsdIndicator_Initialize_PreservesSeriesCount()
{
var indicator = new VwapsdIndicator { NumDevs = 2.0 };
// Initialize should not throw
indicator.Initialize();
// After init, line series should exist (VWAP, Upper, Lower, Width)
indicator.Initialize();
Assert.Equal(4, indicator.LinesSeries.Count);
}
// ── Parameters ──────────────────────────────────────────────────────
[Fact]
public void VwapsdIndicator_Parameters_CanBeChanged()
{
var indicator = new VwapsdIndicator { NumDevs = 1.5 };
Assert.Equal(1.5, indicator.NumDevs);
indicator.NumDevs = 2.5;
Assert.Equal(2.5, indicator.NumDevs);
}
[Fact]
public void VwapsdIndicator_ShowColdValues_CanBeChanged()
{
var indicator = new VwapsdIndicator();
Assert.True(indicator.ShowColdValues);
indicator.ShowColdValues = false;
Assert.False(indicator.ShowColdValues);
}
// ── ProcessUpdate: HistoricalBar ────────────────────────────────────
[Fact]
public void VwapsdIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new VwapsdIndicator { NumDevs = 2.0 };
indicator.Initialize();
// Add historical data with volume
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
// Process update
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
// Line series should have values
Assert.Equal(1, indicator.LinesSeries[0].Count);
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
}
// ── ProcessUpdate: NewBar ───────────────────────────────────────────
[Fact]
public void VwapsdIndicator_ProcessUpdate_NewBar_ComputesValue()
{
@@ -81,6 +179,8 @@ public class VwapsdIndicatorTests
Assert.Equal(2, indicator.LinesSeries[0].Count);
}
// ── ProcessUpdate: NewTick ──────────────────────────────────────────
[Fact]
public void VwapsdIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
{
@@ -100,6 +200,8 @@ public class VwapsdIndicatorTests
Assert.True(double.IsFinite(secondValue));
}
// ── MultipleUpdates ─────────────────────────────────────────────────
[Fact]
public void VwapsdIndicator_MultipleUpdates_ProducesCorrectSequence()
{
@@ -129,15 +231,7 @@ public class VwapsdIndicatorTests
Assert.True(lastVwap >= 95 && lastVwap <= 110);
}
[Fact]
public void VwapsdIndicator_Parameters_CanBeChanged()
{
var indicator = new VwapsdIndicator { NumDevs = 1.5 };
Assert.Equal(1.5, indicator.NumDevs);
indicator.NumDevs = 2.5;
Assert.Equal(2.5, indicator.NumDevs);
}
// ── AllBandsUpdate ──────────────────────────────────────────────────
[Fact]
public void VwapsdIndicator_AllBandsUpdate_Correctly()
@@ -161,6 +255,8 @@ public class VwapsdIndicatorTests
}
}
// ── BandRelationships ───────────────────────────────────────────────
[Fact]
public void VwapsdIndicator_BandRelationships_AreCorrect()
{
@@ -168,7 +264,6 @@ public class VwapsdIndicatorTests
indicator.Initialize();
var now = DateTime.UtcNow;
// Add varied data to generate band width
double[] closes = { 100, 105, 95, 110, 90, 105, 100, 108, 92, 103 };
double[] volumes = { 1000, 1500, 2000, 1200, 1800, 1100, 1600, 1300, 1900, 1400 };
@@ -180,21 +275,19 @@ public class VwapsdIndicatorTests
now = now.AddMinutes(1);
}
// Get last values: VWAP=0, Upper=1, Lower=2, Width=3
double vwap = indicator.LinesSeries[0].GetValue(0);
double upper = indicator.LinesSeries[1].GetValue(0);
double lower = indicator.LinesSeries[2].GetValue(0);
double width = indicator.LinesSeries[3].GetValue(0);
// Band relationships: Upper > VWAP > Lower
Assert.True(upper >= vwap, $"Upper ({upper}) should be >= VWAP ({vwap})");
Assert.True(vwap >= lower, $"VWAP ({vwap}) should be >= Lower ({lower})");
// Width = Upper - Lower (2 × numDevs × StdDev)
Assert.True(Math.Abs(width - (upper - lower)) < 0.0001,
$"Width ({width}) should equal Upper - Lower ({upper - lower})");
}
// ── VolumeWeighting ─────────────────────────────────────────────────
[Fact]
public void VwapsdIndicator_VolumeWeighting_AffectsVwap()
{
@@ -205,9 +298,6 @@ public class VwapsdIndicatorTests
var now = DateTime.UtcNow;
// Same prices but different volume distributions
// Process both bars for each indicator
// Indicator1: high volume on low price, low volume on high price
indicator1.HistoricalData.AddBar(now, 100, 102, 98, 100, 10000);
indicator1.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
@@ -223,11 +313,11 @@ public class VwapsdIndicatorTests
double vwap1 = indicator1.LinesSeries[0].GetValue(0);
double vwap2 = indicator2.LinesSeries[0].GetValue(0);
// VWAP1 should be lower (weighted toward 100 due to high volume at low price)
// VWAP2 should be higher (weighted toward 110 due to high volume at high price)
Assert.True(vwap1 < vwap2, $"VWAP1 ({vwap1}) should be less than VWAP2 ({vwap2}) due to volume weighting");
}
// ── NumDevs Effect ──────────────────────────────────────────────────
[Fact]
public void VwapsdIndicator_NumDevs_AffectsBandWidth()
{
@@ -250,7 +340,6 @@ public class VwapsdIndicatorTests
now = now.AddMinutes(1);
}
// Width should be proportional to numDevs
double width1 = indicator1.LinesSeries[3].GetValue(0);
double width2 = indicator2.LinesSeries[3].GetValue(0);
@@ -258,4 +347,114 @@ public class VwapsdIndicatorTests
Assert.True(Math.Abs(width2 - 2 * width1) < 0.0001,
$"Width2 ({width2}) should be ~2x Width1 ({width1})");
}
// ── Width Non-Negative ──────────────────────────────────────────────
[Fact]
public void VwapsdIndicator_Width_IsNonNegative()
{
var indicator = new VwapsdIndicator { NumDevs = 2.0 };
indicator.Initialize();
var now = DateTime.UtcNow;
double[] closes = { 100, 102, 98, 105, 97, 103, 101, 99 };
double[] volumes = { 1000, 1200, 800, 1500, 900, 1100, 1300, 700 };
for (int i = 0; i < closes.Length; i++)
{
double close = closes[i];
indicator.HistoricalData.AddBar(now, close, close + 2, close - 2, close, volumes[i]);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
now = now.AddMinutes(1);
}
// Width should be non-negative at every bar
for (int i = 0; i < closes.Length; i++)
{
double w = indicator.LinesSeries[3].GetValue(closes.Length - 1 - i);
Assert.True(w >= 0.0, $"Width at bar {i} ({w}) should be >= 0");
}
}
// ── SingleBar Zero Width ────────────────────────────────────────────
[Fact]
public void VwapsdIndicator_SingleBar_ProducesZeroWidth()
{
var indicator = new VwapsdIndicator { NumDevs = 2.0 };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
// With only one bar, stddev is 0 → width should be 0
double width = indicator.LinesSeries[3].GetValue(0);
Assert.Equal(0.0, width, 4);
}
// ── ShowColdValues False ────────────────────────────────────────────
[Fact]
public void VwapsdIndicator_ShowColdValues_False_SuppressesColdValues()
{
var indicator = new VwapsdIndicator { NumDevs = 2.0, ShowColdValues = false };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
// With ShowColdValues=false, cold bars produce NaN
double vwap = indicator.LinesSeries[0].GetValue(0);
// Value is either NaN (suppressed) or finite (hot)
Assert.True(double.IsNaN(vwap) || double.IsFinite(vwap));
}
[Fact]
public void VwapsdIndicator_ShowColdValues_True_ShowsAllValues()
{
var indicator = new VwapsdIndicator { NumDevs = 2.0, ShowColdValues = true };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
// With ShowColdValues=true, all values should be finite
double vwap = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(vwap));
}
// ── ReInitialize Updates Series Names ───────────────────────────────
[Fact]
public void VwapsdIndicator_ReInitialize_UpdatesSigmaNotation()
{
var indicator = new VwapsdIndicator { NumDevs = 2.0 };
indicator.Initialize();
Assert.Equal("Upper (+2.0σ)", indicator.LinesSeries[1].Name);
Assert.Equal("Lower (-2.0σ)", indicator.LinesSeries[2].Name);
// Change NumDevs and re-init
indicator.NumDevs = 3.0;
indicator.Initialize();
Assert.Equal("Upper (+3.0σ)", indicator.LinesSeries[1].Name);
Assert.Equal("Lower (-3.0σ)", indicator.LinesSeries[2].Name);
}
// ── VWAP Series Name Unchanged After Init ───────────────────────────
[Fact]
public void VwapsdIndicator_Initialize_VwapAndWidthNames_Unchanged()
{
var indicator = new VwapsdIndicator { NumDevs = 2.0 };
indicator.Initialize();
// VWAP and Width series names should remain as constructor set them
Assert.Equal("VWAP", indicator.LinesSeries[0].Name);
Assert.Equal("Width", indicator.LinesSeries[3].Name);
}
}