mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-13 08:08:05 +00:00
feat: Add Prime method to various indicators for initializing state with historical data
- Implemented Prime method in Vel, Ao, Apo, Frama, Adl, Adosc, Aobv, Cmf, Efi, Eom, Iii, Kvo, Mfi, Nvi, Obv, Pvd, Pvi, Pvo, Pvr, Pvt, Tvi, Twap, Va, Vf, Vo, Vroc, Vwad, Vwap, and Vwma classes. - The Prime method resets the indicator state and processes the provided historical bar data to initialize the indicator. - Added warmup period property to Adl and Wad classes to define the minimum number of data points required for validity. - Updated benchmark tests to use Batch methods for performance evaluation.
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user