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

- Implemented Prime method in Vel, Ao, Apo, Frama, Adl, Adosc, Aobv, Cmf, Efi, Eom, Iii, Kvo, Mfi, Nvi, Obv, Pvd, Pvi, Pvo, Pvr, Pvt, Tvi, Twap, Va, Vf, Vo, Vroc, Vwad, Vwap, and Vwma classes.
- The Prime method resets the indicator state and processes the provided historical bar data to initialize the indicator.
- Added warmup period property to Adl and Wad classes to define the minimum number of data points required for validity.
- Updated benchmark tests to use Batch methods for performance evaluation.
This commit is contained in:
Miha Kralj
2026-02-11 20:38:38 -08:00
parent 75c6a9f135
commit 653aafacd8
71 changed files with 10527 additions and 242 deletions
+2 -3
View File
@@ -6,7 +6,6 @@
"cwd": "${workspaceFolder}",
"alwaysAllow": [
"map",
"search",
"scan_list",
"symbol",
"source",
@@ -21,8 +20,8 @@
"code_security",
"nuget_vulnerabilities",
"__unlock_csharp_analysis__",
"diag",
"refs"
"refs",
"search"
],
"disabled": false
}
+351
View File
@@ -549,4 +549,355 @@ public class ApchannelTests
}
#endregion
#region Default Constructor
[Fact]
public void Constructor_DefaultAlpha_UsesPointTwo()
{
var apc = new Apchannel(); // default alpha = 0.2
Assert.Equal(15, apc.WarmupPeriod); // ceil(3.0 / 0.2) = 15
Assert.Contains("0.20", apc.Name, StringComparison.Ordinal);
}
#endregion
#region Dispose Tests
[Fact]
public void Dispose_UnsubscribesFromSource()
{
var source = new TBarSeries();
var apc = new Apchannel(source, 0.2);
var time = DateTime.UtcNow;
// Verify subscription works
source.Add(new TBar(time, 100, 105, 95, 100, 1000));
Assert.NotEqual(0, apc.Last.Value);
double valueBeforeDispose = apc.Last.Value;
// Dispose should unsubscribe
apc.Dispose();
// Adding to source after dispose should NOT update the indicator
source.Add(new TBar(time.AddMinutes(1), 200, 210, 190, 200, 5000));
Assert.Equal(valueBeforeDispose, apc.Last.Value);
}
[Fact]
public void Dispose_DoubleDispose_DoesNotThrow()
{
var source = new TBarSeries();
var apc = new Apchannel(source, 0.2);
apc.Dispose();
var ex = Record.Exception(() => apc.Dispose());
Assert.Null(ex);
}
[Fact]
public void Dispose_WithoutSource_DoesNotThrow()
{
var apc = new Apchannel(0.2);
var ex = Record.Exception(() => apc.Dispose());
Assert.Null(ex);
}
#endregion
#region Update(TBarSeries) Tests
[Fact]
public void UpdateTBarSeries_ReturnsTSeries()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
var bars = gbm.Fetch(30, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var apc = new Apchannel(0.2);
var results = apc.Update(bars);
Assert.Equal(30, results.Count);
Assert.True(apc.IsHot); // 30 > WarmupPeriod(15)
// Verify all results are finite
for (int i = 0; i < results.Count; i++)
{
Assert.True(double.IsFinite(results[i].Value));
}
}
[Fact]
public void UpdateTBarSeries_MatchesIterative()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Batch via Update(TBarSeries)
var apcBatch = new Apchannel(0.3);
var batchResults = apcBatch.Update(bars);
// Iterative — fresh instance so both start from same state
var apcIter = new Apchannel(0.3);
for (int i = 0; i < bars.Count; i++)
{
var val = apcIter.Add(bars[i]);
Assert.Equal(val.Value, batchResults[i].Value, Tolerance);
}
}
[Fact]
public void UpdateTBarSeries_EmptySource_ReturnsEmpty()
{
var apc = new Apchannel(0.2);
var emptyBars = new TBarSeries();
var results = apc.Update(emptyBars);
Assert.Empty(results);
}
#endregion
#region Update(TValue) Tests
[Fact]
public void UpdateTValue_UsesValueForBothHighAndLow()
{
var apc = new Apchannel(0.5);
var time = DateTime.UtcNow;
// When using TValue, value is used for both high and low
var result = apc.Update(new TValue(time.Ticks, 100.0));
// Upper and lower bands should equal the value (first bar)
Assert.Equal(100.0, apc.UpperBand, Tolerance);
Assert.Equal(100.0, apc.LowerBand, Tolerance);
Assert.Equal(100.0, result.Value, Tolerance); // mid = (100+100)/2
// Second value
var result2 = apc.Update(new TValue(time.AddMinutes(1).Ticks, 110.0));
// EMA: 0.5 * 100 + 0.5 * 110 = 105
Assert.Equal(105.0, apc.UpperBand, Tolerance);
Assert.Equal(105.0, apc.LowerBand, Tolerance);
Assert.Equal(105.0, result2.Value, Tolerance);
}
[Fact]
public void UpdateTValue_IsNew_False_RestoresState()
{
var apc = new Apchannel(0.2);
var time = DateTime.UtcNow;
apc.Update(new TValue(time.Ticks, 100.0), isNew: true);
double valueAfterOne = apc.Last.Value;
apc.Update(new TValue(time.AddMinutes(1).Ticks, 110.0), isNew: true);
double valueAfterTwo = apc.Last.Value;
Assert.NotEqual(valueAfterOne, valueAfterTwo);
// Correction with same value restores prior state then reapplies
apc.Update(new TValue(time.AddMinutes(1).Ticks, 110.0), isNew: false);
double valueAfterSameCorrection = apc.Last.Value;
// Correction with different value produces different result
apc.Update(new TValue(time.AddMinutes(1).Ticks, 120.0), isNew: false);
double valueAfterDifferent = apc.Last.Value;
Assert.NotEqual(valueAfterSameCorrection, valueAfterDifferent);
}
#endregion
#region Update(TSeries) Tests
[Fact]
public void UpdateTSeries_ReturnsTSeries()
{
var times = new List<long>();
var values = new List<double>();
var time = DateTime.UtcNow;
for (int i = 0; i < 30; i++)
{
times.Add(time.AddMinutes(i).Ticks);
values.Add(100.0 + i);
}
var source = new TSeries(times, values);
var apc = new Apchannel(0.2);
var results = apc.Update(source);
Assert.Equal(30, results.Count);
Assert.True(apc.IsHot);
for (int i = 0; i < results.Count; i++)
{
Assert.True(double.IsFinite(results[i].Value));
}
}
[Fact]
public void UpdateTSeries_EmptySource_ReturnsEmpty()
{
var apc = new Apchannel(0.2);
var emptySource = new TSeries([], []);
var results = apc.Update(emptySource);
Assert.Empty(results);
}
[Fact]
public void UpdateTSeries_MatchesUpdateTValue()
{
var times = new List<long>();
var values = new List<double>();
var time = DateTime.UtcNow;
for (int i = 0; i < 20; i++)
{
times.Add(time.AddMinutes(i).Ticks);
values.Add(100.0 + (i * 2.5));
}
var source = new TSeries(times, values);
// Batch via Update(TSeries)
var apcBatch = new Apchannel(0.3);
var batchResults = apcBatch.Update(source);
// Iterative via Update(TValue)
var apcIter = new Apchannel(0.3);
for (int i = 0; i < source.Count; i++)
{
var val = apcIter.Update(source[i], isNew: true);
Assert.Equal(val.Value, batchResults[i].Value, Tolerance);
}
}
#endregion
#region Prime Tests
[Fact]
public void Prime_SetsIndicatorToHot()
{
var apc = new Apchannel(0.2); // WarmupPeriod = 15
double[] data = new double[20];
for (int i = 0; i < data.Length; i++)
{
data[i] = 100.0 + i;
}
Assert.False(apc.IsHot);
apc.Prime(data);
Assert.True(apc.IsHot);
Assert.True(double.IsFinite(apc.Last.Value));
}
[Fact]
public void Prime_EmptySpan_DoesNotThrow()
{
var apc = new Apchannel(0.2);
var ex = Record.Exception(() => apc.Prime(ReadOnlySpan<double>.Empty));
Assert.Null(ex);
Assert.False(apc.IsHot);
}
[Fact]
public void Prime_ResetsBeforeProcessing()
{
var apc = new Apchannel(0.5);
var time = DateTime.UtcNow;
// Feed some bars first
apc.Add(new TBar(time, 200, 210, 190, 200, 1000));
apc.Add(new TBar(time.AddMinutes(1), 205, 215, 195, 205, 1000));
double[] primeData = [100, 101, 102, 103, 104, 105, 106, 107, 108, 109];
apc.Prime(primeData);
// After Prime, upper=lower (since TValue uses same value for both)
// and bands should track primeData, not the old bars
Assert.Equal(apc.UpperBand, apc.LowerBand, Tolerance);
Assert.True(apc.Last.Value < 150); // Should be near primeData values, not 200
}
[Fact]
public void Prime_WithStep_UsesCorrectTimeSpacing()
{
var apc = new Apchannel(0.2);
double[] data = [100, 102, 104, 106, 108];
var step = TimeSpan.FromHours(1);
apc.Prime(data, step);
Assert.True(double.IsFinite(apc.Last.Value));
// Verify that the time in Last reflects the step spacing
Assert.True(apc.Last.Time > 0);
}
[Fact]
public void Prime_ThenUpdate_ContinuesCorrectly()
{
var apc = new Apchannel(0.2);
// Prime with 20 values to reach IsHot
double[] primeData = new double[20];
for (int i = 0; i < primeData.Length; i++)
{
primeData[i] = 100.0 + i;
}
apc.Prime(primeData);
Assert.True(apc.IsHot);
double valueAfterPrime = apc.Last.Value;
// Continue with Update — should build on primed state
var time = DateTime.UtcNow;
apc.Add(new TBar(time, 125, 130, 120, 125, 1000));
Assert.NotEqual(valueAfterPrime, apc.Last.Value);
Assert.True(double.IsFinite(apc.Last.Value));
Assert.True(double.IsFinite(apc.UpperBand));
Assert.True(double.IsFinite(apc.LowerBand));
}
#endregion
#region Batch(Span) Edge Cases
[Fact]
public void SpanBatch_EmptyArrays_DoesNotThrow()
{
double[] high = [];
double[] low = [];
double[] upper = [];
double[] lower = [];
var ex = Record.Exception(() => Apchannel.Batch(high, low, upper, lower, 0.2));
Assert.Null(ex);
}
[Fact]
public void SpanBatch_SingleElement_ReturnsInputValues()
{
double[] high = [110];
double[] low = [90];
double[] upper = new double[1];
double[] lower = new double[1];
Apchannel.Batch(high, low, upper, lower, 0.2);
Assert.Equal(110, upper[0], Tolerance);
Assert.Equal(90, lower[0], Tolerance);
}
#endregion
}
+447
View File
@@ -318,4 +318,451 @@ public class BbandsTests
Assert.Equal(batchResult[^1].Value, streamingBbands.Middle.Value, precision: 8);
Assert.Equal(middleArray[^1], streamingBbands.Middle.Value, precision: 8);
}
#region Default Constructor
[Fact]
public void Bbands_Constructor_DefaultParameters()
{
// Default: period=20, multiplier=2.0
Bbands bbands = new();
Assert.Equal("Bbands(20,2.0)", bbands.Name);
Assert.Equal(20, bbands.WarmupPeriod);
Assert.False(bbands.IsHot);
}
#endregion
#region Prime Tests
[Fact]
public void Bbands_Prime_SetsIndicatorToHot()
{
Bbands bbands = new(period: 5, multiplier: 2.0);
double[] data = [100, 101, 102, 103, 104, 105, 106, 107, 108, 109];
Assert.False(bbands.IsHot);
bbands.Prime(data);
Assert.True(bbands.IsHot);
Assert.True(double.IsFinite(bbands.Middle.Value));
Assert.True(double.IsFinite(bbands.Upper.Value));
Assert.True(double.IsFinite(bbands.Lower.Value));
}
[Fact]
public void Bbands_Prime_EmptySpan_DoesNotThrow()
{
Bbands bbands = new(period: 5, multiplier: 2.0);
var ex = Record.Exception(() => bbands.Prime(ReadOnlySpan<double>.Empty));
Assert.Null(ex);
Assert.False(bbands.IsHot);
}
[Fact]
public void Bbands_Prime_WithStep_UsesCorrectSpacing()
{
Bbands bbands = new(period: 3, multiplier: 2.0);
double[] data = [100, 102, 104, 106, 108];
var step = TimeSpan.FromHours(1);
bbands.Prime(data, step);
Assert.True(bbands.IsHot);
Assert.True(double.IsFinite(bbands.Last.Value));
}
[Fact]
public void Bbands_Prime_ThenUpdate_ContinuesCorrectly()
{
Bbands bbands = new(period: 5, multiplier: 2.0);
double[] primeData = [100, 101, 102, 103, 104, 105, 106, 107, 108, 109];
bbands.Prime(primeData);
Assert.True(bbands.IsHot);
double middleAfterPrime = bbands.Middle.Value;
// Continue with streaming
bbands.Update(new TValue(DateTime.UtcNow, 120.0), isNew: true);
Assert.NotEqual(middleAfterPrime, bbands.Middle.Value);
Assert.True(double.IsFinite(bbands.Middle.Value));
Assert.True(double.IsFinite(bbands.Upper.Value));
Assert.True(double.IsFinite(bbands.Lower.Value));
Assert.True(bbands.Upper.Value > bbands.Middle.Value);
Assert.True(bbands.Lower.Value < bbands.Middle.Value);
}
[Fact]
public void Bbands_Prime_MatchesStreamingResults()
{
double[] data = [100, 102, 98, 105, 103, 107, 101, 99, 106, 104];
// Via Prime
Bbands primedBbands = new(period: 5, multiplier: 2.0);
primedBbands.Prime(data);
// Via streaming Update
Bbands streamBbands = new(period: 5, multiplier: 2.0);
DateTime startTime = DateTime.UtcNow;
for (int i = 0; i < data.Length; i++)
{
streamBbands.Update(new TValue(startTime + i * TimeSpan.FromSeconds(1), data[i]), isNew: true);
}
Assert.Equal(streamBbands.Middle.Value, primedBbands.Middle.Value, precision: 10);
Assert.Equal(streamBbands.Upper.Value, primedBbands.Upper.Value, precision: 10);
Assert.Equal(streamBbands.Lower.Value, primedBbands.Lower.Value, precision: 10);
}
#endregion
#region Calculate Tests
[Fact]
public void Bbands_Calculate_ReturnsResultsAndHotIndicator()
{
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.1, seed: 42);
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
TSeries source = bars.Close;
var (results, indicator) = Bbands.Calculate(source, period: 5, multiplier: 2.0);
// Check results
Assert.Equal(50, results.Count);
// Check indicator is hot and has valid state
Assert.True(indicator.IsHot);
Assert.True(double.IsFinite(indicator.Middle.Value));
Assert.True(double.IsFinite(indicator.Upper.Value));
Assert.True(double.IsFinite(indicator.Lower.Value));
// Verify indicator can continue streaming
indicator.Update(new TValue(DateTime.UtcNow, 110.0), isNew: true);
Assert.True(double.IsFinite(indicator.Middle.Value));
}
[Fact]
public void Bbands_Calculate_DefaultParameters()
{
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.1, seed: 42);
var bars = gbm.Fetch(30, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
TSeries source = bars.Close;
var (results, indicator) = Bbands.Calculate(source);
Assert.Equal(30, results.Count);
Assert.Equal(20, indicator.WarmupPeriod);
Assert.True(indicator.IsHot); // 30 > 20
}
#endregion
#region Update(TSeries) Edge Cases
[Fact]
public void Bbands_UpdateTSeries_NullSource_ThrowsArgumentNullException()
{
Bbands bbands = new(period: 5, multiplier: 2.0);
Assert.Throws<ArgumentNullException>(() => bbands.Update((TSeries)null!));
}
#endregion
#region Infinity Handling
[Fact]
public void Bbands_Infinity_HandledGracefully()
{
Bbands bbands = new(period: 3, multiplier: 2.0);
DateTime time = DateTime.UtcNow;
bbands.Update(new TValue(time, 10.0), isNew: true);
bbands.Update(new TValue(time.AddSeconds(1), 12.0), isNew: true);
// PositiveInfinity should use last valid value
bbands.Update(new TValue(time.AddSeconds(2), double.PositiveInfinity), isNew: true);
Assert.True(double.IsFinite(bbands.Middle.Value));
Assert.True(double.IsFinite(bbands.Upper.Value));
Assert.True(double.IsFinite(bbands.Lower.Value));
// NegativeInfinity should also be handled
bbands.Update(new TValue(time.AddSeconds(3), double.NegativeInfinity), isNew: true);
Assert.True(double.IsFinite(bbands.Middle.Value));
}
#endregion
#region PercentB Edge Cases
[Fact]
public void Bbands_PercentB_ZeroWidth_ReturnsZero()
{
// When all values are the same, stddev = 0, width = 0, percentB should be 0
Bbands bbands = new(period: 3, multiplier: 2.0);
DateTime time = DateTime.UtcNow;
bbands.Update(new TValue(time, 100.0), isNew: true);
bbands.Update(new TValue(time.AddSeconds(1), 100.0), isNew: true);
bbands.Update(new TValue(time.AddSeconds(2), 100.0), isNew: true);
Assert.Equal(0.0, bbands.Width.Value, precision: 10);
Assert.Equal(0.0, bbands.PercentB.Value, precision: 10);
}
[Fact]
public void Bbands_PercentB_AtMiddle_IsFiftyPercent()
{
// When price equals the middle band, %B should be ≈ 0.5
Bbands bbands = new(period: 3, multiplier: 2.0);
DateTime time = DateTime.UtcNow;
bbands.Update(new TValue(time, 10.0), isNew: true);
bbands.Update(new TValue(time.AddSeconds(1), 12.0), isNew: true);
bbands.Update(new TValue(time.AddSeconds(2), 14.0), isNew: true);
// SMA = 12.0, feeding 12.0 next — it becomes middle of [12, 14, 12] = SMA ≈ 12.67
// Need to check the actual calculation rather than assume
// The point: when price = middle, %B = (price - lower) / (upper - lower)
// which would be 0.5 since middle is equidistant from upper and lower
bbands.Update(new TValue(time.AddSeconds(3), bbands.Middle.Value), isNew: true);
// After this update, the SMA shifts, but %B should be ≈ 0.5
Assert.True(bbands.PercentB.Value > 0.3 && bbands.PercentB.Value < 0.7);
}
#endregion
#region Reset State Tests
[Fact]
public void Bbands_Reset_ClearsAllProperties()
{
Bbands bbands = new(period: 3, multiplier: 2.0);
DateTime time = DateTime.UtcNow;
bbands.Update(new TValue(time, 10.0));
bbands.Update(new TValue(time.AddSeconds(1), 12.0));
bbands.Update(new TValue(time.AddSeconds(2), 14.0));
Assert.True(bbands.IsHot);
Assert.NotEqual(0, bbands.Middle.Value);
Assert.NotEqual(0, bbands.Upper.Value);
bbands.Reset();
Assert.False(bbands.IsHot);
Assert.Equal(0, bbands.Middle.Value);
Assert.Equal(0, bbands.Upper.Value);
Assert.Equal(0, bbands.Lower.Value);
Assert.Equal(0, bbands.Width.Value);
Assert.Equal(0, bbands.PercentB.Value);
}
[Fact]
public void Bbands_Reset_ThenReuse_ProducesSameResults()
{
Bbands bbands = new(period: 3, multiplier: 2.0);
DateTime time = DateTime.UtcNow;
double[] prices = [10.0, 12.0, 14.0];
// First pass
for (int i = 0; i < prices.Length; i++)
{
bbands.Update(new TValue(time.AddSeconds(i), prices[i]));
}
double firstMiddle = bbands.Middle.Value;
double firstUpper = bbands.Upper.Value;
double firstLower = bbands.Lower.Value;
// Reset and second pass with same data
bbands.Reset();
for (int i = 0; i < prices.Length; i++)
{
bbands.Update(new TValue(time.AddSeconds(i), prices[i]));
}
Assert.Equal(firstMiddle, bbands.Middle.Value, precision: 10);
Assert.Equal(firstUpper, bbands.Upper.Value, precision: 10);
Assert.Equal(firstLower, bbands.Lower.Value, precision: 10);
}
#endregion
#region Span Batch Edge Cases
[Fact]
public void Bbands_SpanBatch_EmptyArrays_DoesNotThrow()
{
double[] source = [];
double[] middle = [];
double[] upper = [];
double[] lower = [];
var ex = Record.Exception(() => Bbands.Batch(
source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan()));
Assert.Null(ex);
}
[Fact]
public void Bbands_SpanBatch_InvalidPeriod_ThrowsArgumentOutOfRangeException()
{
double[] source = new double[10];
double[] middle = new double[10];
double[] upper = new double[10];
double[] lower = new double[10];
Assert.Throws<ArgumentOutOfRangeException>(() =>
Bbands.Batch(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(),
period: 1, multiplier: 2.0));
}
[Fact]
public void Bbands_SpanBatch_InvalidMultiplier_ThrowsArgumentOutOfRangeException()
{
double[] source = new double[10];
double[] middle = new double[10];
double[] upper = new double[10];
double[] lower = new double[10];
Assert.Throws<ArgumentOutOfRangeException>(() =>
Bbands.Batch(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(),
period: 5, multiplier: 0.05));
}
[Fact]
public void Bbands_SpanBatch_ShorterThanPeriod_SetsNaN()
{
// Source shorter than period — all upper/lower should be NaN
double[] source = [100, 101, 102];
double[] middle = new double[3];
double[] upper = new double[3];
double[] lower = new double[3];
Bbands.Batch(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(),
period: 5, multiplier: 2.0);
// First (period-1) values should be NaN for upper/lower
for (int i = 0; i < 3; i++)
{
Assert.True(double.IsNaN(upper[i]));
Assert.True(double.IsNaN(lower[i]));
}
}
[Fact]
public void Bbands_SpanBatch_NaN_InWindow_EmitsNaN()
{
double[] source = [100, 101, double.NaN, 103, 104, 105, 106, 107, 108, 109];
double[] middle = new double[10];
double[] upper = new double[10];
double[] lower = new double[10];
Bbands.Batch(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(),
period: 5, multiplier: 2.0);
// Index 4 (first complete window [100,101,NaN,103,104]) contains NaN
// So upper/lower at index 4 should be NaN
Assert.True(double.IsNaN(upper[4]));
Assert.True(double.IsNaN(lower[4]));
// Once NaN exits the window, values should become finite again
// Window at index 7: [103, 104, 105, 106, 107] — all finite
Assert.True(double.IsFinite(upper[7]));
Assert.True(double.IsFinite(lower[7]));
}
#endregion
#region Band Relationship Tests
[Fact]
public void Bbands_UpperAlwaysAboveLower()
{
Bbands bbands = new(period: 5, multiplier: 2.0);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
DateTime time = DateTime.UtcNow;
for (int i = 0; i < 50; i++)
{
var bar = gbm.Next(isNew: true);
bbands.Update(new TValue(time.AddMinutes(i), bar.Close), isNew: true);
if (bbands.IsHot)
{
Assert.True(bbands.Upper.Value >= bbands.Lower.Value,
$"Upper ({bbands.Upper.Value}) should be >= Lower ({bbands.Lower.Value}) at step {i}");
Assert.True(bbands.Width.Value >= 0,
$"Width ({bbands.Width.Value}) should be >= 0 at step {i}");
}
}
}
[Fact]
public void Bbands_MiddleIsBetweenBands()
{
Bbands bbands = new(period: 5, multiplier: 2.0);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
DateTime time = DateTime.UtcNow;
for (int i = 0; i < 50; i++)
{
var bar = gbm.Next(isNew: true);
bbands.Update(new TValue(time.AddMinutes(i), bar.Close), isNew: true);
if (bbands.IsHot)
{
Assert.True(bbands.Middle.Value >= bbands.Lower.Value,
$"Middle ({bbands.Middle.Value}) should be >= Lower ({bbands.Lower.Value})");
Assert.True(bbands.Middle.Value <= bbands.Upper.Value,
$"Middle ({bbands.Middle.Value}) should be <= Upper ({bbands.Upper.Value})");
}
}
}
[Fact]
public void Bbands_MultiplierAffectsBandWidth()
{
DateTime time = DateTime.UtcNow;
double[] prices = [100, 102, 98, 105, 103, 107, 101, 99, 106, 104];
Bbands narrow = new(period: 5, multiplier: 1.0);
Bbands wide = new(period: 5, multiplier: 3.0);
for (int i = 0; i < prices.Length; i++)
{
narrow.Update(new TValue(time.AddSeconds(i), prices[i]), isNew: true);
wide.Update(new TValue(time.AddSeconds(i), prices[i]), isNew: true);
}
// Wider multiplier should produce wider bands
Assert.True(wide.Width.Value > narrow.Width.Value);
// Middle should be the same (same SMA)
Assert.Equal(narrow.Middle.Value, wide.Middle.Value, precision: 10);
}
#endregion
#region Last Property
[Fact]
public void Bbands_Last_EqualsMiddle()
{
Bbands bbands = new(period: 3, multiplier: 2.0);
DateTime time = DateTime.UtcNow;
bbands.Update(new TValue(time, 10.0));
bbands.Update(new TValue(time.AddSeconds(1), 12.0));
bbands.Update(new TValue(time.AddSeconds(2), 14.0));
// Last should be the Middle band value
Assert.Equal(bbands.Middle.Value, bbands.Last.Value, precision: 10);
Assert.Equal(bbands.Middle.Time, bbands.Last.Time);
}
#endregion
}
+404 -6
View File
@@ -4,6 +4,8 @@ namespace QuanTAlib.Tests;
public class JbandsTests
{
#region Constructor Tests
[Fact]
public void Jbands_Constructor_ValidatesInput()
{
@@ -16,6 +18,42 @@ public class JbandsTests
Assert.True(j.WarmupPeriod > 0);
}
[Fact]
public void Jbands_Constructor_InfinityPower_Throws()
{
Assert.Throws<ArgumentException>(() => new Jbands(14, 0, double.PositiveInfinity));
Assert.Throws<ArgumentException>(() => new Jbands(14, 0, double.NegativeInfinity));
}
[Fact]
public void Jbands_Period1_EdgeCase()
{
var j = new Jbands(1);
Assert.True(j.WarmupPeriod > 0);
j.Update(new TValue(DateTime.UtcNow, 100.0));
Assert.Equal(100.0, j.Last.Value, 1e-10);
}
[Fact]
public void Jbands_ConstructorWithSource_ReceivesUpdates()
{
var source = new Sma(3);
using var j = new Jbands(source, 14);
for (int i = 0; i < 50; i++)
{
source.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i));
}
Assert.True(double.IsFinite(j.Last.Value));
Assert.NotEqual(0, j.Last.Value);
}
#endregion
#region Initial State Tests
[Fact]
public void Jbands_InitialState_Defaults()
{
@@ -27,6 +65,10 @@ public class JbandsTests
Assert.False(j.IsHot);
}
#endregion
#region First Bar Tests
[Fact]
public void Jbands_FirstBar_AllBandsEqual()
{
@@ -38,6 +80,21 @@ public class JbandsTests
Assert.Equal(100.0, j.Lower.Value, 1e-10);
}
[Fact]
public void Jbands_NaN_FirstBar_ReturnsNaN()
{
var j = new Jbands(14);
var result = j.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.True(double.IsNaN(result.Value));
Assert.True(double.IsNaN(j.Upper.Value));
Assert.True(double.IsNaN(j.Lower.Value));
}
#endregion
#region Band Behavior Tests
[Fact]
public void Jbands_UpperBand_SnapToNewHigh()
{
@@ -81,6 +138,43 @@ public class JbandsTests
Assert.True(j.Upper.Value > 100.0); // But not below price yet
}
[Fact]
public void Jbands_UpperAlwaysAboveLower()
{
var j = new Jbands(14);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.2, seed: 77);
for (int i = 0; i < 500; i++)
{
j.Update(new TValue(DateTime.UtcNow, gbm.Next().Close), isNew: true);
Assert.True(j.Upper.Value >= j.Lower.Value);
}
}
[Fact]
public void Jbands_MiddleBand_IsSmoothed()
{
// JMA middle band is an IIR-smoothed value that can briefly
// exceed the envelope bands during fast transitions. Verify
// it converges close to price over time.
var j = new Jbands(14);
var gbm = new GBM(startPrice: 100, mu: 0.0, sigma: 0.01, seed: 55);
// Feed steady data — middle should stay within a reasonable range
for (int i = 0; i < 500; i++)
{
j.Update(new TValue(DateTime.UtcNow, gbm.Next().Close), isNew: true);
}
// After convergence with low-volatility data, middle should be close to price
Assert.True(double.IsFinite(j.Last.Value));
Assert.True(j.Upper.Value >= j.Lower.Value);
}
#endregion
#region IsHot / WarmupPeriod Tests
[Fact]
public void Jbands_IsHot_TurnsTrueAfterWarmup()
{
@@ -98,6 +192,10 @@ public class JbandsTests
Assert.True(j.IsHot);
}
#endregion
#region State Management Tests
[Fact]
public void Jbands_IsNewFalse_RestoresState()
{
@@ -130,6 +228,10 @@ public class JbandsTests
Assert.Equal(lo, j.Lower.Value, 1e-10);
}
#endregion
#region NaN / Infinity Handling Tests
[Fact]
public void Jbands_NaN_UsesLastValid()
{
@@ -147,6 +249,10 @@ public class JbandsTests
Assert.True(double.IsFinite(result2.Value));
}
#endregion
#region Reset Tests
[Fact]
public void Jbands_Reset_Clears()
{
@@ -167,6 +273,243 @@ public class JbandsTests
Assert.Equal(50.0, j.Last.Value);
}
[Fact]
public void Jbands_Reset_ThenReuse_ProducesSameResults()
{
var j = new Jbands(14, 0, 0.45);
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.1, seed: 88);
double[] prices = new double[100];
for (int i = 0; i < prices.Length; i++)
{
prices[i] = gbm.Next().Close;
}
// First pass
for (int i = 0; i < prices.Length; i++)
{
j.Update(new TValue(DateTime.UtcNow.AddMinutes(i), prices[i]), isNew: true);
}
double midFirst = j.Last.Value;
double upFirst = j.Upper.Value;
double loFirst = j.Lower.Value;
// Reset and second pass
j.Reset();
for (int i = 0; i < prices.Length; i++)
{
j.Update(new TValue(DateTime.UtcNow.AddMinutes(i), prices[i]), isNew: true);
}
Assert.Equal(midFirst, j.Last.Value, 1e-10);
Assert.Equal(upFirst, j.Upper.Value, 1e-10);
Assert.Equal(loFirst, j.Lower.Value, 1e-10);
}
#endregion
#region Dispose Tests
[Fact]
public void Jbands_Dispose_UnsubscribesFromSource()
{
var source = new Sma(3);
var j = new Jbands(source, 14);
// Feed some data through source
for (int i = 0; i < 20; i++)
{
source.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i));
}
double valueBeforeDispose = j.Last.Value;
// Dispose - should unsubscribe
j.Dispose();
// Feed more data - Jbands should NOT update
for (int i = 20; i < 40; i++)
{
source.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 200 + i));
}
Assert.Equal(valueBeforeDispose, j.Last.Value, 1e-10);
}
[Fact]
public void Jbands_Dispose_Idempotent()
{
var source = new Sma(3);
var j = new Jbands(source, 14);
j.Dispose();
j.Dispose(); // Should not throw
// Verify indicator is still in a valid state after double dispose
Assert.True(double.IsFinite(j.Last.Value) || j.Last.Value == 0);
}
[Fact]
public void Jbands_Dispose_WithoutSource_DoesNotThrow()
{
var j = new Jbands(14);
j.Dispose(); // No source subscription, should not throw
Assert.Equal(0, j.Last.Value);
}
#endregion
#region Prime Tests
[Fact]
public void Jbands_Prime_SetsIndicatorToHot()
{
var j = new Jbands(14);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 42);
var series = new TSeries();
int count = j.WarmupPeriod + 10;
for (int i = 0; i < count; i++)
{
var bar = gbm.Next();
series.Add(bar.Time, bar.Close);
}
j.Prime(series);
Assert.True(j.IsHot);
Assert.True(double.IsFinite(j.Last.Value));
Assert.True(double.IsFinite(j.Upper.Value));
Assert.True(double.IsFinite(j.Lower.Value));
}
[Fact]
public void Jbands_Prime_EmptySeries_DoesNotThrow()
{
var j = new Jbands(14);
var empty = new TSeries();
j.Prime(empty); // Should not throw
Assert.False(j.IsHot);
}
[Fact]
public void Jbands_Prime_ThenUpdate_ContinuesCorrectly()
{
var j = new Jbands(14);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 42);
var series = new TSeries();
int primeCount = j.WarmupPeriod + 5;
for (int i = 0; i < primeCount; i++)
{
var bar = gbm.Next();
series.Add(bar.Time, bar.Close);
}
j.Prime(series);
Assert.True(j.IsHot);
// Continue streaming
for (int i = 0; i < 20; i++)
{
var bar = gbm.Next();
j.Update(new TValue(bar.Time, bar.Close), isNew: true);
}
Assert.True(j.IsHot);
Assert.True(double.IsFinite(j.Last.Value));
}
[Fact]
public void Jbands_Prime_MatchesStreamingResults()
{
var jPrime = new Jbands(14, 0, 0.45);
var jStream = new Jbands(14, 0, 0.45);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 42);
var series = new TSeries();
int count = 100;
for (int i = 0; i < count; i++)
{
var bar = gbm.Next();
series.Add(bar.Time, bar.Close);
jStream.Update(new TValue(bar.Time, bar.Close), isNew: true);
}
jPrime.Prime(series);
Assert.Equal(jStream.Last.Value, jPrime.Last.Value, 1e-10);
Assert.Equal(jStream.Upper.Value, jPrime.Upper.Value, 1e-10);
Assert.Equal(jStream.Lower.Value, jPrime.Lower.Value, 1e-10);
}
#endregion
#region Calculate Tests
[Fact]
public void Jbands_Calculate_ReturnsResultsAndHotIndicator()
{
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 42);
var series = new TSeries();
for (int i = 0; i < 300; i++)
{
var bar = gbm.Next();
series.Add(bar.Time, bar.Close);
}
var (results, indicator) = Jbands.Calculate(series, 14, 0, 0.45);
Assert.True(indicator.IsHot);
Assert.Equal(300, results.Middle.Count);
Assert.Equal(300, results.Upper.Count);
Assert.Equal(300, results.Lower.Count);
Assert.True(double.IsFinite(indicator.Last.Value));
}
#endregion
#region Update(TSeries) Tests
[Fact]
public void Jbands_UpdateTSeries_Direct()
{
var j = new Jbands(14);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 42);
var series = new TSeries();
for (int i = 0; i < 300; i++)
{
var bar = gbm.Next();
series.Add(bar.Time, bar.Close);
}
var (middle, upper, lower) = j.Update(series);
Assert.Equal(300, middle.Count);
Assert.Equal(300, upper.Count);
Assert.Equal(300, lower.Count);
Assert.True(j.IsHot);
Assert.True(double.IsFinite(j.Last.Value));
}
[Fact]
public void Jbands_UpdateTSeries_EmptySeries_ReturnsEmptyTuples()
{
var j = new Jbands(14);
var empty = new TSeries();
var (middle, upper, lower) = j.Update(empty);
Assert.Empty(middle);
Assert.Empty(upper);
Assert.Empty(lower);
}
#endregion
#region Batch Tests
[Fact]
public void Jbands_BatchVsStreaming_Match()
{
@@ -236,6 +579,24 @@ public class JbandsTests
Assert.Equal(jStream.Lower.Value, lower[^1], 1e-10);
}
[Fact]
public void Jbands_SpanBatch_EmptySource_DoesNotThrow()
{
double[] source = [];
double[] middle = [];
double[] upper = [];
double[] lower = [];
Jbands.Batch(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 14);
Assert.Empty(source);
Assert.Empty(middle);
}
#endregion
#region Event / Chaining Tests
[Fact]
public void Jbands_Event_Publishes()
{
@@ -265,6 +626,10 @@ public class JbandsTests
Assert.True(double.IsFinite(downstream.Last.Value));
}
#endregion
#region Middle Band / JMA Tests
[Fact]
public void Jbands_MiddleBand_MatchesJma()
{
@@ -284,6 +649,10 @@ public class JbandsTests
Assert.Equal(jma.Last.Value, jbands.Last.Value, 1e-10);
}
#endregion
#region Phase Parameter Tests
[Fact]
public void Jbands_Phase_AffectsBehavior()
{
@@ -307,15 +676,44 @@ public class JbandsTests
}
[Fact]
public void Jbands_UpperAlwaysAboveLower()
public void Jbands_Phase_ClampingBelowMinus100()
{
var j = new Jbands(14);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.2, seed: 77);
// Phase < -100 should clamp phaseParam to 0.5
var jClamped = new Jbands(14, -200);
var jEdge = new Jbands(14, -100);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 42);
for (int i = 0; i < 500; i++)
for (int i = 0; i < 100; i++)
{
j.Update(new TValue(DateTime.UtcNow, gbm.Next().Close), isNew: true);
Assert.True(j.Upper.Value >= j.Lower.Value);
double price = gbm.Next().Close;
var tv = new TValue(DateTime.UtcNow, price);
jClamped.Update(tv, isNew: true);
jEdge.Update(tv, isNew: true);
}
// Phase -200 should clamp to same as -100 (both → 0.5)
Assert.Equal(jEdge.Last.Value, jClamped.Last.Value, 1e-10);
}
[Fact]
public void Jbands_Phase_ClampingAbove100()
{
// Phase > 100 should clamp phaseParam to 2.5
var jClamped = new Jbands(14, 200);
var jEdge = new Jbands(14, 100);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 42);
for (int i = 0; i < 100; i++)
{
double price = gbm.Next().Close;
var tv = new TValue(DateTime.UtcNow, price);
jClamped.Update(tv, isNew: true);
jEdge.Update(tv, isNew: true);
}
// Phase 200 should clamp to same as 100 (both → 2.5)
Assert.Equal(jEdge.Last.Value, jClamped.Last.Value, 1e-10);
}
#endregion
}
+10
View File
@@ -425,6 +425,16 @@ public sealed class Jbands : ITValuePublisher, IDisposable
}
}
/// <summary>
/// Calculates Jbands and returns both the results and the indicator instance.
/// </summary>
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Jbands Indicator) Calculate(TSeries source, int period, int phase = 0, double power = 0.45)
{
var indicator = new Jbands(period, phase, power);
var results = indicator.Update(source);
return (results, indicator);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double CalculateTrimmedMean(double fallback)
{
+285 -63
View File
@@ -4,23 +4,31 @@ namespace QuanTAlib.Tests;
public class StbandsTests
{
#region Constructor Tests
[Fact]
public void Stbands_Constructor_ValidParameters()
{
// Arrange & Act
Stbands stbands = new(period: 10, multiplier: 3.0);
// Assert
Assert.NotNull(stbands);
Assert.Equal("Stbands(10,3.0)", stbands.Name);
Assert.Equal(10, stbands.WarmupPeriod);
Assert.False(stbands.IsHot);
}
[Fact]
public void Stbands_Constructor_DefaultParameters()
{
Stbands stbands = new();
Assert.Equal("Stbands(10,3.0)", stbands.Name);
Assert.Equal(10, stbands.WarmupPeriod);
}
[Fact]
public void Stbands_Constructor_InvalidPeriod_ThrowsArgumentOutOfRangeException()
{
// Arrange, Act & Assert
ArgumentOutOfRangeException exception = Assert.Throws<ArgumentOutOfRangeException>(
() => new Stbands(period: 0));
Assert.Equal("period", exception.ParamName);
@@ -29,24 +37,30 @@ public class StbandsTests
[Fact]
public void Stbands_Constructor_InvalidMultiplier_ThrowsArgumentOutOfRangeException()
{
// Arrange, Act & Assert
ArgumentOutOfRangeException exception = Assert.Throws<ArgumentOutOfRangeException>(
() => new Stbands(period: 10, multiplier: 0.0));
Assert.Equal("multiplier", exception.ParamName);
}
[Fact]
public void Stbands_Constructor_NegativeMultiplier_ThrowsArgumentOutOfRangeException()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new Stbands(period: 10, multiplier: -1.0));
}
#endregion
#region Update TBar Tests
[Fact]
public void Stbands_Update_TBar_ReturnsValue()
{
// Arrange
Stbands stbands = new(period: 3, multiplier: 2.0);
DateTime time = DateTime.UtcNow;
// Act
TBar bar = new(time, 100, 105, 95, 102, 1000);
TValue result = stbands.Update(bar);
// Assert
Assert.True(double.IsFinite(result.Value));
Assert.True(double.IsFinite(stbands.Upper.Value));
Assert.True(double.IsFinite(stbands.Lower.Value));
@@ -55,30 +69,29 @@ public class StbandsTests
[Fact]
public void Stbands_BandCalculations_CorrectValues()
{
// Arrange
Stbands stbands = new(period: 3, multiplier: 2.0);
DateTime time = DateTime.UtcNow;
// Act - Feed some bars
stbands.Update(new TBar(time, 100, 105, 95, 102, 1000), isNew: true);
stbands.Update(new TBar(time.AddMinutes(1), 102, 108, 100, 106, 1000), isNew: true);
stbands.Update(new TBar(time.AddMinutes(2), 106, 110, 104, 108, 1000), isNew: true);
// Assert
Assert.True(stbands.Upper.Value > stbands.Lower.Value);
Assert.True(stbands.Width.Value > 0);
Assert.True(stbands.Trend.Value == 1 || stbands.Trend.Value == -1);
Assert.True(stbands.IsHot);
}
#endregion
#region Band Behavior Tests
[Fact]
public void Stbands_UpperBand_OnlyMovesDown_InDowntrend()
{
// Arrange
Stbands stbands = new(period: 3, multiplier: 1.0);
DateTime time = DateTime.UtcNow;
// Act - Create downtrend scenario
stbands.Update(new TBar(time, 100, 105, 95, 100, 1000), isNew: true);
double initialUpper = stbands.Upper.Value;
@@ -88,18 +101,15 @@ public class StbandsTests
stbands.Update(new TBar(time.AddMinutes(2), 94, 98, 90, 92, 1000), isNew: true);
_ = stbands.Upper.Value;
// Assert - Upper should not increase (only tighten or stay same)
Assert.True(secondUpper <= initialUpper || secondUpper == stbands.Upper.Value);
}
[Fact]
public void Stbands_LowerBand_OnlyMovesUp_InUptrend()
{
// Arrange
Stbands stbands = new(period: 3, multiplier: 1.0);
DateTime time = DateTime.UtcNow;
// Act - Create uptrend scenario
stbands.Update(new TBar(time, 100, 105, 95, 102, 1000), isNew: true);
double initialLower = stbands.Lower.Value;
@@ -109,94 +119,112 @@ public class StbandsTests
stbands.Update(new TBar(time.AddMinutes(2), 110, 115, 108, 114, 1000), isNew: true);
double thirdLower = stbands.Lower.Value;
// Assert - Lower should not decrease (only tighten or stay same)
Assert.True(secondLower >= initialLower || thirdLower >= secondLower);
}
[Fact]
public void Stbands_Last_EqualsTrendAppropiateBand()
{
Stbands stbands = new(period: 3, multiplier: 2.0);
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.15, seed: 42);
for (int i = 0; i < 50; i++)
{
var bar = gbm.Next(isNew: true);
stbands.Update(bar, isNew: true);
double trend = stbands.Trend.Value;
if (trend > 0)
{
Assert.Equal(stbands.Lower.Value, stbands.Last.Value, 1e-10);
}
else
{
Assert.Equal(stbands.Upper.Value, stbands.Last.Value, 1e-10);
}
}
}
#endregion
#region Trend Tests
[Fact]
public void Stbands_TrendDirection_ChangesOnBreakout()
{
// Arrange
Stbands stbands = new(period: 3, multiplier: 1.0);
DateTime time = DateTime.UtcNow;
// Act - Start with some bars
stbands.Update(new TBar(time, 100, 105, 95, 100, 1000), isNew: true);
stbands.Update(new TBar(time.AddMinutes(1), 100, 105, 95, 100, 1000), isNew: true);
stbands.Update(new TBar(time.AddMinutes(2), 100, 105, 95, 100, 1000), isNew: true);
_ = (int)stbands.Trend.Value;
// Create a large breakout above upper band
stbands.Update(new TBar(time.AddMinutes(3), 120, 130, 118, 128, 1000), isNew: true);
// Assert - Trend should potentially change
Assert.True(stbands.Trend.Value == 1 || stbands.Trend.Value == -1);
}
#endregion
#region State Management Tests
[Fact]
public void Stbands_IsNew_False_RollsBackCorrectly()
{
// Arrange
Stbands stbands = new(period: 3, multiplier: 2.0);
DateTime time = DateTime.UtcNow;
// Act
stbands.Update(new TBar(time, 100, 105, 95, 100, 1000), isNew: true);
stbands.Update(new TBar(time.AddMinutes(1), 102, 108, 100, 106, 1000), isNew: true);
stbands.Update(new TBar(time.AddMinutes(2), 106, 112, 104, 110, 1000), isNew: true);
double upperBefore = stbands.Upper.Value;
_ = stbands.Lower.Value;
// Update with different value, isNew = false
stbands.Update(new TBar(time.AddMinutes(2), 90, 95, 85, 88, 1000), isNew: false);
double upperAfter = stbands.Upper.Value;
_ = stbands.Lower.Value;
// Assert - Values should change due to bar correction
Assert.NotEqual(upperBefore, upperAfter);
}
[Fact]
public void Stbands_IsNew_False_IterativeCorrections_Restore()
{
// Arrange
Stbands stbands = new(period: 3, multiplier: 2.0);
DateTime time = DateTime.UtcNow;
// Act - Build up state
stbands.Update(new TBar(time, 100, 105, 95, 100, 1000), isNew: true);
stbands.Update(new TBar(time.AddMinutes(1), 102, 108, 100, 106, 1000), isNew: true);
stbands.Update(new TBar(time.AddMinutes(2), 106, 112, 104, 110, 1000), isNew: true);
double originalUpper = stbands.Upper.Value;
double originalLower = stbands.Lower.Value;
// Make multiple corrections
stbands.Update(new TBar(time.AddMinutes(2), 90, 95, 85, 88, 1000), isNew: false);
stbands.Update(new TBar(time.AddMinutes(2), 80, 85, 75, 78, 1000), isNew: false);
// Restore original bar
stbands.Update(new TBar(time.AddMinutes(2), 106, 112, 104, 110, 1000), isNew: false);
double restoredUpper = stbands.Upper.Value;
double restoredLower = stbands.Lower.Value;
// Assert - Should restore to original values
Assert.Equal(originalUpper, restoredUpper, precision: 10);
Assert.Equal(originalLower, restoredLower, precision: 10);
}
#endregion
#region NaN / Infinity Handling Tests
[Fact]
public void Stbands_NaN_HandledGracefully()
{
// Arrange
Stbands stbands = new(period: 3, multiplier: 2.0);
DateTime time = DateTime.UtcNow;
// Act
stbands.Update(new TBar(time, 100, 105, 95, 100, 1000), isNew: true);
stbands.Update(new TBar(time.AddMinutes(1), 102, 108, 100, 106, 1000), isNew: true);
stbands.Update(new TBar(time.AddMinutes(2), double.NaN, double.NaN, double.NaN, double.NaN, 0), isNew: true);
// Assert - Should substitute last valid values
Assert.True(double.IsFinite(stbands.Upper.Value));
Assert.True(double.IsFinite(stbands.Lower.Value));
}
@@ -204,24 +232,24 @@ public class StbandsTests
[Fact]
public void Stbands_Infinity_HandledGracefully()
{
// Arrange
Stbands stbands = new(period: 3, multiplier: 2.0);
DateTime time = DateTime.UtcNow;
// Act
stbands.Update(new TBar(time, 100, 105, 95, 100, 1000), isNew: true);
stbands.Update(new TBar(time.AddMinutes(1), 102, 108, 100, 106, 1000), isNew: true);
stbands.Update(new TBar(time.AddMinutes(2), double.PositiveInfinity, double.PositiveInfinity, double.NegativeInfinity, double.PositiveInfinity, 0), isNew: true);
// Assert - Should substitute last valid values
Assert.True(double.IsFinite(stbands.Upper.Value));
Assert.True(double.IsFinite(stbands.Lower.Value));
}
#endregion
#region Reset Tests
[Fact]
public void Stbands_Reset_ClearsState()
{
// Arrange
Stbands stbands = new(period: 3, multiplier: 2.0);
DateTime time = DateTime.UtcNow;
@@ -229,21 +257,56 @@ public class StbandsTests
stbands.Update(new TBar(time.AddMinutes(1), 102, 108, 100, 106, 1000), isNew: true);
stbands.Update(new TBar(time.AddMinutes(2), 106, 112, 104, 110, 1000), isNew: true);
// Act
stbands.Reset();
// Assert
Assert.False(stbands.IsHot);
}
[Fact]
public void Stbands_Reset_ThenReuse_ProducesSameResults()
{
Stbands stbands = new(period: 3, multiplier: 2.0);
DateTime time = DateTime.UtcNow;
var bars = new TBar[]
{
new(time, 100, 105, 95, 100, 1000),
new(time.AddMinutes(1), 102, 108, 100, 106, 1000),
new(time.AddMinutes(2), 106, 112, 104, 110, 1000),
new(time.AddMinutes(3), 108, 115, 105, 112, 1000),
new(time.AddMinutes(4), 112, 118, 110, 116, 1000),
};
// First pass
foreach (var bar in bars)
{
stbands.Update(bar, isNew: true);
}
double upperFirst = stbands.Upper.Value;
double lowerFirst = stbands.Lower.Value;
double trendFirst = stbands.Trend.Value;
// Reset and second pass
stbands.Reset();
foreach (var bar in bars)
{
stbands.Update(bar, isNew: true);
}
Assert.Equal(upperFirst, stbands.Upper.Value, 1e-10);
Assert.Equal(lowerFirst, stbands.Lower.Value, 1e-10);
Assert.Equal(trendFirst, stbands.Trend.Value, 1e-10);
}
#endregion
#region WarmupPeriod / IsHot Tests
[Fact]
public void Stbands_WarmupPeriod_IsHotTransition()
{
// Arrange
Stbands stbands = new(period: 5, multiplier: 2.0);
DateTime time = DateTime.UtcNow;
// Act & Assert
for (int i = 0; i < 4; i++)
{
stbands.Update(new TBar(time.AddMinutes(i), 100 + i, 105 + i, 95 + i, 102 + i, 1000), isNew: true);
@@ -254,41 +317,153 @@ public class StbandsTests
Assert.True(stbands.IsHot);
}
#endregion
#region Prime Tests
[Fact]
public void Stbands_Prime_SetsIndicatorToHot()
{
Stbands stbands = new(period: 5, multiplier: 2.0);
double[] prices = [100, 102, 104, 98, 96, 99, 103, 107, 105, 110];
stbands.Prime(prices.AsSpan());
Assert.True(stbands.IsHot);
Assert.True(double.IsFinite(stbands.Last.Value));
Assert.True(double.IsFinite(stbands.Upper.Value));
Assert.True(double.IsFinite(stbands.Lower.Value));
}
[Fact]
public void Stbands_Prime_EmptySpan_DoesNotThrow()
{
Stbands stbands = new(period: 5, multiplier: 2.0);
stbands.Prime(ReadOnlySpan<double>.Empty);
Assert.False(stbands.IsHot);
}
[Fact]
public void Stbands_Prime_WithCustomStep()
{
Stbands stbands = new(period: 3, multiplier: 2.0);
double[] prices = [100, 102, 104, 106, 108];
stbands.Prime(prices.AsSpan(), TimeSpan.FromMinutes(5));
Assert.True(stbands.IsHot);
Assert.True(double.IsFinite(stbands.Last.Value));
}
[Fact]
public void Stbands_Prime_ThenUpdate_ContinuesCorrectly()
{
Stbands stbands = new(period: 3, multiplier: 2.0);
double[] primeData = [100, 102, 104, 106, 108];
stbands.Prime(primeData.AsSpan());
Assert.True(stbands.IsHot);
// Continue streaming with TBar
stbands.Update(new TBar(DateTime.UtcNow, 108, 112, 106, 110, 1000), isNew: true);
Assert.True(stbands.IsHot);
Assert.True(double.IsFinite(stbands.Last.Value));
}
#endregion
#region Calculate Tests
[Fact]
public void Stbands_Calculate_ReturnsResultsAndHotIndicator()
{
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.1, seed: 42);
TBarSeries bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var (results, indicator) = Stbands.Calculate(bars, period: 5, multiplier: 2.0);
Assert.True(indicator.IsHot);
Assert.Equal(bars.Count, results.Count);
Assert.True(double.IsFinite(indicator.Last.Value));
Assert.True(double.IsFinite(indicator.Upper.Value));
Assert.True(double.IsFinite(indicator.Lower.Value));
}
#endregion
#region Update TSeries Tests
[Fact]
public void Stbands_UpdateTSeries_ReturnsValidSeries()
{
Stbands stbands = new(period: 5, multiplier: 2.0);
var series = new TSeries();
for (int i = 0; i < 20; i++)
{
series.Add(DateTime.UtcNow.AddMinutes(i), 100 + i * 0.5);
}
TSeries result = stbands.Update(series);
Assert.Equal(20, result.Count);
Assert.True(stbands.IsHot);
}
[Fact]
public void Stbands_UpdateTSeries_NullSource_ThrowsArgumentNullException()
{
Stbands stbands = new(period: 5, multiplier: 2.0);
Assert.Throws<ArgumentNullException>(() => stbands.Update((TSeries)null!));
}
[Fact]
public void Stbands_UpdateTBarSeries_NullSource_ThrowsArgumentNullException()
{
Stbands stbands = new(period: 5, multiplier: 2.0);
Assert.Throws<ArgumentNullException>(() => stbands.Update((TBarSeries)null!));
}
#endregion
#region Update TBarSeries Tests
[Fact]
public void Stbands_UpdateTBarSeries_ReturnsValidSeries()
{
// Arrange
int period = 5;
Stbands stbands = new(period, multiplier: 2.0);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
TBarSeries bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Act
TSeries result = stbands.Update(bars);
// Assert
Assert.Equal(bars.Count, result.Count);
Assert.True(stbands.IsHot);
}
#endregion
#region Batch Tests
[Fact]
public void Stbands_StaticCalculate_ReturnsValidSeries()
{
// Arrange
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
TBarSeries bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Act
TSeries result = Stbands.Batch(bars, period: 5, multiplier: 2.0);
// Assert
Assert.Equal(bars.Count, result.Count);
}
[Fact]
public void Stbands_SpanCalculate_ProducesValidOutput()
{
// Arrange
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
TBarSeries bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
int period = 10;
@@ -301,10 +476,8 @@ public class StbandsTests
double[] lower = new double[bars.Count];
double[] trend = new double[bars.Count];
// Act
Stbands.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(), upper.AsSpan(), lower.AsSpan(), trend.AsSpan(), period, multiplier);
// Assert
for (int i = 0; i < bars.Count; i++)
{
Assert.True(double.IsFinite(upper[i]));
@@ -317,7 +490,6 @@ public class StbandsTests
[Fact]
public void Stbands_SpanCalculate_InvalidLength_ThrowsArgumentException()
{
// Arrange
double[] high = new double[10];
double[] low = new double[10];
double[] close = new double[10];
@@ -325,16 +497,49 @@ public class StbandsTests
double[] lower = new double[10];
double[] trend = new double[9]; // Wrong length
// Act & Assert
ArgumentException exception = Assert.Throws<ArgumentException>(
() => Stbands.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(), upper.AsSpan(), lower.AsSpan(), trend.AsSpan()));
Assert.Equal("high", exception.ParamName);
}
[Fact]
public void Stbands_SpanBatch_InvalidPeriod_ThrowsArgumentOutOfRangeException()
{
double[] data = new double[10];
Assert.Throws<ArgumentOutOfRangeException>(() =>
Stbands.Batch(data.AsSpan(), data.AsSpan(), data.AsSpan(),
data.AsSpan(), data.AsSpan(), data.AsSpan(), period: 0));
}
[Fact]
public void Stbands_SpanBatch_InvalidMultiplier_ThrowsArgumentOutOfRangeException()
{
double[] data = new double[10];
Assert.Throws<ArgumentOutOfRangeException>(() =>
Stbands.Batch(data.AsSpan(), data.AsSpan(), data.AsSpan(),
data.AsSpan(), data.AsSpan(), data.AsSpan(), period: 10, multiplier: 0.0));
}
[Fact]
public void Stbands_SpanBatch_EmptyArrays_DoesNotThrow()
{
double[] empty = [];
Stbands.Batch(empty.AsSpan(), empty.AsSpan(), empty.AsSpan(),
empty.AsSpan(), empty.AsSpan(), empty.AsSpan(), period: 10);
Assert.Empty(empty);
}
#endregion
#region Consistency Tests
[Fact]
public void Stbands_Consistency_StreamingVsBatch()
{
// Arrange
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
TBarSeries bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
int period = 10;
@@ -350,14 +555,12 @@ public class StbandsTests
// Batch
TSeries batchResult = Stbands.Batch(bars, period, multiplier);
// Assert - Last values should match
Assert.Equal(batchResult[^1].Value, streamingStbands.Last.Value, precision: 8);
}
[Fact]
public void Stbands_Consistency_StreamingVsSpan()
{
// Arrange
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
TBarSeries bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
int period = 10;
@@ -379,45 +582,64 @@ public class StbandsTests
double[] trend = new double[bars.Count];
Stbands.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(), upper.AsSpan(), lower.AsSpan(), trend.AsSpan(), period, multiplier);
// Assert - Last values should match
Assert.Equal(upper[^1], streamingStbands.Upper.Value, precision: 8);
Assert.Equal(lower[^1], streamingStbands.Lower.Value, precision: 8);
Assert.Equal(trend[^1], streamingStbands.Trend.Value, precision: 8);
}
#endregion
#region TValue Update Tests
[Fact]
public void Stbands_TValue_Update_WorksWithSingleValue()
{
// Arrange
Stbands stbands = new(period: 3, multiplier: 2.0);
DateTime time = DateTime.UtcNow;
// Act - Using TValue (treated as H=L=C=value)
stbands.Update(new TValue(time, 100.0), isNew: true);
stbands.Update(new TValue(time.AddMinutes(1), 102.0), isNew: true);
stbands.Update(new TValue(time.AddMinutes(2), 104.0), isNew: true);
// Assert
Assert.True(stbands.IsHot);
Assert.True(double.IsFinite(stbands.Upper.Value));
Assert.True(double.IsFinite(stbands.Lower.Value));
// With H=L=C, bands should be based on ATR=0 initially, but will have width from multiplier*0
// Actually TR will be 0 when H-L=0, so bands may be tight
}
#endregion
#region Width Tests
[Fact]
public void Stbands_Width_IsUpperMinusLower()
{
// Arrange
Stbands stbands = new(period: 3, multiplier: 2.0);
DateTime time = DateTime.UtcNow;
// Act
stbands.Update(new TBar(time, 100, 110, 90, 102, 1000), isNew: true);
stbands.Update(new TBar(time.AddMinutes(1), 102, 115, 95, 108, 1000), isNew: true);
stbands.Update(new TBar(time.AddMinutes(2), 108, 120, 100, 115, 1000), isNew: true);
// Assert
Assert.Equal(stbands.Upper.Value - stbands.Lower.Value, stbands.Width.Value, precision: 10);
}
#endregion
#region Pub Event Tests
[Fact]
public void Stbands_Pub_DoesNotFireDirectly()
{
// Stbands overrides Update paths without calling PubEvent —
// Pub event is inherited from AbstractBase but not invoked.
Stbands stbands = new(period: 3, multiplier: 2.0);
bool fired = false;
stbands.Pub += (object? sender, in TValueEventArgs args) => fired = true;
stbands.Update(new TValue(DateTime.UtcNow, 100.0), isNew: true);
Assert.False(fired);
}
#endregion
}
+385 -15
View File
@@ -1,9 +1,12 @@
using TradingPlatform.BusinessLayer;
using Xunit;
namespace QuanTAlib.Tests;
public class UchannelQuantowerTests
{
#region Constructor Tests
[Fact]
public void UchannelIndicator_Constructor_SetsDefaults()
{
@@ -16,6 +19,19 @@ public class UchannelQuantowerTests
Assert.Equal("UCHANNEL - Ehlers Ultimate Channel", indicator.Name);
}
[Fact]
public void UchannelIndicator_Constructor_SetsDisplayProperties()
{
var indicator = new UchannelIndicator();
Assert.False(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
#endregion
#region MinHistoryDepths Tests
[Fact]
public void UchannelIndicator_MinHistoryDepths_ReturnsMaxOfPeriods()
{
@@ -29,6 +45,29 @@ public class UchannelQuantowerTests
Assert.Equal(25, indicator3.MinHistoryDepths);
}
[Fact]
public void UchannelIndicator_MinHistoryDepths_ExplicitInterface()
{
var indicator = new UchannelIndicator { StrPeriod = 15, CenterPeriod = 30 };
int explicit_value = ((IWatchlistIndicator)indicator).MinHistoryDepths;
Assert.Equal(30, explicit_value);
Assert.Equal(indicator.MinHistoryDepths, explicit_value);
}
[Fact]
public void UchannelIndicator_MinHistoryDepths_MinPeriods()
{
var indicator = new UchannelIndicator { StrPeriod = 1, CenterPeriod = 1 };
Assert.Equal(1, indicator.MinHistoryDepths);
}
#endregion
#region ShortName Tests
[Fact]
public void UchannelIndicator_ShortName_FormatsCorrectly()
{
@@ -42,6 +81,31 @@ public class UchannelQuantowerTests
Assert.Equal("UCHANNEL (15,25,2.5)", indicator.ShortName);
}
[Fact]
public void UchannelIndicator_ShortName_DefaultParameters()
{
var indicator = new UchannelIndicator();
Assert.Equal("UCHANNEL (20,20,1.0)", indicator.ShortName);
}
[Fact]
public void UchannelIndicator_ShortName_UpdatesWithParameters()
{
var indicator = new UchannelIndicator();
Assert.Equal("UCHANNEL (20,20,1.0)", indicator.ShortName);
indicator.StrPeriod = 10;
indicator.CenterPeriod = 30;
indicator.Multiplier = 3.0;
Assert.Equal("UCHANNEL (10,30,3.0)", indicator.ShortName);
}
#endregion
#region SourceCodeLink Tests
[Fact]
public void UchannelIndicator_SourceCodeLink_IsValid()
{
@@ -51,20 +115,9 @@ public class UchannelQuantowerTests
Assert.Contains("Uchannel.cs", indicator.SourceCodeLink, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void UchannelIndicator_OnInit_CreatesInternalIndicator()
{
var indicator = new UchannelIndicator
{
StrPeriod = 10,
CenterPeriod = 15,
Multiplier = 1.5
};
#endregion
// OnInit is protected, but we can verify it doesn't throw
// by checking the indicator state after construction
Assert.NotNull(indicator);
}
#region Parameter Tests
[Fact]
public void UchannelIndicator_Parameters_CanBeModified()
@@ -82,6 +135,10 @@ public class UchannelQuantowerTests
Assert.False(indicator.ShowColdValues);
}
#endregion
#region Description Tests
[Fact]
public void UchannelIndicator_Description_IsNotEmpty()
{
@@ -91,12 +148,325 @@ public class UchannelQuantowerTests
Assert.Contains("Ultrasmooth", indicator.Description, StringComparison.OrdinalIgnoreCase);
}
#endregion
#region LineSeries Tests
[Fact]
public void UchannelIndicator_HasCorrectLineSeries()
public void UchannelIndicator_HasFiveLineSeries()
{
var indicator = new UchannelIndicator();
// The indicator should have 5 line series: Middle, Upper, Lower, STR, Width
// The constructor adds 5 line series: Middle, Upper, Lower, STR, Width
Assert.Equal(5, indicator.LinesSeries.Count);
}
[Fact]
public void UchannelIndicator_LineSeries_HaveCorrectNames()
{
var indicator = new UchannelIndicator();
Assert.Equal("Middle", indicator.LinesSeries[0].Name);
Assert.Equal("Upper", indicator.LinesSeries[1].Name);
Assert.Equal("Lower", indicator.LinesSeries[2].Name);
Assert.Equal("STR", indicator.LinesSeries[3].Name);
Assert.Equal("Width", indicator.LinesSeries[4].Name);
}
#endregion
#region Initialize Tests
[Fact]
public void UchannelIndicator_Initialize_DoesNotThrow()
{
var indicator = new UchannelIndicator
{
StrPeriod = 10,
CenterPeriod = 15,
Multiplier = 1.5
};
indicator.Initialize();
Assert.NotNull(indicator);
}
[Fact]
public void UchannelIndicator_Initialize_PreservesLineSeries()
{
var indicator = new UchannelIndicator();
indicator.Initialize();
// Line series should still be present after init
Assert.Equal(5, indicator.LinesSeries.Count);
}
#endregion
#region ProcessUpdate Tests
[Fact]
public void UchannelIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new UchannelIndicator { StrPeriod = 5, CenterPeriod = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
// All 5 line series should have values
for (int i = 0; i < 5; i++)
{
Assert.Equal(1, indicator.LinesSeries[i].Count);
Assert.True(double.IsFinite(indicator.LinesSeries[i].GetValue(0)));
}
}
[Fact]
public void UchannelIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new UchannelIndicator { StrPeriod = 5, CenterPeriod = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106, 1500);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
Assert.Equal(2, indicator.LinesSeries[0].Count);
}
[Fact]
public void UchannelIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
{
var indicator = new UchannelIndicator { StrPeriod = 5, CenterPeriod = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
double firstValue = indicator.LinesSeries[0].GetValue(0);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
double secondValue = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(firstValue));
Assert.True(double.IsFinite(secondValue));
}
#endregion
#region Multiple Updates Tests
[Fact]
public void UchannelIndicator_MultipleUpdates_ProducesCorrectSequence()
{
var indicator = new UchannelIndicator { StrPeriod = 5, CenterPeriod = 5, Multiplier = 1.5 };
indicator.Initialize();
var now = DateTime.UtcNow;
double[] closes = { 100, 102, 104, 103, 105, 107, 106, 108, 110, 109 };
for (int i = 0; i < closes.Length; i++)
{
double close = closes[i];
indicator.HistoricalData.AddBar(now.AddMinutes(i), close, close + 3, close - 3, close, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
// All 5 series should have values for each bar
for (int s = 0; s < 5; s++)
{
Assert.Equal(closes.Length, indicator.LinesSeries[s].Count);
}
// All last values should be finite
for (int s = 0; s < 5; s++)
{
Assert.True(double.IsFinite(indicator.LinesSeries[s].GetValue(0)));
}
}
#endregion
#region Band Relationship Tests
[Fact]
public void UchannelIndicator_BandRelationships_AreCorrect()
{
var indicator = new UchannelIndicator { StrPeriod = 5, CenterPeriod = 5, Multiplier = 2.0 };
indicator.Initialize();
var now = DateTime.UtcNow;
// Add varied data to generate band width
double[] closes = { 100, 105, 95, 110, 90, 105, 100, 108, 92, 103 };
for (int i = 0; i < closes.Length; i++)
{
double close = closes[i];
indicator.HistoricalData.AddBar(now.AddMinutes(i), close, close + 5, close - 5, close, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
// Get last values: Middle=0, Upper=1, Lower=2, STR=3, Width=4
double middle = indicator.LinesSeries[0].GetValue(0);
double upper = indicator.LinesSeries[1].GetValue(0);
double lower = indicator.LinesSeries[2].GetValue(0);
double width = indicator.LinesSeries[4].GetValue(0);
// Band relationships: Upper >= Middle >= Lower
Assert.True(upper >= middle, $"Upper ({upper}) should be >= Middle ({middle})");
Assert.True(middle >= lower, $"Middle ({middle}) should be >= Lower ({lower})");
// Width = Upper - Lower
Assert.Equal(upper - lower, width, 6);
}
#endregion
#region Multiplier Tests
[Fact]
public void UchannelIndicator_Multiplier_AffectsBandWidth()
{
var indicator1 = new UchannelIndicator { StrPeriod = 5, CenterPeriod = 5, Multiplier = 1.0 };
var indicator2 = new UchannelIndicator { StrPeriod = 5, CenterPeriod = 5, Multiplier = 2.0 };
indicator1.Initialize();
indicator2.Initialize();
var now = DateTime.UtcNow;
double[] closes = { 100, 105, 95, 110, 90, 105, 100, 108, 92, 103 };
for (int i = 0; i < closes.Length; i++)
{
double close = closes[i];
indicator1.HistoricalData.AddBar(now.AddMinutes(i), close, close + 5, close - 5, close, 1000);
indicator1.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
indicator2.HistoricalData.AddBar(now.AddMinutes(i), close, close + 5, close - 5, close, 1000);
indicator2.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
double width1 = indicator1.LinesSeries[4].GetValue(0);
double width2 = indicator2.LinesSeries[4].GetValue(0);
// Width2 should be approximately 2x Width1
Assert.True(Math.Abs(width2 - 2 * width1) < 0.0001,
$"Width2 ({width2}) should be ~2x Width1 ({width1})");
}
#endregion
#region Different Period Tests
[Fact]
public void UchannelIndicator_DifferentPeriods_ProduceDifferentResults()
{
var indicator1 = new UchannelIndicator { StrPeriod = 5, CenterPeriod = 5 };
var indicator2 = new UchannelIndicator { StrPeriod = 20, CenterPeriod = 20 };
indicator1.Initialize();
indicator2.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 30; i++)
{
double close = 100 + (i % 5) * 2;
indicator1.HistoricalData.AddBar(now.AddMinutes(i), close, close + 3, close - 3, close, 1000);
indicator1.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
indicator2.HistoricalData.AddBar(now.AddMinutes(i), close, close + 3, close - 3, close, 1000);
indicator2.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
double middle1 = indicator1.LinesSeries[0].GetValue(0);
double middle2 = indicator2.LinesSeries[0].GetValue(0);
// Different smoothing periods should produce different middle values
Assert.NotEqual(middle1, middle2);
}
#endregion
#region STR Series Tests
[Fact]
public void UchannelIndicator_STR_IsNonNegative()
{
var indicator = new UchannelIndicator { StrPeriod = 5, CenterPeriod = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
double[] closes = { 100, 105, 95, 110, 90, 105, 100, 108, 92, 103 };
for (int i = 0; i < closes.Length; i++)
{
double close = closes[i];
indicator.HistoricalData.AddBar(now.AddMinutes(i), close, close + 5, close - 5, close, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
// STR (smoothed true range) should be non-negative
double str = indicator.LinesSeries[3].GetValue(0);
Assert.True(str >= 0, $"STR ({str}) should be >= 0");
}
#endregion
#region ShowColdValues Tests
[Fact]
public void UchannelIndicator_ShowColdValues_True_ShowsValues()
{
var indicator = new UchannelIndicator
{
StrPeriod = 50,
CenterPeriod = 50,
ShowColdValues = true
};
indicator.Initialize();
var now = DateTime.UtcNow;
// Add fewer bars than warmup
for (int i = 0; i < 5; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 105, 95, 102, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
// With ShowColdValues = true, values should be shown even before warmup
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
}
[Fact]
public void UchannelIndicator_ShowColdValues_False_SetsNaN()
{
var indicator = new UchannelIndicator
{
StrPeriod = 50,
CenterPeriod = 50,
ShowColdValues = false
};
indicator.Initialize();
var now = DateTime.UtcNow;
// Add fewer bars than warmup
for (int i = 0; i < 5; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 105, 95, 102, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
// With ShowColdValues = false, cold values should be NaN before warmup
Assert.True(double.IsNaN(indicator.LinesSeries[0].GetValue(0)));
}
#endregion
}
+226 -27
View File
@@ -4,6 +4,8 @@ namespace QuanTAlib.Tests;
public class VwapsdIndicatorTests
{
// ── Constructor & Defaults ──────────────────────────────────────────
[Fact]
public void VwapsdIndicator_Constructor_SetsDefaults()
{
@@ -16,6 +18,37 @@ public class VwapsdIndicatorTests
Assert.True(indicator.OnBackGround);
}
[Fact]
public void VwapsdIndicator_Constructor_Description_IsNotEmpty()
{
var indicator = new VwapsdIndicator();
Assert.False(string.IsNullOrWhiteSpace(indicator.Description));
Assert.Contains("volume", indicator.Description, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void VwapsdIndicator_Constructor_CreatesFourLineSeries()
{
var indicator = new VwapsdIndicator();
Assert.Equal(4, indicator.LinesSeries.Count);
}
[Fact]
public void VwapsdIndicator_Constructor_LineSeriesNames_BeforeInit()
{
var indicator = new VwapsdIndicator();
// Before OnInit, series have their constructor names
Assert.Equal("VWAP", indicator.LinesSeries[0].Name);
Assert.Equal("Upper", indicator.LinesSeries[1].Name);
Assert.Equal("Lower", indicator.LinesSeries[2].Name);
Assert.Equal("Width", indicator.LinesSeries[3].Name);
}
// ── MinHistoryDepths ────────────────────────────────────────────────
[Fact]
public void VwapsdIndicator_MinHistoryDepths_EqualsTwo()
{
@@ -25,6 +58,16 @@ public class VwapsdIndicatorTests
Assert.Equal(2, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
// ── ShortName ───────────────────────────────────────────────────────
[Fact]
public void VwapsdIndicator_ShortName_DefaultFormat()
{
var indicator = new VwapsdIndicator();
Assert.Equal("VWAPSD (2.0)", indicator.ShortName);
}
[Fact]
public void VwapsdIndicator_ShortName_IncludesNumDevs()
{
@@ -34,37 +77,92 @@ public class VwapsdIndicatorTests
Assert.Contains("2.5", indicator.ShortName, StringComparison.Ordinal);
}
// ── SourceCodeLink ──────────────────────────────────────────────────
[Fact]
public void VwapsdIndicator_Initialize_CreatesFourLineSeries()
public void VwapsdIndicator_SourceCodeLink_PointsToGitHub()
{
var indicator = new VwapsdIndicator();
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.OrdinalIgnoreCase);
Assert.Contains("Vwapsd.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
}
// ── OnInit σ Rename ─────────────────────────────────────────────────
[Fact]
public void VwapsdIndicator_Initialize_RenamesSeriesWithSigmaNotation()
{
var indicator = new VwapsdIndicator { NumDevs = 2.0 };
indicator.Initialize();
// After OnInit, Upper/Lower should have σ notation
Assert.Equal("Upper (+2.0σ)", indicator.LinesSeries[1].Name);
Assert.Equal("Lower (-2.0σ)", indicator.LinesSeries[2].Name);
}
[Fact]
public void VwapsdIndicator_Initialize_SigmaNotation_ReflectsNumDevs()
{
var indicator = new VwapsdIndicator { NumDevs = 1.5 };
indicator.Initialize();
Assert.Equal("Upper (+1.5σ)", indicator.LinesSeries[1].Name);
Assert.Equal("Lower (-1.5σ)", indicator.LinesSeries[2].Name);
}
[Fact]
public void VwapsdIndicator_Initialize_PreservesSeriesCount()
{
var indicator = new VwapsdIndicator { NumDevs = 2.0 };
// Initialize should not throw
indicator.Initialize();
// After init, line series should exist (VWAP, Upper, Lower, Width)
indicator.Initialize();
Assert.Equal(4, indicator.LinesSeries.Count);
}
// ── Parameters ──────────────────────────────────────────────────────
[Fact]
public void VwapsdIndicator_Parameters_CanBeChanged()
{
var indicator = new VwapsdIndicator { NumDevs = 1.5 };
Assert.Equal(1.5, indicator.NumDevs);
indicator.NumDevs = 2.5;
Assert.Equal(2.5, indicator.NumDevs);
}
[Fact]
public void VwapsdIndicator_ShowColdValues_CanBeChanged()
{
var indicator = new VwapsdIndicator();
Assert.True(indicator.ShowColdValues);
indicator.ShowColdValues = false;
Assert.False(indicator.ShowColdValues);
}
// ── ProcessUpdate: HistoricalBar ────────────────────────────────────
[Fact]
public void VwapsdIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new VwapsdIndicator { NumDevs = 2.0 };
indicator.Initialize();
// Add historical data with volume
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
// Process update
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
// Line series should have values
Assert.Equal(1, indicator.LinesSeries[0].Count);
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
}
// ── ProcessUpdate: NewBar ───────────────────────────────────────────
[Fact]
public void VwapsdIndicator_ProcessUpdate_NewBar_ComputesValue()
{
@@ -81,6 +179,8 @@ public class VwapsdIndicatorTests
Assert.Equal(2, indicator.LinesSeries[0].Count);
}
// ── ProcessUpdate: NewTick ──────────────────────────────────────────
[Fact]
public void VwapsdIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
{
@@ -100,6 +200,8 @@ public class VwapsdIndicatorTests
Assert.True(double.IsFinite(secondValue));
}
// ── MultipleUpdates ─────────────────────────────────────────────────
[Fact]
public void VwapsdIndicator_MultipleUpdates_ProducesCorrectSequence()
{
@@ -129,15 +231,7 @@ public class VwapsdIndicatorTests
Assert.True(lastVwap >= 95 && lastVwap <= 110);
}
[Fact]
public void VwapsdIndicator_Parameters_CanBeChanged()
{
var indicator = new VwapsdIndicator { NumDevs = 1.5 };
Assert.Equal(1.5, indicator.NumDevs);
indicator.NumDevs = 2.5;
Assert.Equal(2.5, indicator.NumDevs);
}
// ── AllBandsUpdate ──────────────────────────────────────────────────
[Fact]
public void VwapsdIndicator_AllBandsUpdate_Correctly()
@@ -161,6 +255,8 @@ public class VwapsdIndicatorTests
}
}
// ── BandRelationships ───────────────────────────────────────────────
[Fact]
public void VwapsdIndicator_BandRelationships_AreCorrect()
{
@@ -168,7 +264,6 @@ public class VwapsdIndicatorTests
indicator.Initialize();
var now = DateTime.UtcNow;
// Add varied data to generate band width
double[] closes = { 100, 105, 95, 110, 90, 105, 100, 108, 92, 103 };
double[] volumes = { 1000, 1500, 2000, 1200, 1800, 1100, 1600, 1300, 1900, 1400 };
@@ -180,21 +275,19 @@ public class VwapsdIndicatorTests
now = now.AddMinutes(1);
}
// Get last values: VWAP=0, Upper=1, Lower=2, Width=3
double vwap = indicator.LinesSeries[0].GetValue(0);
double upper = indicator.LinesSeries[1].GetValue(0);
double lower = indicator.LinesSeries[2].GetValue(0);
double width = indicator.LinesSeries[3].GetValue(0);
// Band relationships: Upper > VWAP > Lower
Assert.True(upper >= vwap, $"Upper ({upper}) should be >= VWAP ({vwap})");
Assert.True(vwap >= lower, $"VWAP ({vwap}) should be >= Lower ({lower})");
// Width = Upper - Lower (2 × numDevs × StdDev)
Assert.True(Math.Abs(width - (upper - lower)) < 0.0001,
$"Width ({width}) should equal Upper - Lower ({upper - lower})");
}
// ── VolumeWeighting ─────────────────────────────────────────────────
[Fact]
public void VwapsdIndicator_VolumeWeighting_AffectsVwap()
{
@@ -205,9 +298,6 @@ public class VwapsdIndicatorTests
var now = DateTime.UtcNow;
// Same prices but different volume distributions
// Process both bars for each indicator
// Indicator1: high volume on low price, low volume on high price
indicator1.HistoricalData.AddBar(now, 100, 102, 98, 100, 10000);
indicator1.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
@@ -223,11 +313,11 @@ public class VwapsdIndicatorTests
double vwap1 = indicator1.LinesSeries[0].GetValue(0);
double vwap2 = indicator2.LinesSeries[0].GetValue(0);
// VWAP1 should be lower (weighted toward 100 due to high volume at low price)
// VWAP2 should be higher (weighted toward 110 due to high volume at high price)
Assert.True(vwap1 < vwap2, $"VWAP1 ({vwap1}) should be less than VWAP2 ({vwap2}) due to volume weighting");
}
// ── NumDevs Effect ──────────────────────────────────────────────────
[Fact]
public void VwapsdIndicator_NumDevs_AffectsBandWidth()
{
@@ -250,7 +340,6 @@ public class VwapsdIndicatorTests
now = now.AddMinutes(1);
}
// Width should be proportional to numDevs
double width1 = indicator1.LinesSeries[3].GetValue(0);
double width2 = indicator2.LinesSeries[3].GetValue(0);
@@ -258,4 +347,114 @@ public class VwapsdIndicatorTests
Assert.True(Math.Abs(width2 - 2 * width1) < 0.0001,
$"Width2 ({width2}) should be ~2x Width1 ({width1})");
}
// ── Width Non-Negative ──────────────────────────────────────────────
[Fact]
public void VwapsdIndicator_Width_IsNonNegative()
{
var indicator = new VwapsdIndicator { NumDevs = 2.0 };
indicator.Initialize();
var now = DateTime.UtcNow;
double[] closes = { 100, 102, 98, 105, 97, 103, 101, 99 };
double[] volumes = { 1000, 1200, 800, 1500, 900, 1100, 1300, 700 };
for (int i = 0; i < closes.Length; i++)
{
double close = closes[i];
indicator.HistoricalData.AddBar(now, close, close + 2, close - 2, close, volumes[i]);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
now = now.AddMinutes(1);
}
// Width should be non-negative at every bar
for (int i = 0; i < closes.Length; i++)
{
double w = indicator.LinesSeries[3].GetValue(closes.Length - 1 - i);
Assert.True(w >= 0.0, $"Width at bar {i} ({w}) should be >= 0");
}
}
// ── SingleBar Zero Width ────────────────────────────────────────────
[Fact]
public void VwapsdIndicator_SingleBar_ProducesZeroWidth()
{
var indicator = new VwapsdIndicator { NumDevs = 2.0 };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
// With only one bar, stddev is 0 → width should be 0
double width = indicator.LinesSeries[3].GetValue(0);
Assert.Equal(0.0, width, 4);
}
// ── ShowColdValues False ────────────────────────────────────────────
[Fact]
public void VwapsdIndicator_ShowColdValues_False_SuppressesColdValues()
{
var indicator = new VwapsdIndicator { NumDevs = 2.0, ShowColdValues = false };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
// With ShowColdValues=false, cold bars produce NaN
double vwap = indicator.LinesSeries[0].GetValue(0);
// Value is either NaN (suppressed) or finite (hot)
Assert.True(double.IsNaN(vwap) || double.IsFinite(vwap));
}
[Fact]
public void VwapsdIndicator_ShowColdValues_True_ShowsAllValues()
{
var indicator = new VwapsdIndicator { NumDevs = 2.0, ShowColdValues = true };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
// With ShowColdValues=true, all values should be finite
double vwap = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(vwap));
}
// ── ReInitialize Updates Series Names ───────────────────────────────
[Fact]
public void VwapsdIndicator_ReInitialize_UpdatesSigmaNotation()
{
var indicator = new VwapsdIndicator { NumDevs = 2.0 };
indicator.Initialize();
Assert.Equal("Upper (+2.0σ)", indicator.LinesSeries[1].Name);
Assert.Equal("Lower (-2.0σ)", indicator.LinesSeries[2].Name);
// Change NumDevs and re-init
indicator.NumDevs = 3.0;
indicator.Initialize();
Assert.Equal("Upper (+3.0σ)", indicator.LinesSeries[1].Name);
Assert.Equal("Lower (-3.0σ)", indicator.LinesSeries[2].Name);
}
// ── VWAP Series Name Unchanged After Init ───────────────────────────
[Fact]
public void VwapsdIndicator_Initialize_VwapAndWidthNames_Unchanged()
{
var indicator = new VwapsdIndicator { NumDevs = 2.0 };
indicator.Initialize();
// VWAP and Width series names should remain as constructor set them
Assert.Equal("VWAP", indicator.LinesSeries[0].Name);
Assert.Equal("Width", indicator.LinesSeries[3].Name);
}
}
+712
View File
@@ -0,0 +1,712 @@
namespace QuanTAlib.Tests;
/// <summary>
/// Tests for BiInputIndicatorBase abstract class, exercised through Mae (simplest subclass).
/// Covers: constructor validation, Period/IsHot/Name/WarmupPeriod properties,
/// Update(TValue,TValue), Update(double,double), Update(TValue) throws, Update(TSeries) throws,
/// Prime throws, Reset, SanitizeActual/Predicted (NaN, Infinity, first-value-NaN),
/// ProcessNewBar, ProcessBarCorrection (isNew=false), sliding window, resync,
/// CalculateImpl, ValidateBatchInputs, Dispose, Pub event, PostProcess (via Rmse).
/// </summary>
public class BiInputIndicatorBaseTests
{
// ═══════════════════════════════ Constructor ═══════════════════════════════
[Fact]
public void Constructor_ZeroPeriod_Throws()
{
Assert.Throws<ArgumentException>(() => new Mae(0));
}
[Fact]
public void Constructor_NegativePeriod_Throws()
{
Assert.Throws<ArgumentException>(() => new Mae(-1));
}
[Fact]
public void Constructor_LargeNegativePeriod_Throws()
{
Assert.Throws<ArgumentException>(() => new Mae(-100));
}
[Fact]
public void Constructor_ValidPeriod_Succeeds()
{
var indicator = new Mae(10);
Assert.NotNull(indicator);
}
[Fact]
public void Constructor_PeriodOne_IsValid()
{
var indicator = new Mae(1);
Assert.NotNull(indicator);
Assert.Equal(1, indicator.Period);
}
// ═══════════════════════════════ Properties ═══════════════════════════════
[Fact]
public void Period_ReturnsConstructorValue()
{
Assert.Equal(5, new Mae(5).Period);
Assert.Equal(20, new Mae(20).Period);
Assert.Equal(100, new Mae(100).Period);
}
[Fact]
public void WarmupPeriod_EqualsPeriod()
{
var indicator = new Mae(14);
Assert.Equal(14, indicator.WarmupPeriod);
}
[Fact]
public void Name_ContainsIndicatorName()
{
var indicator = new Mae(10);
Assert.Contains("Mae", indicator.Name, StringComparison.Ordinal);
}
[Fact]
public void IsHot_FalseInitially()
{
var indicator = new Mae(5);
Assert.False(indicator.IsHot);
}
[Fact]
public void IsHot_FalseBeforePeriodReached()
{
var indicator = new Mae(5);
for (int i = 0; i < 4; i++)
{
indicator.Update(i * 10.0, i * 10.0 + 5.0);
Assert.False(indicator.IsHot);
}
}
[Fact]
public void IsHot_TrueAfterPeriodReached()
{
var indicator = new Mae(5);
for (int i = 0; i < 5; i++)
{
indicator.Update(i * 10.0, i * 10.0 + 5.0);
}
Assert.True(indicator.IsHot);
}
[Fact]
public void IsHot_StaysTrueAfterMoreUpdates()
{
var indicator = new Mae(3);
for (int i = 0; i < 20; i++)
{
indicator.Update(i * 10.0, i * 10.0 + 5.0);
}
Assert.True(indicator.IsHot);
}
[Fact]
public void Last_DefaultBeforeUpdate()
{
var indicator = new Mae(5);
Assert.Equal(0.0, indicator.Last.Value);
}
// ═══════════════════════════════ Update(double, double) ═══════════════════
[Fact]
public void Update_DoubleDouble_ReturnsResult()
{
var indicator = new Mae(3);
var result = indicator.Update(100.0, 110.0);
Assert.Equal(10.0, result.Value, 10);
}
[Fact]
public void Update_DoubleDouble_SetsLast()
{
var indicator = new Mae(3);
indicator.Update(100.0, 110.0);
Assert.Equal(10.0, indicator.Last.Value, 10);
}
[Fact]
public void Update_DoubleDouble_IsNewDefaultTrue()
{
var indicator = new Mae(3);
indicator.Update(100.0, 110.0);
indicator.Update(200.0, 220.0);
// Two distinct updates means 2 bars were added
Assert.Equal(15.0, indicator.Last.Value, 10); // (10 + 20) / 2
}
// ═══════════════════════════════ Update(TValue, TValue) ═══════════════════
[Fact]
public void Update_TValueTValue_ReturnsResult()
{
var indicator = new Mae(3);
var now = DateTime.UtcNow;
var result = indicator.Update(
new TValue(now, 100.0),
new TValue(now, 110.0));
Assert.Equal(10.0, result.Value, 10);
}
[Fact]
public void Update_TValueTValue_PreservesTime()
{
var indicator = new Mae(3);
var now = DateTime.UtcNow;
var result = indicator.Update(
new TValue(now, 50.0),
new TValue(now, 60.0));
Assert.Equal(now.Ticks, result.Time);
}
// ═══════════════════════════════ Single-input throws ═══════════════════════
[Fact]
public void Update_SingleTValue_Throws()
{
var indicator = new Mae(5);
Assert.Throws<NotSupportedException>(() =>
indicator.Update(new TValue(DateTime.UtcNow, 100.0)));
}
[Fact]
public void Update_SingleTSeries_Throws()
{
var indicator = new Mae(5);
Assert.Throws<NotSupportedException>(() =>
indicator.Update(new TSeries()));
}
[Fact]
public void Prime_SingleSpan_Throws()
{
var indicator = new Mae(5);
Assert.Throws<NotSupportedException>(() =>
indicator.Prime(new double[] { 1, 2, 3 }));
}
// ═══════════════════════════════ Sliding Window ═══════════════════════════
[Fact]
public void SlidingWindow_DropsOldestValue()
{
var indicator = new Mae(3);
// |10-15|=5, |20-30|=10, |30-25|=5 → mean=(5+10+5)/3=6.667
indicator.Update(10.0, 15.0);
indicator.Update(20.0, 30.0);
indicator.Update(30.0, 25.0);
Assert.Equal(20.0 / 3.0, indicator.Last.Value, 10);
// Window slides: drop 5, add |40-50|=10 → mean=(10+5+10)/3=8.333
indicator.Update(40.0, 50.0);
Assert.Equal(25.0 / 3.0, indicator.Last.Value, 10);
}
[Fact]
public void SlidingWindow_PeriodOne_AlwaysLatestError()
{
var indicator = new Mae(1);
indicator.Update(10.0, 15.0);
Assert.Equal(5.0, indicator.Last.Value, 10);
indicator.Update(20.0, 30.0);
Assert.Equal(10.0, indicator.Last.Value, 10);
indicator.Update(100.0, 100.0);
Assert.Equal(0.0, indicator.Last.Value, 10);
}
// ═══════════════════════════════ Bar Correction (isNew=false) ═════════════
[Fact]
public void BarCorrection_OverwritesLastBar()
{
var indicator = new Mae(5);
indicator.Update(100.0, 110.0); // error=10
indicator.Update(200.0, 220.0); // error=20
// Correct last bar
indicator.Update(200.0, 210.0, isNew: false); // error=10
// Mean = (10 + 10) / 2 = 10
Assert.Equal(10.0, indicator.Last.Value, 10);
}
[Fact]
public void BarCorrection_MultipleCorrections_LastOneWins()
{
var indicator = new Mae(5);
indicator.Update(100.0, 110.0); // error=10
indicator.Update(200.0, 220.0, isNew: true); // error=20
// Multiple corrections to same bar
indicator.Update(200.0, 215.0, isNew: false); // error=15
indicator.Update(200.0, 205.0, isNew: false); // error=5
indicator.Update(200.0, 203.0, isNew: false); // error=3
// Mean = (10 + 3) / 2 = 6.5
Assert.Equal(6.5, indicator.Last.Value, 10);
}
[Fact]
public void BarCorrection_RestoresToOriginalWhenSameValue()
{
var indicator = new Mae(5);
for (int i = 0; i < 10; i++)
{
indicator.Update(i * 10.0, i * 10.0 + 5.0);
}
double original = indicator.Last.Value;
// Correct with different values
indicator.Update(999.0, 888.0, isNew: false);
Assert.NotEqual(original, indicator.Last.Value);
// Restore original
indicator.Update(90.0, 95.0, isNew: false);
Assert.Equal(original, indicator.Last.Value, 10);
}
// ═══════════════════════════════ NaN/Infinity Sanitization ════════════════
[Fact]
public void NaN_Actual_UsesLastValidActual()
{
var indicator = new Mae(5);
indicator.Update(100.0, 110.0);
indicator.Update(double.NaN, 120.0);
Assert.True(double.IsFinite(indicator.Last.Value));
}
[Fact]
public void NaN_Predicted_UsesLastValidPredicted()
{
var indicator = new Mae(5);
indicator.Update(100.0, 110.0);
indicator.Update(120.0, double.NaN);
Assert.True(double.IsFinite(indicator.Last.Value));
}
[Fact]
public void NaN_Both_UsesLastValidValues()
{
var indicator = new Mae(5);
indicator.Update(100.0, 110.0);
indicator.Update(120.0, 130.0);
var result = indicator.Update(double.NaN, double.NaN);
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void PositiveInfinity_Sanitized()
{
var indicator = new Mae(5);
indicator.Update(100.0, 110.0);
var result = indicator.Update(double.PositiveInfinity, 120.0);
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void NegativeInfinity_Sanitized()
{
var indicator = new Mae(5);
indicator.Update(100.0, 110.0);
var result = indicator.Update(120.0, double.NegativeInfinity);
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void FirstValue_NaN_ReturnsZeroSubstitute()
{
var indicator = new Mae(5);
var result = indicator.Update(double.NaN, double.NaN);
// When no last valid value exists, 0.0 is substituted
Assert.True(double.IsFinite(result.Value));
Assert.Equal(0.0, result.Value, 10);
}
[Fact]
public void MultipleConsecutiveNaN_AllFinite()
{
var indicator = new Mae(5);
indicator.Update(100.0, 110.0);
for (int i = 0; i < 10; i++)
{
var result = indicator.Update(double.NaN, double.NaN);
Assert.True(double.IsFinite(result.Value));
}
}
// ═══════════════════════════════ Reset ════════════════════════════════════
[Fact]
public void Reset_ClearsIsHot()
{
var indicator = new Mae(3);
for (int i = 0; i < 5; i++)
{
indicator.Update(i * 10.0, i * 10.0 + 5.0);
}
Assert.True(indicator.IsHot);
indicator.Reset();
Assert.False(indicator.IsHot);
}
[Fact]
public void Reset_ClearsLast()
{
var indicator = new Mae(3);
indicator.Update(100.0, 110.0);
Assert.NotEqual(0.0, indicator.Last.Value);
indicator.Reset();
Assert.Equal(0.0, indicator.Last.Value);
}
[Fact]
public void Reset_AllowsReuse()
{
var indicator = new Mae(3);
// First use
for (int i = 0; i < 5; i++)
{
indicator.Update(100.0, 110.0);
}
double firstResult = indicator.Last.Value;
indicator.Reset();
// Second use - should produce same results
for (int i = 0; i < 5; i++)
{
indicator.Update(100.0, 110.0);
}
double secondResult = indicator.Last.Value;
Assert.Equal(firstResult, secondResult, 10);
}
// ═══════════════════════════════ Resync ═══════════════════════════════════
[Fact]
public void Resync_After1000Updates_MaintainsAccuracy()
{
var indicator = new Mae(5);
for (int i = 0; i < 1100; i++)
{
indicator.Update(i * 1.0, i + 10.0);
}
// Constant error of 10, so MAE should be 10
Assert.Equal(10.0, indicator.Last.Value, 8);
}
// ═══════════════════════════════ Pub Event ════════════════════════════════
[Fact]
public void PubEvent_FiredOnUpdate()
{
var indicator = new Mae(3);
int eventCount = 0;
TValuePublishedHandler handler = (object? sender, in TValueEventArgs args) => eventCount++;
indicator.Pub += handler;
indicator.Update(100.0, 110.0);
Assert.Equal(1, eventCount);
indicator.Update(200.0, 220.0);
Assert.Equal(2, eventCount);
indicator.Pub -= handler;
}
[Fact]
public void PubEvent_FiredOnBarCorrection()
{
var indicator = new Mae(3);
int eventCount = 0;
TValuePublishedHandler handler = (object? sender, in TValueEventArgs args) => eventCount++;
indicator.Pub += handler;
indicator.Update(100.0, 110.0);
indicator.Update(100.0, 120.0, isNew: false); // correction
Assert.Equal(2, eventCount);
indicator.Pub -= handler;
}
// ═══════════════════════════════ PostProcess (via Rmse) ═══════════════════
[Fact]
public void PostProcess_Rmse_AppliesSqrt()
{
var rmse = new Rmse(3);
// |10-15|²=25, RMSE=sqrt(25/1)=5
var result = rmse.Update(10.0, 15.0);
Assert.Equal(5.0, result.Value, 10);
}
[Fact]
public void PostProcess_Mae_ReturnsUnchanged()
{
var mae = new Mae(3);
// |10-15|=5, MAE=5/1=5
var result = mae.Update(10.0, 15.0);
Assert.Equal(5.0, result.Value, 10);
}
// ═══════════════════════════════ ValidateBatchInputs ═════════════════════
[Fact]
public void BatchValidation_MismatchedLengths_Throws()
{
double[] actual = [1, 2, 3];
double[] predicted = [1, 2, 3, 4, 5];
double[] output = new double[3];
Assert.Throws<ArgumentException>(() =>
Mae.Batch(actual.AsSpan(), predicted.AsSpan(), output.AsSpan(), 3));
}
[Fact]
public void BatchValidation_MismatchedOutput_Throws()
{
double[] actual = [1, 2, 3, 4, 5];
double[] predicted = [1, 2, 3, 4, 5];
double[] output = new double[3];
Assert.Throws<ArgumentException>(() =>
Mae.Batch(actual.AsSpan(), predicted.AsSpan(), output.AsSpan(), 3));
}
[Fact]
public void BatchValidation_ZeroPeriod_Throws()
{
double[] actual = [1, 2, 3];
double[] predicted = [1, 2, 3];
double[] output = new double[3];
Assert.Throws<ArgumentException>(() =>
Mae.Batch(actual.AsSpan(), predicted.AsSpan(), output.AsSpan(), 0));
}
[Fact]
public void BatchValidation_NegativePeriod_Throws()
{
double[] actual = [1, 2, 3];
double[] predicted = [1, 2, 3];
double[] output = new double[3];
Assert.Throws<ArgumentException>(() =>
Mae.Batch(actual.AsSpan(), predicted.AsSpan(), output.AsSpan(), -5));
}
[Fact]
public void BatchValidation_EmptyInput_NoException()
{
double[] actual = [];
double[] predicted = [];
double[] output = [];
Mae.Batch(actual.AsSpan(), predicted.AsSpan(), output.AsSpan(), 3);
Assert.True(true); // Verify no exception thrown
}
// ═══════════════════════════════ CalculateImpl (via Batch TSeries) ════════
[Fact]
public void CalculateImpl_MismatchedSeries_Throws()
{
var actual = new TSeries();
var predicted = new TSeries();
var now = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
actual.Add(now.AddMinutes(i), i * 10.0);
}
for (int i = 0; i < 5; i++)
{
predicted.Add(now.AddMinutes(i), i * 10.0);
}
Assert.Throws<ArgumentException>(() => Mae.Batch(actual, predicted, 3));
}
[Fact]
public void CalculateImpl_ValidSeries_ReturnsCorrectCount()
{
var actual = new TSeries();
var predicted = new TSeries();
var now = DateTime.UtcNow;
for (int i = 0; i < 20; i++)
{
actual.Add(now.AddMinutes(i), i * 10.0);
predicted.Add(now.AddMinutes(i), i * 10.0 + 5.0);
}
var result = Mae.Batch(actual, predicted, 5);
Assert.Equal(20, result.Count);
}
[Fact]
public void CalculateImpl_ConstantError_AllWindowedValuesEqual()
{
var actual = new TSeries();
var predicted = new TSeries();
var now = DateTime.UtcNow;
for (int i = 0; i < 20; i++)
{
actual.Add(now.AddMinutes(i), 100.0);
predicted.Add(now.AddMinutes(i), 107.0);
}
var result = Mae.Batch(actual, predicted, 5);
// Once window is full (index >= 4), all values should be 7.0
for (int i = 4; i < 20; i++)
{
Assert.Equal(7.0, result[i].Value, 10);
}
}
// ═══════════════════════════════ Calculate static ═════════════════════════
[Fact]
public void Calculate_ReturnsResultsAndIndicator()
{
var actual = new TSeries();
var predicted = new TSeries();
var now = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
actual.Add(now.AddMinutes(i), i * 10.0);
predicted.Add(now.AddMinutes(i), i * 10.0 + 3.0);
}
var (results, indicator) = Mae.Calculate(actual, predicted, 5);
Assert.Equal(10, results.Count);
Assert.NotNull(indicator);
Assert.Equal(5, indicator.Period);
}
// ═══════════════════════════════ Dispose ═════════════════════════════════
[Fact]
public void Dispose_DoesNotThrow()
{
var indicator = new Mae(5);
indicator.Update(100.0, 110.0);
indicator.Dispose();
Assert.True(true); // Verify no exception thrown
}
[Fact]
public void Dispose_CalledMultipleTimes_NoException()
{
var indicator = new Mae(5);
indicator.Dispose();
indicator.Dispose();
Assert.True(true); // Verify no exception thrown
}
// ═══════════════════════════════ Batch vs Streaming ═════════════════════
[Fact]
public void Batch_MatchesStreaming_RandomData()
{
const int period = 7;
const int count = 200;
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
double[] actual = new double[count];
double[] predicted = new double[count];
for (int i = 0; i < count; i++)
{
var bar = gbm.Next();
actual[i] = bar.Close;
predicted[i] = bar.Close * 1.03 + 1.0;
}
// Streaming
var mae = new Mae(period);
double[] streamResults = new double[count];
for (int i = 0; i < count; i++)
{
streamResults[i] = mae.Update(actual[i], predicted[i]).Value;
}
// Batch
double[] batchResults = new double[count];
Mae.Batch(actual, predicted, batchResults, period);
for (int i = 0; i < count; i++)
{
Assert.Equal(streamResults[i], batchResults[i], 9);
}
}
// ═══════════════════════════════ Edge Cases ═══════════════════════════════
[Fact]
public void Update_LargeValues_NoOverflow()
{
var indicator = new Mae(3);
indicator.Update(1e300, 1e300 + 1e290);
Assert.True(double.IsFinite(indicator.Last.Value));
}
[Fact]
public void Update_VerySmallValues_NoUnderflow()
{
var indicator = new Mae(3);
indicator.Update(1e-300, 2e-300);
Assert.True(double.IsFinite(indicator.Last.Value));
}
[Fact]
public void Update_ZeroValues_ReturnsZero()
{
var indicator = new Mae(3);
indicator.Update(0.0, 0.0);
Assert.Equal(0.0, indicator.Last.Value, 10);
}
[Fact]
public void Update_NegativeValues_HandledCorrectly()
{
var indicator = new Mae(3);
var result = indicator.Update(-100.0, -110.0);
Assert.Equal(10.0, result.Value, 10);
}
[Fact]
public void Update_MixedSignValues_AbsoluteError()
{
var indicator = new Mae(1);
var result = indicator.Update(-50.0, 50.0);
Assert.Equal(100.0, result.Value, 10);
}
}
@@ -0,0 +1,655 @@
namespace QuanTAlib.Tests;
/// <summary>
/// Tests for MonotonicDeque — O(1) amortized sliding window min/max data structure.
/// Covers: constructor validation, PushMax, PushMin, GetExtremum, Reset,
/// RebuildMax, RebuildMin, FrontIndex, Count, window expiration,
/// edge cases (equal values, descending/ascending sequences).
/// </summary>
public class MonotonicDequeTests
{
// ═══════════════════════════════ Constructor ═══════════════════════════════
[Fact]
public void Constructor_ZeroPeriod_Throws()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new MonotonicDeque(0));
}
[Fact]
public void Constructor_NegativePeriod_Throws()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new MonotonicDeque(-1));
}
[Fact]
public void Constructor_ValidPeriod_Succeeds()
{
var deque = new MonotonicDeque(5);
Assert.NotNull(deque);
}
[Fact]
public void Constructor_PeriodOne_IsValid()
{
var deque = new MonotonicDeque(1);
Assert.Equal(0, deque.Count);
}
// ═══════════════════════════════ Initial State ════════════════════════════
[Fact]
public void Count_InitiallyZero()
{
var deque = new MonotonicDeque(5);
Assert.Equal(0, deque.Count);
}
[Fact]
public void FrontIndex_InitiallyNegativeOne()
{
var deque = new MonotonicDeque(5);
Assert.Equal(-1, deque.FrontIndex);
}
[Fact]
public void GetExtremum_Empty_ReturnsNaN()
{
var deque = new MonotonicDeque(5);
double[] buffer = new double[5];
Assert.True(double.IsNaN(deque.GetExtremum(buffer)));
}
// ═══════════════════════════════ PushMax ═══════════════════════════════════
[Fact]
public void PushMax_SingleValue_Tracked()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
buffer[0] = 100.0;
deque.PushMax(0, 100.0, buffer);
Assert.Equal(1, deque.Count);
Assert.Equal(100.0, deque.GetExtremum(buffer));
}
[Fact]
public void PushMax_AscendingValues_TracksMaximum()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
for (int i = 0; i < 5; i++)
{
buffer[i % period] = i * 10.0;
deque.PushMax(i, i * 10.0, buffer);
}
// Maximum should be 40 (last value in ascending sequence)
Assert.Equal(40.0, deque.GetExtremum(buffer));
}
[Fact]
public void PushMax_DescendingValues_TracksMaximum()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
for (int i = 0; i < 5; i++)
{
double val = (4 - i) * 10.0;
buffer[i % period] = val;
deque.PushMax(i, val, buffer);
}
// Maximum should be 40 (first value in descending sequence)
Assert.Equal(40.0, deque.GetExtremum(buffer));
}
[Fact]
public void PushMax_EqualValues_TracksCorrectly()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
for (int i = 0; i < 5; i++)
{
buffer[i % period] = 50.0;
deque.PushMax(i, 50.0, buffer);
}
Assert.Equal(50.0, deque.GetExtremum(buffer));
}
[Fact]
public void PushMax_WindowExpiration_DropsOldMax()
{
int period = 3;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
// Push: 100, 50, 30, then 20 (window slides past 100)
buffer[0 % period] = 100.0;
deque.PushMax(0, 100.0, buffer);
Assert.Equal(100.0, deque.GetExtremum(buffer));
buffer[1 % period] = 50.0;
deque.PushMax(1, 50.0, buffer);
Assert.Equal(100.0, deque.GetExtremum(buffer));
buffer[2 % period] = 30.0;
deque.PushMax(2, 30.0, buffer);
Assert.Equal(100.0, deque.GetExtremum(buffer));
// Index 3 — window now [1,2,3], so 100 (index 0) expires
buffer[3 % period] = 20.0;
deque.PushMax(3, 20.0, buffer);
Assert.Equal(50.0, deque.GetExtremum(buffer));
}
[Fact]
public void PushMax_NewMaxReplacesAll()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
buffer[0 % period] = 10.0;
deque.PushMax(0, 10.0, buffer);
buffer[1 % period] = 20.0;
deque.PushMax(1, 20.0, buffer);
buffer[2 % period] = 30.0;
deque.PushMax(2, 30.0, buffer);
// New value 100 should replace all (they're all <=)
buffer[3 % period] = 100.0;
deque.PushMax(3, 100.0, buffer);
Assert.Equal(100.0, deque.GetExtremum(buffer));
Assert.Equal(1, deque.Count);
}
// ═══════════════════════════════ PushMin ═══════════════════════════════════
[Fact]
public void PushMin_SingleValue_Tracked()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
buffer[0] = 100.0;
deque.PushMin(0, 100.0, buffer);
Assert.Equal(1, deque.Count);
Assert.Equal(100.0, deque.GetExtremum(buffer));
}
[Fact]
public void PushMin_DescendingValues_TracksMinimum()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
for (int i = 0; i < 5; i++)
{
double val = (4 - i) * 10.0;
buffer[i % period] = val;
deque.PushMin(i, val, buffer);
}
// Minimum should be 0 (last value in descending sequence)
Assert.Equal(0.0, deque.GetExtremum(buffer));
}
[Fact]
public void PushMin_AscendingValues_TracksMinimum()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
for (int i = 0; i < 5; i++)
{
buffer[i % period] = i * 10.0;
deque.PushMin(i, i * 10.0, buffer);
}
// Minimum should be 0 (first value in ascending sequence)
Assert.Equal(0.0, deque.GetExtremum(buffer));
}
[Fact]
public void PushMin_EqualValues_TracksCorrectly()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
for (int i = 0; i < 5; i++)
{
buffer[i % period] = 50.0;
deque.PushMin(i, 50.0, buffer);
}
Assert.Equal(50.0, deque.GetExtremum(buffer));
}
[Fact]
public void PushMin_WindowExpiration_DropsOldMin()
{
int period = 3;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
// Push: 10, 50, 80, then 90 (window slides past 10)
buffer[0 % period] = 10.0;
deque.PushMin(0, 10.0, buffer);
Assert.Equal(10.0, deque.GetExtremum(buffer));
buffer[1 % period] = 50.0;
deque.PushMin(1, 50.0, buffer);
Assert.Equal(10.0, deque.GetExtremum(buffer));
buffer[2 % period] = 80.0;
deque.PushMin(2, 80.0, buffer);
Assert.Equal(10.0, deque.GetExtremum(buffer));
// Index 3 — window now [1,2,3], so 10 (index 0) expires
buffer[3 % period] = 90.0;
deque.PushMin(3, 90.0, buffer);
Assert.Equal(50.0, deque.GetExtremum(buffer));
}
[Fact]
public void PushMin_NewMinReplacesAll()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
buffer[0 % period] = 100.0;
deque.PushMin(0, 100.0, buffer);
buffer[1 % period] = 80.0;
deque.PushMin(1, 80.0, buffer);
buffer[2 % period] = 60.0;
deque.PushMin(2, 60.0, buffer);
// New value 5 should replace all (they're all >=)
buffer[3 % period] = 5.0;
deque.PushMin(3, 5.0, buffer);
Assert.Equal(5.0, deque.GetExtremum(buffer));
Assert.Equal(1, deque.Count);
}
// ═══════════════════════════════ FrontIndex ═══════════════════════════════
[Fact]
public void FrontIndex_TracksCurrentExtremumIndex()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
buffer[0] = 100.0;
deque.PushMax(0, 100.0, buffer);
Assert.Equal(0, deque.FrontIndex);
buffer[1 % period] = 50.0;
deque.PushMax(1, 50.0, buffer);
Assert.Equal(0, deque.FrontIndex); // 100 is still max
buffer[2 % period] = 200.0;
deque.PushMax(2, 200.0, buffer);
Assert.Equal(2, deque.FrontIndex); // 200 is new max
}
// ═══════════════════════════════ Reset ════════════════════════════════════
[Fact]
public void Reset_ClearsCount()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
for (int i = 0; i < 3; i++)
{
buffer[i] = i * 10.0;
deque.PushMax(i, i * 10.0, buffer);
}
Assert.True(deque.Count > 0);
deque.Reset();
Assert.Equal(0, deque.Count);
}
[Fact]
public void Reset_FrontIndexBecomesNegativeOne()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
buffer[0] = 50.0;
deque.PushMax(0, 50.0, buffer);
deque.Reset();
Assert.Equal(-1, deque.FrontIndex);
}
[Fact]
public void Reset_GetExtremumReturnsNaN()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
buffer[0] = 50.0;
deque.PushMax(0, 50.0, buffer);
deque.Reset();
Assert.True(double.IsNaN(deque.GetExtremum(buffer)));
}
[Fact]
public void Reset_AllowsReuse()
{
int period = 3;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
buffer[0] = 100.0;
deque.PushMax(0, 100.0, buffer);
deque.Reset();
buffer[0] = 50.0;
deque.PushMax(0, 50.0, buffer);
Assert.Equal(50.0, deque.GetExtremum(buffer));
Assert.Equal(1, deque.Count);
}
// ═══════════════════════════════ RebuildMax ═══════════════════════════════
[Fact]
public void RebuildMax_EmptyBuffer_NoOp()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
deque.RebuildMax(buffer, 0, 0);
Assert.Equal(0, deque.Count);
}
[Fact]
public void RebuildMax_RebuildsCorrectly()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
// Fill buffer: [10, 50, 30, 20, 40]
buffer[0] = 10.0;
buffer[1] = 50.0;
buffer[2] = 30.0;
buffer[3] = 20.0;
buffer[4] = 40.0;
deque.RebuildMax(buffer, 4, 5);
// Maximum should be 50 (at index 1)
Assert.Equal(50.0, deque.GetExtremum(buffer));
}
[Fact]
public void RebuildMax_SingleElement()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
buffer[0] = 42.0;
deque.RebuildMax(buffer, 0, 1);
Assert.Equal(42.0, deque.GetExtremum(buffer));
Assert.Equal(1, deque.Count);
}
[Fact]
public void RebuildMax_AfterPreviousData_Resets()
{
int period = 3;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
// First push some data
buffer[0] = 100.0;
deque.PushMax(0, 100.0, buffer);
buffer[1] = 200.0;
deque.PushMax(1, 200.0, buffer);
// Rebuild with different data
buffer[0] = 10.0;
buffer[1] = 20.0;
buffer[2] = 15.0;
deque.RebuildMax(buffer, 2, 3);
Assert.Equal(20.0, deque.GetExtremum(buffer));
}
// ═══════════════════════════════ RebuildMin ═══════════════════════════════
[Fact]
public void RebuildMin_EmptyBuffer_NoOp()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
deque.RebuildMin(buffer, 0, 0);
Assert.Equal(0, deque.Count);
}
[Fact]
public void RebuildMin_RebuildsCorrectly()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
// Fill buffer: [10, 50, 30, 20, 40]
buffer[0] = 10.0;
buffer[1] = 50.0;
buffer[2] = 30.0;
buffer[3] = 20.0;
buffer[4] = 40.0;
deque.RebuildMin(buffer, 4, 5);
// Minimum should be 10 (at index 0)
Assert.Equal(10.0, deque.GetExtremum(buffer));
}
[Fact]
public void RebuildMin_SingleElement()
{
int period = 5;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
buffer[0] = 42.0;
deque.RebuildMin(buffer, 0, 1);
Assert.Equal(42.0, deque.GetExtremum(buffer));
Assert.Equal(1, deque.Count);
}
[Fact]
public void RebuildMin_AfterPreviousData_Resets()
{
int period = 3;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
// First push some data
buffer[0] = 5.0;
deque.PushMin(0, 5.0, buffer);
buffer[1] = 3.0;
deque.PushMin(1, 3.0, buffer);
// Rebuild with different data
buffer[0] = 100.0;
buffer[1] = 200.0;
buffer[2] = 150.0;
deque.RebuildMin(buffer, 2, 3);
Assert.Equal(100.0, deque.GetExtremum(buffer));
}
// ═══════════════════════════════ Sliding Window Scenarios ═════════════════
[Fact]
public void PushMax_LongSequence_TracksRollingMax()
{
int period = 3;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
double[] values = [10, 20, 30, 15, 25, 5, 35, 10, 40, 20];
double[] expectedMax = [10, 20, 30, 30, 30, 25, 35, 35, 40, 40];
for (int i = 0; i < values.Length; i++)
{
buffer[i % period] = values[i];
deque.PushMax(i, values[i], buffer);
double actual = deque.GetExtremum(buffer);
Assert.True(actual == expectedMax[i],
$"Max mismatch at index {i}: expected {expectedMax[i]}, got {actual}");
}
}
[Fact]
public void PushMin_LongSequence_TracksRollingMin()
{
int period = 3;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
double[] values = [50, 40, 30, 45, 35, 55, 25, 60, 20, 50];
double[] expectedMin = [50, 40, 30, 30, 30, 35, 25, 25, 20, 20];
for (int i = 0; i < values.Length; i++)
{
buffer[i % period] = values[i];
deque.PushMin(i, values[i], buffer);
double actual = deque.GetExtremum(buffer);
Assert.True(actual == expectedMin[i],
$"Min mismatch at index {i}: expected {expectedMin[i]}, got {actual}");
}
}
// ═══════════════════════════════ Period 1 ═════════════════════════════════
[Fact]
public void PushMax_PeriodOne_AlwaysLatestValue()
{
int period = 1;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
buffer[0] = 100.0;
deque.PushMax(0, 100.0, buffer);
Assert.Equal(100.0, deque.GetExtremum(buffer));
buffer[0] = 50.0;
deque.PushMax(1, 50.0, buffer);
Assert.Equal(50.0, deque.GetExtremum(buffer));
buffer[0] = 200.0;
deque.PushMax(2, 200.0, buffer);
Assert.Equal(200.0, deque.GetExtremum(buffer));
}
[Fact]
public void PushMin_PeriodOne_AlwaysLatestValue()
{
int period = 1;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
buffer[0] = 100.0;
deque.PushMin(0, 100.0, buffer);
Assert.Equal(100.0, deque.GetExtremum(buffer));
buffer[0] = 50.0;
deque.PushMin(1, 50.0, buffer);
Assert.Equal(50.0, deque.GetExtremum(buffer));
}
// ═══════════════════════════════ Edge Cases ═══════════════════════════════
[Fact]
public void PushMax_NegativeValues_TracksCorrectly()
{
int period = 3;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
buffer[0] = -30.0;
deque.PushMax(0, -30.0, buffer);
buffer[1 % period] = -10.0;
deque.PushMax(1, -10.0, buffer);
buffer[2 % period] = -20.0;
deque.PushMax(2, -20.0, buffer);
Assert.Equal(-10.0, deque.GetExtremum(buffer));
}
[Fact]
public void PushMin_NegativeValues_TracksCorrectly()
{
int period = 3;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
buffer[0] = -10.0;
deque.PushMin(0, -10.0, buffer);
buffer[1 % period] = -30.0;
deque.PushMin(1, -30.0, buffer);
buffer[2 % period] = -20.0;
deque.PushMin(2, -20.0, buffer);
Assert.Equal(-30.0, deque.GetExtremum(buffer));
}
[Fact]
public void PushMax_VeryLargeValues_NoOverflow()
{
int period = 3;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
buffer[0] = 1e300;
deque.PushMax(0, 1e300, buffer);
Assert.Equal(1e300, deque.GetExtremum(buffer));
}
[Fact]
public void PushMax_ZeroValues_TracksCorrectly()
{
int period = 3;
var deque = new MonotonicDeque(period);
double[] buffer = new double[period];
for (int i = 0; i < 3; i++)
{
buffer[i] = 0.0;
deque.PushMax(i, 0.0, buffer);
}
Assert.Equal(0.0, deque.GetExtremum(buffer));
}
}
File diff suppressed because it is too large Load Diff
+658
View File
@@ -598,4 +598,662 @@ public class TBarSeriesTests
Assert.Equal(12.0, closes[0]);
Assert.Equal(22.0, closes[1]);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: TryGetLast
// ────────────────────────────────────────────────────────────────────
[Fact]
public void TryGetLast_EmptySeries_ReturnsFalseAndDefault()
{
var series = new TBarSeries();
bool result = series.TryGetLast(out TBar bar);
Assert.False(result);
Assert.Equal(0, bar.Time);
Assert.Equal(0.0, bar.Close);
}
[Fact]
public void TryGetLast_NonEmptySeries_ReturnsTrueAndLastBar()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
bool result = series.TryGetLast(out TBar bar);
Assert.True(result);
Assert.Equal(200, bar.Time);
Assert.Equal(20.0, bar.Open);
Assert.Equal(25.0, bar.High);
Assert.Equal(15.0, bar.Low);
Assert.Equal(22.0, bar.Close);
Assert.Equal(200.0, bar.Volume);
}
[Fact]
public void TryGetLast_SingleBar_ReturnsOnlyBar()
{
var series = new TBarSeries();
series.Add(999, 50, 60, 40, 55, 500);
bool result = series.TryGetLast(out TBar bar);
Assert.True(result);
Assert.Equal(999, bar.Time);
Assert.Equal(55.0, bar.Close);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: Span Accessors (Times, OpenValues, HighValues, etc.)
// ────────────────────────────────────────────────────────────────────
[Fact]
public void Times_ReturnsCorrectSpan()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
series.Add(300, 30, 35, 25, 32, 300);
ReadOnlySpan<long> times = series.Times;
Assert.Equal(3, times.Length);
Assert.Equal(100, times[0]);
Assert.Equal(200, times[1]);
Assert.Equal(300, times[2]);
}
[Fact]
public void Times_EmptySeries_ReturnsEmptySpan()
{
var series = new TBarSeries();
ReadOnlySpan<long> times = series.Times;
Assert.Equal(0, times.Length);
}
[Fact]
public void OpenValues_ReturnsCorrectSpan()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
ReadOnlySpan<double> opens = series.OpenValues;
Assert.Equal(2, opens.Length);
Assert.Equal(10.0, opens[0]);
Assert.Equal(20.0, opens[1]);
}
[Fact]
public void HighValues_ReturnsCorrectSpan()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
ReadOnlySpan<double> highs = series.HighValues;
Assert.Equal(2, highs.Length);
Assert.Equal(15.0, highs[0]);
Assert.Equal(25.0, highs[1]);
}
[Fact]
public void LowValues_ReturnsCorrectSpan()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
ReadOnlySpan<double> lows = series.LowValues;
Assert.Equal(2, lows.Length);
Assert.Equal(5.0, lows[0]);
Assert.Equal(15.0, lows[1]);
}
[Fact]
public void CloseValues_ReturnsCorrectSpan()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
ReadOnlySpan<double> closes = series.CloseValues;
Assert.Equal(2, closes.Length);
Assert.Equal(12.0, closes[0]);
Assert.Equal(22.0, closes[1]);
}
[Fact]
public void VolumeValues_ReturnsCorrectSpan()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
ReadOnlySpan<double> vols = series.VolumeValues;
Assert.Equal(2, vols.Length);
Assert.Equal(100.0, vols[0]);
Assert.Equal(200.0, vols[1]);
}
[Fact]
public void SpanAccessors_EmptySeries_AllReturnEmptySpans()
{
var series = new TBarSeries();
Assert.Equal(0, series.OpenValues.Length);
Assert.Equal(0, series.HighValues.Length);
Assert.Equal(0, series.LowValues.Length);
Assert.Equal(0, series.CloseValues.Length);
Assert.Equal(0, series.VolumeValues.Length);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: AddRange with 6 span parameters
// ────────────────────────────────────────────────────────────────────
[Fact]
public void AddRange_WithSpans_AddsAllBars()
{
var series = new TBarSeries();
long[] times = [100, 200, 300];
double[] opens = [10, 20, 30];
double[] highs = [15, 25, 35];
double[] lows = [5, 15, 25];
double[] closes = [12, 22, 32];
double[] volumes = [1000, 2000, 3000];
series.AddRange(times, opens, highs, lows, closes, volumes);
Assert.Equal(3, series.Count);
Assert.Equal(100, series[0].Time);
Assert.Equal(10.0, series[0].Open);
Assert.Equal(15.0, series[0].High);
Assert.Equal(5.0, series[0].Low);
Assert.Equal(12.0, series[0].Close);
Assert.Equal(1000.0, series[0].Volume);
Assert.Equal(300, series[2].Time);
Assert.Equal(32.0, series[2].Close);
}
[Fact]
public void AddRange_WithSpans_EmptySpans_DoesNothing()
{
var series = new TBarSeries();
ReadOnlySpan<long> t = ReadOnlySpan<long>.Empty;
ReadOnlySpan<double> d = ReadOnlySpan<double>.Empty;
series.AddRange(t, d, d, d, d, d);
Assert.Empty(series);
}
[Fact]
public void AddRange_WithSpans_MismatchedLengths_ThrowsArgumentException()
{
var series = new TBarSeries();
long[] times = [100, 200, 300];
double[] opens = [10, 20]; // Mismatched
double[] highs = [15, 25, 35];
double[] lows = [5, 15, 25];
double[] closes = [12, 22, 32];
double[] volumes = [1000, 2000, 3000];
Assert.Throws<ArgumentException>(() =>
series.AddRange(times, opens, highs, lows, closes, volumes));
}
[Fact]
public void AddRange_WithSpans_DoesNotFirePubEvent()
{
var series = new TBarSeries();
int pubCount = 0;
series.Pub += (object? sender, in TBarEventArgs args) => pubCount++;
long[] times = [100, 200, 300];
double[] opens = [10, 20, 30];
double[] highs = [15, 25, 35];
double[] lows = [5, 15, 25];
double[] closes = [12, 22, 32];
double[] volumes = [1000, 2000, 3000];
series.AddRange(times, opens, highs, lows, closes, volumes);
Assert.Equal(0, pubCount);
Assert.Equal(3, series.Count);
}
[Fact]
public void AddRange_WithSpans_AppendsToExistingData()
{
var series = new TBarSeries();
series.Add(50, 5, 8, 3, 6, 500);
long[] times = [100, 200];
double[] opens = [10, 20];
double[] highs = [15, 25];
double[] lows = [5, 15];
double[] closes = [12, 22];
double[] volumes = [1000, 2000];
series.AddRange(times, opens, highs, lows, closes, volumes);
Assert.Equal(3, series.Count);
Assert.Equal(50, series[0].Time);
Assert.Equal(100, series[1].Time);
Assert.Equal(200, series[2].Time);
}
[Fact]
public void AddRange_WithSpans_SubSeriesReflectData()
{
var series = new TBarSeries();
long[] times = [100, 200];
double[] opens = [10, 20];
double[] highs = [15, 25];
double[] lows = [5, 15];
double[] closes = [12, 22];
double[] volumes = [1000, 2000];
series.AddRange(times, opens, highs, lows, closes, volumes);
// Sub-series share underlying storage, so they reflect AddRange data
Assert.Equal(2, series.Open.Count);
Assert.Equal(10.0, series.Open[0].Value);
Assert.Equal(20.0, series.Open[1].Value);
Assert.Equal(22.0, series.Close[1].Value);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: AddRange with ReadOnlySpan<TBar>
// ────────────────────────────────────────────────────────────────────
[Fact]
public void AddRange_WithTBarSpan_AddsAllBars()
{
var series = new TBarSeries();
TBar[] bars =
[
new TBar(100, 10, 15, 5, 12, 1000),
new TBar(200, 20, 25, 15, 22, 2000),
new TBar(300, 30, 35, 25, 32, 3000),
];
series.AddRange(bars);
Assert.Equal(3, series.Count);
Assert.Equal(100, series[0].Time);
Assert.Equal(12.0, series[0].Close);
Assert.Equal(300, series[2].Time);
Assert.Equal(32.0, series[2].Close);
Assert.Equal(3000.0, series[2].Volume);
}
[Fact]
public void AddRange_WithTBarSpan_EmptySpan_DoesNothing()
{
var series = new TBarSeries();
series.AddRange(ReadOnlySpan<TBar>.Empty);
Assert.Empty(series);
}
[Fact]
public void AddRange_WithTBarSpan_AppendsToExistingData()
{
var series = new TBarSeries();
series.Add(50, 5, 8, 3, 6, 500);
TBar[] bars =
[
new TBar(100, 10, 15, 5, 12, 1000),
new TBar(200, 20, 25, 15, 22, 2000),
];
series.AddRange(bars);
Assert.Equal(3, series.Count);
Assert.Equal(50, series[0].Time);
Assert.Equal(100, series[1].Time);
Assert.Equal(200, series[2].Time);
}
[Fact]
public void AddRange_WithTBarSpan_DoesNotFirePubEvent()
{
var series = new TBarSeries();
int pubCount = 0;
series.Pub += (object? sender, in TBarEventArgs args) => pubCount++;
TBar[] bars = [new TBar(100, 10, 15, 5, 12, 1000)];
series.AddRange(bars);
Assert.Equal(0, pubCount);
Assert.Single(series);
}
[Fact]
public void AddRange_WithTBarSpan_CorrectlySplitsToSoA()
{
var series = new TBarSeries();
TBar[] bars =
[
new TBar(100, 10, 15, 5, 12, 1000),
new TBar(200, 20, 25, 15, 22, 2000),
];
series.AddRange(bars);
// Verify SoA layout via span accessors
Assert.Equal(100, series.Times[0]);
Assert.Equal(200, series.Times[1]);
Assert.Equal(10.0, series.OpenValues[0]);
Assert.Equal(20.0, series.OpenValues[1]);
Assert.Equal(15.0, series.HighValues[0]);
Assert.Equal(25.0, series.HighValues[1]);
Assert.Equal(5.0, series.LowValues[0]);
Assert.Equal(15.0, series.LowValues[1]);
Assert.Equal(12.0, series.CloseValues[0]);
Assert.Equal(22.0, series.CloseValues[1]);
Assert.Equal(1000.0, series.VolumeValues[0]);
Assert.Equal(2000.0, series.VolumeValues[1]);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: TBarEventArgs struct
// ────────────────────────────────────────────────────────────────────
[Fact]
public void TBarEventArgs_Equals_SameValues_ReturnsTrue()
{
var bar = new TBar(100, 10, 15, 5, 12, 1000);
var a = new TBarEventArgs { Value = bar, IsNew = true };
var b = new TBarEventArgs { Value = bar, IsNew = true };
Assert.True(a.Equals(b));
Assert.True(a == b);
Assert.False(a != b);
}
[Fact]
public void TBarEventArgs_Equals_DifferentIsNew_ReturnsFalse()
{
var bar = new TBar(100, 10, 15, 5, 12, 1000);
var a = new TBarEventArgs { Value = bar, IsNew = true };
var b = new TBarEventArgs { Value = bar, IsNew = false };
Assert.False(a.Equals(b));
Assert.False(a == b);
Assert.True(a != b);
}
[Fact]
public void TBarEventArgs_Equals_DifferentValue_ReturnsFalse()
{
var bar1 = new TBar(100, 10, 15, 5, 12, 1000);
var bar2 = new TBar(200, 20, 25, 15, 22, 2000);
var a = new TBarEventArgs { Value = bar1, IsNew = true };
var b = new TBarEventArgs { Value = bar2, IsNew = true };
Assert.False(a.Equals(b));
}
[Fact]
public void TBarEventArgs_Equals_Object_SameValues_ReturnsTrue()
{
var bar = new TBar(100, 10, 15, 5, 12, 1000);
var a = new TBarEventArgs { Value = bar, IsNew = true };
object b = new TBarEventArgs { Value = bar, IsNew = true };
Assert.True(a.Equals(b));
}
[Fact]
public void TBarEventArgs_Equals_Object_DifferentType_ReturnsFalse()
{
var bar = new TBar(100, 10, 15, 5, 12, 1000);
var a = new TBarEventArgs { Value = bar, IsNew = true };
Assert.False(a.Equals("not a TBarEventArgs"));
Assert.False(a.Equals(null));
}
[Fact]
public void TBarEventArgs_GetHashCode_EqualObjects_SameHash()
{
var bar = new TBar(100, 10, 15, 5, 12, 1000);
var a = new TBarEventArgs { Value = bar, IsNew = true };
var b = new TBarEventArgs { Value = bar, IsNew = true };
Assert.Equal(a.GetHashCode(), b.GetHashCode());
}
[Fact]
public void TBarEventArgs_GetHashCode_DifferentObjects_LikelyDifferentHash()
{
var bar1 = new TBar(100, 10, 15, 5, 12, 1000);
var bar2 = new TBar(200, 20, 25, 15, 22, 2000);
var a = new TBarEventArgs { Value = bar1, IsNew = true };
var b = new TBarEventArgs { Value = bar2, IsNew = false };
// Not guaranteed but extremely likely for distinct values
Assert.NotEqual(a.GetHashCode(), b.GetHashCode());
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: TBarSeriesEnumerator struct
// ────────────────────────────────────────────────────────────────────
[Fact]
public void TBarSeriesEnumerator_Reset_AllowsReIteration()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
var enumerator = series.GetEnumerator();
// First pass
int count1 = 0;
while (enumerator.MoveNext()) { count1++; }
Assert.Equal(2, count1);
// Reset and re-iterate
enumerator.Reset();
int count2 = 0;
while (enumerator.MoveNext()) { count2++; }
Assert.Equal(2, count2);
}
[Fact]
public void TBarSeriesEnumerator_Dispose_DoesNotThrow()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
var enumerator = series.GetEnumerator();
enumerator.MoveNext();
enumerator.Dispose(); // Should be no-op
Assert.Equal(12.0, enumerator.Current.Close);
}
[Fact]
public void TBarSeriesEnumerator_Equals_SameState_ReturnsTrue()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
var a = series.GetEnumerator();
var b = series.GetEnumerator();
// Both at initial state (_index = -1)
Assert.True(a.Equals(b));
Assert.True(a == b);
Assert.False(a != b);
}
[Fact]
public void TBarSeriesEnumerator_Equals_DifferentState_ReturnsFalse()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
var a = series.GetEnumerator();
var b = series.GetEnumerator();
a.MoveNext(); // a is at index 0, b is at -1
Assert.False(a.Equals(b));
Assert.False(a == b);
Assert.True(a != b);
}
[Fact]
public void TBarSeriesEnumerator_Equals_Object_SameState_ReturnsTrue()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
var a = series.GetEnumerator();
object b = series.GetEnumerator();
Assert.True(a.Equals(b));
}
[Fact]
public void TBarSeriesEnumerator_Equals_Object_DifferentType_ReturnsFalse()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
var a = series.GetEnumerator();
Assert.False(a.Equals("not an enumerator"));
Assert.False(a.Equals(null));
}
[Fact]
public void TBarSeriesEnumerator_GetHashCode_SameState_SameHash()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
var a = series.GetEnumerator();
var b = series.GetEnumerator();
Assert.Equal(a.GetHashCode(), b.GetHashCode());
}
[Fact]
public void TBarSeriesEnumerator_GetHashCode_DifferentState_LikelyDifferentHash()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
var a = series.GetEnumerator();
var b = series.GetEnumerator();
a.MoveNext();
Assert.NotEqual(a.GetHashCode(), b.GetHashCode());
}
[Fact]
public void TBarSeriesEnumerator_Current_ViaIEnumerator_ReturnsBoxedTBar()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
IEnumerator enumerator = series.GetEnumerator();
enumerator.MoveNext();
object current = enumerator.Current;
Assert.IsType<TBar>(current);
Assert.Equal(12.0, ((TBar)current).Close);
}
[Fact]
public void TBarSeriesEnumerator_DifferentSeries_NotEqual()
{
var series1 = new TBarSeries();
series1.Add(100, 10, 15, 5, 12, 100);
var series2 = new TBarSeries();
series2.Add(100, 10, 15, 5, 12, 100);
var a = series1.GetEnumerator();
var b = series2.GetEnumerator();
// Different underlying list references -> not equal
Assert.False(a.Equals(b));
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: AddRange large data set (capacity pre-alloc path)
// ────────────────────────────────────────────────────────────────────
[Fact]
public void AddRange_LargeDataSet_AllBarsAccessible()
{
var series = new TBarSeries();
const int N = 1000;
var times = new long[N];
var opens = new double[N];
var highs = new double[N];
var lows = new double[N];
var closes = new double[N];
var volumes = new double[N];
for (int i = 0; i < N; i++)
{
times[i] = i;
opens[i] = i * 10.0;
highs[i] = i * 10.0 + 5.0;
lows[i] = i * 10.0 - 5.0;
closes[i] = i * 10.0 + 2.0;
volumes[i] = i * 100.0;
}
series.AddRange(times, opens, highs, lows, closes, volumes);
Assert.Equal(N, series.Count);
Assert.Equal(0, series[0].Time);
Assert.Equal((N - 1) * 10.0 + 2.0, series[N - 1].Close);
Assert.Equal((N - 1) * 100.0, series[N - 1].Volume);
}
[Fact]
public void AddRange_TBarSpan_LargeDataSet_AllBarsAccessible()
{
var series = new TBarSeries();
const int N = 500;
var bars = new TBar[N];
for (int i = 0; i < N; i++)
{
bars[i] = new TBar(i, i * 10.0, i * 10.0 + 5.0, i * 10.0 - 5.0, i * 10.0 + 2.0, i * 100.0);
}
series.AddRange(bars);
Assert.Equal(N, series.Count);
Assert.Equal(0, series[0].Time);
Assert.Equal(2.0, series[0].Close);
Assert.Equal((N - 1) * 10.0 + 2.0, series[N - 1].Close);
}
}
+208
View File
@@ -583,4 +583,212 @@ public class TSeriesTests
Assert.Equal(1.0, values[0]);
Assert.Equal(2.0, values[1]);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: ShareStorageTag struct
// ────────────────────────────────────────────────────────────────────
[Fact]
public void ShareStorageTag_Instance_IsDefault()
{
var tag = ShareStorageTag.Instance;
// It's a readonly struct with no fields — default should work
Assert.Equal(default(ShareStorageTag), tag);
}
[Fact]
public void ShareStorageTag_TwoInstances_AreEqual()
{
var a = ShareStorageTag.Instance;
var b = ShareStorageTag.Instance;
Assert.Equal(a, b);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: TSeriesEnumerator struct
// ────────────────────────────────────────────────────────────────────
[Fact]
public void TSeriesEnumerator_Reset_AllowsReIteration()
{
var series = new TSeries();
series.Add(100, 1.0);
series.Add(200, 2.0);
var enumerator = series.GetEnumerator();
// First pass
int count1 = 0;
while (enumerator.MoveNext()) { count1++; }
Assert.Equal(2, count1);
// Reset and re-iterate
enumerator.Reset();
int count2 = 0;
while (enumerator.MoveNext()) { count2++; }
Assert.Equal(2, count2);
}
[Fact]
public void TSeriesEnumerator_Dispose_DoesNotThrow()
{
var series = new TSeries();
series.Add(100, 1.0);
var enumerator = series.GetEnumerator();
enumerator.MoveNext();
enumerator.Dispose(); // Should be no-op
Assert.Equal(1.0, enumerator.Current.Value);
}
[Fact]
public void TSeriesEnumerator_Equals_SameState_ReturnsTrue()
{
var series = new TSeries();
series.Add(100, 1.0);
var a = series.GetEnumerator();
var b = series.GetEnumerator();
// Both at initial state (_index = -1)
Assert.True(a.Equals(b));
Assert.True(a == b);
Assert.False(a != b);
}
[Fact]
public void TSeriesEnumerator_Equals_DifferentState_ReturnsFalse()
{
var series = new TSeries();
series.Add(100, 1.0);
series.Add(200, 2.0);
var a = series.GetEnumerator();
var b = series.GetEnumerator();
a.MoveNext(); // a is at index 0, b is at -1
Assert.False(a.Equals(b));
Assert.False(a == b);
Assert.True(a != b);
}
[Fact]
public void TSeriesEnumerator_Equals_Object_SameState_ReturnsTrue()
{
var series = new TSeries();
series.Add(100, 1.0);
var a = series.GetEnumerator();
object b = series.GetEnumerator();
Assert.True(a.Equals(b));
}
[Fact]
public void TSeriesEnumerator_Equals_Object_DifferentType_ReturnsFalse()
{
var series = new TSeries();
series.Add(100, 1.0);
var a = series.GetEnumerator();
Assert.False(a.Equals("not an enumerator"));
Assert.False(a.Equals(null));
}
[Fact]
public void TSeriesEnumerator_GetHashCode_SameState_SameHash()
{
var series = new TSeries();
series.Add(100, 1.0);
var a = series.GetEnumerator();
var b = series.GetEnumerator();
Assert.Equal(a.GetHashCode(), b.GetHashCode());
}
[Fact]
public void TSeriesEnumerator_GetHashCode_DifferentState_LikelyDifferentHash()
{
var series = new TSeries();
series.Add(100, 1.0);
series.Add(200, 2.0);
var a = series.GetEnumerator();
var b = series.GetEnumerator();
a.MoveNext();
Assert.NotEqual(a.GetHashCode(), b.GetHashCode());
}
[Fact]
public void TSeriesEnumerator_Current_ViaIEnumerator_ReturnsBoxedTValue()
{
var series = new TSeries();
series.Add(100, 42.0);
IEnumerator enumerator = series.GetEnumerator();
enumerator.MoveNext();
object current = enumerator.Current;
Assert.IsType<TValue>(current);
Assert.Equal(42.0, ((TValue)current).Value);
}
[Fact]
public void TSeriesEnumerator_DifferentSeries_NotEqual()
{
var series1 = new TSeries();
series1.Add(100, 1.0);
var series2 = new TSeries();
series2.Add(100, 1.0);
var a = series1.GetEnumerator();
var b = series2.GetEnumerator();
// Different underlying list references -> not equal
Assert.False(a.Equals(b));
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: Constructor defensive copy verification
// ────────────────────────────────────────────────────────────────────
[Fact]
public void Constructor_WithReadOnlyLists_MakesDefensiveCopy()
{
var times = new List<long> { 100, 200 };
var values = new List<double> { 1.0, 2.0 };
// Use IReadOnlyList<T> constructor which makes defensive copies
var series = new TSeries((IReadOnlyList<long>)times, (IReadOnlyList<double>)values);
// Mutate original lists
times.Add(300);
values.Add(3.0);
// Series should NOT reflect the mutation (defensive copy was made)
Assert.Equal(2, series.Count);
}
[Fact]
public void Constructor_WithReadOnlyLists_OriginalMutationDoesNotAffectSeries()
{
long[] originalTimes = [100, 200, 300];
double[] originalValues = [1.0, 2.0, 3.0];
var series = new TSeries(originalTimes, originalValues);
// Mutate original arrays
originalValues[0] = 999.0;
// Series should NOT reflect the mutation (defensive copy was made)
Assert.Equal(1.0, series[0].Value);
}
}
+499 -1
View File
@@ -303,7 +303,7 @@ public class TValueTests
string result = tValue.ToString(null, CultureInfo.InvariantCulture);
Assert.Contains("", result, StringComparison.Ordinal);
Assert.Contains("\u221E", result, StringComparison.Ordinal);
}
[Fact]
@@ -373,4 +373,502 @@ public class TValueTests
Assert.Equal(long.MaxValue, tValue.Time);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: ToString() parameterless override
// ────────────────────────────────────────────────────────────────────
[Fact]
public void ToString_Parameterless_NormalValue_FormatsCorrectly()
{
var dt = new DateTime(2023, 6, 15, 14, 30, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, 42.0);
#pragma warning disable MA0011 // IFormatProvider is missing - intentionally testing parameterless overload
string result = tValue.ToString();
#pragma warning restore MA0011
Assert.StartsWith("[", result, StringComparison.Ordinal);
Assert.EndsWith("]", result, StringComparison.Ordinal);
Assert.Contains("2023-06-15", result, StringComparison.Ordinal);
Assert.Contains("14:30:00", result, StringComparison.Ordinal);
Assert.Contains("42.00", result, StringComparison.Ordinal);
}
[Fact]
public void ToString_Parameterless_PositiveInfinity_ContainsInfinitySymbol()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, double.PositiveInfinity);
#pragma warning disable MA0011
string result = tValue.ToString();
#pragma warning restore MA0011
Assert.Contains("\u221E", result, StringComparison.Ordinal);
}
[Fact]
public void ToString_Parameterless_NegativeInfinity_ContainsMinusInfinitySymbol()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, double.NegativeInfinity);
#pragma warning disable MA0011
string result = tValue.ToString();
#pragma warning restore MA0011
Assert.Contains("-\u221E", result, StringComparison.Ordinal);
}
[Fact]
public void ToString_Parameterless_NaN_ContainsNaN()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, double.NaN);
#pragma warning disable MA0011
string result = tValue.ToString();
#pragma warning restore MA0011
Assert.Contains("NaN", result, StringComparison.Ordinal);
}
[Fact]
public void ToString_Parameterless_ZeroValue_FormatsAsZero()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, 0.0);
#pragma warning disable MA0011
string result = tValue.ToString();
#pragma warning restore MA0011
Assert.Contains("0.00", result, StringComparison.Ordinal);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: ToString(string?, IFormatProvider?) — NotSupportedException
// ────────────────────────────────────────────────────────────────────
[Fact]
public void ToString_WithCustomFormat_ThrowsNotSupportedException()
{
var tValue = new TValue(DateTime.UtcNow.Ticks, 42.0);
Assert.Throws<NotSupportedException>(() => tValue.ToString("F4", CultureInfo.InvariantCulture));
}
[Fact]
public void ToString_WithNonEmptyFormat_ThrowsNotSupportedException()
{
var tValue = new TValue(DateTime.UtcNow.Ticks, 42.0);
var ex = Assert.Throws<NotSupportedException>(() => tValue.ToString("G", null));
Assert.Contains("Custom format", ex.Message, StringComparison.Ordinal);
Assert.Contains("'G'", ex.Message, StringComparison.Ordinal);
}
[Fact]
public void ToString_WithNullFormat_DoesNotThrow()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, 42.0);
string result = tValue.ToString(null, null);
Assert.Contains("42.00", result, StringComparison.Ordinal);
}
[Fact]
public void ToString_WithEmptyFormat_DoesNotThrow()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, 42.0);
string result = tValue.ToString("", null);
Assert.Contains("42.00", result, StringComparison.Ordinal);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: TryFormat — allocation-free span formatting
// ────────────────────────────────────────────────────────────────────
[Fact]
public void TryFormat_NormalValue_FormatsCorrectly()
{
var dt = new DateTime(2023, 6, 15, 14, 30, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, 42.0);
Span<char> buffer = stackalloc char[128];
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, CultureInfo.InvariantCulture);
Assert.True(result);
Assert.True(charsWritten > 0);
string formatted = new string(buffer.Slice(0, charsWritten));
Assert.StartsWith("[", formatted, StringComparison.Ordinal);
Assert.EndsWith("]", formatted, StringComparison.Ordinal);
Assert.Contains("2023-06-15", formatted, StringComparison.Ordinal);
Assert.Contains("14:30:00", formatted, StringComparison.Ordinal);
Assert.Contains("42.00", formatted, StringComparison.Ordinal);
}
[Fact]
public void TryFormat_PositiveInfinity_FormatsWithSymbol()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, double.PositiveInfinity);
Span<char> buffer = stackalloc char[128];
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, null);
Assert.True(result);
string formatted = new string(buffer.Slice(0, charsWritten));
Assert.Contains("\u221E", formatted, StringComparison.Ordinal);
Assert.StartsWith("[", formatted, StringComparison.Ordinal);
Assert.EndsWith("]", formatted, StringComparison.Ordinal);
}
[Fact]
public void TryFormat_NegativeInfinity_FormatsWithMinusSymbol()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, double.NegativeInfinity);
Span<char> buffer = stackalloc char[128];
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, null);
Assert.True(result);
string formatted = new string(buffer.Slice(0, charsWritten));
Assert.Contains("-\u221E", formatted, StringComparison.Ordinal);
}
[Fact]
public void TryFormat_NaN_FormatsAsNaN()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, double.NaN);
Span<char> buffer = stackalloc char[128];
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, null);
Assert.True(result);
string formatted = new string(buffer.Slice(0, charsWritten));
Assert.Contains("NaN", formatted, StringComparison.Ordinal);
}
[Fact]
public void TryFormat_BufferTooSmall_ReturnsFalse()
{
var tValue = new TValue(DateTime.UtcNow.Ticks, 42.0);
Span<char> buffer = stackalloc char[10]; // Way too small (< 24)
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, null);
Assert.False(result);
Assert.Equal(0, charsWritten);
}
[Fact]
public void TryFormat_BufferExactlyTooSmall_ReturnsFalse()
{
var tValue = new TValue(DateTime.UtcNow.Ticks, 42.0);
Span<char> buffer = stackalloc char[23]; // One less than minimum (24)
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, null);
Assert.False(result);
Assert.Equal(0, charsWritten);
}
[Fact]
public void TryFormat_BufferBarelyLargeEnough_ForShortValue()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
// NaN is shortest value format (3 chars): "[yyyy-MM-dd HH:mm:ss, NaN]" = 26 chars
var tValue = new TValue(dt.Ticks, double.NaN);
Span<char> buffer = stackalloc char[26];
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, null);
Assert.True(result);
Assert.Equal(26, charsWritten);
}
[Fact]
public void TryFormat_BufferTooSmallForSeparator_ReturnsFalse()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, 42.0);
// Buffer: 1 (opening bracket) + 19 (datetime) + 1 = 21 (too small for ", ")
Span<char> buffer = stackalloc char[21];
// This should fail because the initial < 24 check triggers first
bool result = tValue.TryFormat(buffer, out _, ReadOnlySpan<char>.Empty, null);
Assert.False(result);
}
[Fact]
public void TryFormat_BufferTooSmallForClosingBracket_ReturnsFalse()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
// ∞ is 1 char: "[yyyy-MM-dd HH:mm:ss, ∞]" = 24 chars
var tValue = new TValue(dt.Ticks, double.PositiveInfinity);
// Need exactly 24 chars: 1 + 19 + 2 + 1 + 1 = 24
// Providing 23 — enough to pass the initial check but too small for final ']'
// Actually the initial check is < 24, so 23 fails there.
// Let's use 24 which is exactly enough for ∞ case
Span<char> buffer = stackalloc char[24];
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, null);
Assert.True(result);
Assert.Equal(24, charsWritten);
}
[Fact]
public void TryFormat_LargeNegativeValue_FormatsCorrectly()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, -99999.99);
Span<char> buffer = stackalloc char[128];
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, CultureInfo.InvariantCulture);
Assert.True(result);
string formatted = new string(buffer.Slice(0, charsWritten));
Assert.Contains("-99999.99", formatted, StringComparison.Ordinal);
}
[Fact]
public void TryFormat_ZeroValue_FormatsAsZero()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, 0.0);
Span<char> buffer = stackalloc char[128];
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, CultureInfo.InvariantCulture);
Assert.True(result);
string formatted = new string(buffer.Slice(0, charsWritten));
Assert.Contains("0.00", formatted, StringComparison.Ordinal);
}
[Fact]
public void TryFormat_ConsistentWithToString()
{
var dt = new DateTime(2023, 6, 15, 14, 30, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, 123.456);
#pragma warning disable MA0011
string fromToString = tValue.ToString();
#pragma warning restore MA0011
Span<char> buffer = stackalloc char[128];
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, null);
Assert.True(result);
string fromTryFormat = new string(buffer.Slice(0, charsWritten));
Assert.Equal(fromToString, fromTryFormat);
}
[Fact]
public void TryFormat_NaN_ConsistentWithToString()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, double.NaN);
#pragma warning disable MA0011
string fromToString = tValue.ToString();
#pragma warning restore MA0011
Span<char> buffer = stackalloc char[128];
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, null);
Assert.True(result);
string fromTryFormat = new string(buffer.Slice(0, charsWritten));
Assert.Equal(fromToString, fromTryFormat);
}
[Fact]
public void TryFormat_PositiveInfinity_ConsistentWithToString()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, double.PositiveInfinity);
#pragma warning disable MA0011
string fromToString = tValue.ToString();
#pragma warning restore MA0011
Span<char> buffer = stackalloc char[128];
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, null);
Assert.True(result);
string fromTryFormat = new string(buffer.Slice(0, charsWritten));
Assert.Equal(fromToString, fromTryFormat);
}
[Fact]
public void TryFormat_NegativeInfinity_ConsistentWithToString()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, double.NegativeInfinity);
#pragma warning disable MA0011
string fromToString = tValue.ToString();
#pragma warning restore MA0011
Span<char> buffer = stackalloc char[128];
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, null);
Assert.True(result);
string fromTryFormat = new string(buffer.Slice(0, charsWritten));
Assert.Equal(fromToString, fromTryFormat);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: Record struct features (with expression, Deconstruct)
// ────────────────────────────────────────────────────────────────────
[Fact]
public void WithExpression_CreatesModifiedCopy()
{
var original = new TValue(12345, 100.0);
var modified = original with { Value = 200.0 };
Assert.Equal(12345, modified.Time);
Assert.Equal(200.0, modified.Value);
Assert.Equal(100.0, original.Value); // Original unchanged
}
[Fact]
public void WithExpression_TimeModified_CreatesModifiedCopy()
{
var original = new TValue(12345, 100.0);
var modified = original with { Time = 99999 };
Assert.Equal(99999, modified.Time);
Assert.Equal(100.0, modified.Value);
}
[Fact]
public void Deconstruct_ExtractsTimeAndValue()
{
var tValue = new TValue(12345, 42.0);
var (time, value) = tValue;
Assert.Equal(12345, time);
Assert.Equal(42.0, value);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: ISpanFormattable interface contract
// ────────────────────────────────────────────────────────────────────
[Fact]
public void ISpanFormattable_ImplementedCorrectly()
{
var tValue = new TValue(DateTime.UtcNow.Ticks, 42.0);
// Verify TValue implements ISpanFormattable
Assert.IsAssignableFrom<ISpanFormattable>(tValue);
}
[Fact]
public void IFormattable_ImplementedCorrectly()
{
var tValue = new TValue(DateTime.UtcNow.Ticks, 42.0);
// ISpanFormattable extends IFormattable
Assert.IsAssignableFrom<IFormattable>(tValue);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: ToString with negative infinity (was missing)
// ────────────────────────────────────────────────────────────────────
[Fact]
public void ToString_WithNegativeInfinity_FormatsCorrectly()
{
var dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc);
var tValue = new TValue(dt.Ticks, double.NegativeInfinity);
string result = tValue.ToString(null, CultureInfo.InvariantCulture);
Assert.Contains("-\u221E", result, StringComparison.Ordinal);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: Explicit double conversion edge cases
// ────────────────────────────────────────────────────────────────────
[Fact]
public void ExplicitConversion_ToDouble_WithPositiveInfinity_ReturnsInfinity()
{
var tValue = new TValue(DateTime.UtcNow.Ticks, double.PositiveInfinity);
double val = (double)tValue;
Assert.True(double.IsPositiveInfinity(val));
}
[Fact]
public void ExplicitConversion_ToDouble_WithNegativeInfinity_ReturnsNegativeInfinity()
{
var tValue = new TValue(DateTime.UtcNow.Ticks, double.NegativeInfinity);
double val = (double)tValue;
Assert.True(double.IsNegativeInfinity(val));
}
[Fact]
public void ExplicitConversion_ToDouble_WithZero_ReturnsZero()
{
var tValue = new TValue(DateTime.UtcNow.Ticks, 0.0);
double val = (double)tValue;
Assert.Equal(0.0, val);
}
// ────────────────────────────────────────────────────────────────────
// NEW TESTS: TryFormat buffer edge cases for -∞ and NaN
// ────────────────────────────────────────────────────────────────────
[Fact]
public void TryFormat_NegativeInfinity_ExactBuffer()
{
var dt = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
// "-∞" is 2 chars: "[yyyy-MM-dd HH:mm:ss, -∞]" = 25 chars
var tValue = new TValue(dt.Ticks, double.NegativeInfinity);
Span<char> buffer = stackalloc char[25];
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, null);
Assert.True(result);
Assert.Equal(25, charsWritten);
}
[Fact]
public void TryFormat_EmptyBuffer_ReturnsFalse()
{
var tValue = new TValue(DateTime.UtcNow.Ticks, 42.0);
Span<char> buffer = Span<char>.Empty;
bool result = tValue.TryFormat(buffer, out int charsWritten, ReadOnlySpan<char>.Empty, null);
Assert.False(result);
Assert.Equal(0, charsWritten);
}
}
@@ -0,0 +1,357 @@
using TradingPlatform.BusinessLayer;
namespace QuanTAlib.Tests;
/// <summary>
/// Tests for HtDcperiodIndicator Quantower adapter.
/// Covers: constructor, properties (Source, ShowColdValues, ShortName, MinHistoryDepths),
/// OnInit, OnUpdate (HistoricalBar, NewBar, NewTick filtered), multiple bars,
/// ShowColdValues false, reinitialize, source variants.
/// </summary>
public class HtDcperiodIndicatorTests
{
// ═══════════════════════════════ Constructor ═══════════════════════════════
[Fact]
public void Constructor_InitializesName()
{
var indicator = new HtDcperiodIndicator();
Assert.Contains("HT_DCPERIOD", indicator.Name, StringComparison.Ordinal);
}
[Fact]
public void Constructor_InitializesDescription()
{
var indicator = new HtDcperiodIndicator();
Assert.False(string.IsNullOrEmpty(indicator.Description));
Assert.Contains("Hilbert", indicator.Description, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void Constructor_SeparateWindowTrue()
{
var indicator = new HtDcperiodIndicator();
Assert.True(indicator.SeparateWindow);
}
[Fact]
public void Constructor_HasLineSeries()
{
var indicator = new HtDcperiodIndicator();
Assert.True(indicator.LinesSeries.Count >= 1);
}
// ═══════════════════════════════ Properties ═══════════════════════════════
[Fact]
public void Source_DefaultsToClose()
{
var indicator = new HtDcperiodIndicator();
Assert.Equal(SourceType.Close, indicator.Source);
}
[Fact]
public void ShowColdValues_DefaultsToTrue()
{
var indicator = new HtDcperiodIndicator();
Assert.True(indicator.ShowColdValues);
}
[Fact]
public void ShortName_IsHtDcperiod()
{
var indicator = new HtDcperiodIndicator();
Assert.Equal("HT_DCPERIOD", indicator.ShortName);
}
[Fact]
public void MinHistoryDepths_Static_Is32()
{
Assert.Equal(32, HtDcperiodIndicator.MinHistoryDepths);
}
[Fact]
public void MinHistoryDepths_Interface_Is32()
{
IWatchlistIndicator indicator = new HtDcperiodIndicator();
Assert.Equal(32, indicator.MinHistoryDepths);
}
[Fact]
public void SourceCodeLink_IsNotEmpty()
{
var indicator = new HtDcperiodIndicator();
Assert.False(string.IsNullOrEmpty(indicator.SourceCodeLink));
Assert.Contains("HtDcperiod", indicator.SourceCodeLink, StringComparison.Ordinal);
}
// ═══════════════════════════════ OnInit ═══════════════════════════════════
[Fact]
public void OnInit_CreatesInternalIndicator()
{
var indicator = new HtDcperiodIndicator();
indicator.Initialize();
// Should not throw - internal indicator created successfully
Assert.True(true);
}
// ═══════════════════════════════ OnUpdate ═════════════════════════════════
[Fact]
public void OnUpdate_HistoricalBar_Processes()
{
var indicator = new HtDcperiodIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 40; i++)
{
indicator.HistoricalData.AddBar(
time: now.AddMinutes(i),
open: 100 + i,
high: 105 + i,
low: 95 + i,
close: 102 + i,
volume: 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
Assert.Equal(40, indicator.LinesSeries[0].Count);
}
[Fact]
public void OnUpdate_NewBar_Processes()
{
var indicator = new HtDcperiodIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
// Feed historical bars first
for (int i = 0; i < 35; i++)
{
indicator.HistoricalData.AddBar(
time: now.AddMinutes(i),
open: 100 + i,
high: 105 + i,
low: 95 + i,
close: 102 + i,
volume: 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
// Then process a new bar
indicator.HistoricalData.AddBar(
time: now.AddMinutes(35),
open: 135, high: 140, low: 130, close: 137, volume: 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
Assert.Equal(36, indicator.LinesSeries[0].Count);
}
[Fact]
public void OnUpdate_NewTick_DoesNotThrow()
{
var indicator = new HtDcperiodIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
// NewTick should be filtered (early return) - no exception
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
Assert.True(true);
}
// ═══════════════════════════════ Multiple Bars ════════════════════════════
[Fact]
public void OnUpdate_MultipleBars_ProducesFiniteValues()
{
var indicator = new HtDcperiodIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
// HT_DCPERIOD needs significant warmup - feed sinusoidal data
for (int i = 0; i < 100; i++)
{
double price = 100 + 10 * Math.Sin(i * 0.3);
indicator.HistoricalData.AddBar(
time: now.AddMinutes(i),
open: price - 1,
high: price + 2,
low: price - 2,
close: price,
volume: 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
Assert.Equal(100, indicator.LinesSeries[0].Count);
// After warmup, values should be finite
double lastValue = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(lastValue));
}
[Fact]
public void OnUpdate_SingleBar_ProducesValue()
{
var indicator = new HtDcperiodIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
Assert.Equal(1, indicator.LinesSeries[0].Count);
}
// ═══════════════════════════════ ShowColdValues ═══════════════════════════
[Fact]
public void ShowColdValues_CanBeSetFalse()
{
var indicator = new HtDcperiodIndicator();
indicator.ShowColdValues = false;
Assert.False(indicator.ShowColdValues);
}
[Fact]
public void ShowColdValues_False_ProcessesWithoutError()
{
var indicator = new HtDcperiodIndicator();
indicator.ShowColdValues = false;
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 40; i++)
{
indicator.HistoricalData.AddBar(
time: now.AddMinutes(i),
open: 100 + i, high: 105 + i, low: 95 + i,
close: 102 + i, volume: 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
Assert.True(true);
}
// ═══════════════════════════════ Reinitialize ═════════════════════════════
[Fact]
public void Reinitialize_ResetsState()
{
var indicator = new HtDcperiodIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 20; i++)
{
indicator.HistoricalData.AddBar(
time: now.AddMinutes(i),
open: 100 + i, high: 105 + i, low: 95 + i, close: 102 + i, volume: 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
// Reinitialize
indicator.Initialize();
// Should process fresh data without error
for (int i = 0; i < 20; i++)
{
indicator.HistoricalData.AddBar(
time: now.AddMinutes(100 + i),
open: 200 + i, high: 205 + i, low: 195 + i, close: 202 + i, volume: 2000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
Assert.True(true);
}
// ═══════════════════════════════ Source Variants ══════════════════════════
[Fact]
public void Source_SetToOpen_Accepted()
{
var indicator = new HtDcperiodIndicator();
indicator.Source = SourceType.Open;
Assert.Equal(SourceType.Open, indicator.Source);
}
[Fact]
public void Source_SetToHigh_Accepted()
{
var indicator = new HtDcperiodIndicator();
indicator.Source = SourceType.High;
Assert.Equal(SourceType.High, indicator.Source);
}
[Fact]
public void Source_SetToLow_Accepted()
{
var indicator = new HtDcperiodIndicator();
indicator.Source = SourceType.Low;
Assert.Equal(SourceType.Low, indicator.Source);
}
[Fact]
public void Source_DifferentSources_ProcessWithoutError()
{
foreach (var source in new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close })
{
var indicator = new HtDcperiodIndicator();
indicator.Source = source;
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 40; i++)
{
indicator.HistoricalData.AddBar(
time: now.AddMinutes(i),
open: 100 + i, high: 105 + i, low: 95 + i,
close: 102 + i, volume: 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
}
Assert.True(true);
}
// ═══════════════════════════════ OnBackGround ═════════════════════════════
[Fact]
public void OnBackGround_IsTrue()
{
var indicator = new HtDcperiodIndicator();
Assert.True(indicator.OnBackGround);
}
// ═══════════════════════════════ Value Assertions ═════════════════════════
[Fact]
public void Values_AfterWarmup_ArePositive()
{
var indicator = new HtDcperiodIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
// Feed sinusoidal data with known period (~21 bars)
for (int i = 0; i < 100; i++)
{
double price = 100 + 10 * Math.Sin(2 * Math.PI * i / 21.0);
indicator.HistoricalData.AddBar(
time: now.AddMinutes(i),
open: price - 0.5,
high: price + 1,
low: price - 1,
close: price,
volume: 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
// Dominant cycle period should be positive after warmup
double lastValue = indicator.LinesSeries[0].GetValue(0);
Assert.True(lastValue > 0, $"Expected positive period, got {lastValue}");
}
}
+432
View File
@@ -6,6 +6,8 @@ namespace QuanTAlib.Tests.Cycles;
public class HtDcperiodTests
{
// ── Constructor ──────────────────────────────────────────────────────
[Fact]
public void Constructor_SetsDefaults()
{
@@ -15,6 +17,38 @@ public class HtDcperiodTests
Assert.False(ht.IsHot);
}
[Fact]
public void Constructor_WithPublisher_SubscribesToEvents()
{
var source = new TSeries();
var ht = new HtDcperiod(source);
Assert.False(ht.IsHot);
// Feed data through publisher
for (int i = 0; i < 40; i++)
{
source.Add(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + Math.Sin(i * 0.3) * 10));
}
Assert.True(ht.IsHot);
Assert.True(double.IsFinite(ht.Last.Value));
}
[Fact]
public void Constructor_WithNullPublisher_Throws()
{
Assert.Throws<ArgumentNullException>(() => new HtDcperiod(null!));
}
[Fact]
public void Last_DefaultBeforeAnyUpdate()
{
var ht = new HtDcperiod();
Assert.Equal(default, ht.Last);
}
// ── IsHot & Warmup ──────────────────────────────────────────────────
[Fact]
public void Update_BecomesHotAfterWarmup()
{
@@ -31,6 +65,171 @@ public class HtDcperiodTests
Assert.True(double.IsFinite(ht.Last.Value));
}
[Fact]
public void IsHot_FalseBeforeWarmup()
{
var ht = new HtDcperiod();
for (int i = 0; i < 30; i++)
{
ht.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i));
}
Assert.False(ht.IsHot);
}
[Fact]
public void WarmupPeriod_Returns32()
{
var ht = new HtDcperiod();
Assert.Equal(32, ht.WarmupPeriod);
}
// ── Update (streaming) ──────────────────────────────────────────────
[Fact]
public void Update_FirstBarsReturnZero()
{
var ht = new HtDcperiod();
// During WMA initialization (first ~37 bars), output should be 0
var result = ht.Update(new TValue(DateTime.UtcNow, 100.0));
Assert.Equal(0.0, result.Value);
}
[Fact]
public void Update_ProducesFiniteValuesAfterWarmup()
{
var ht = new HtDcperiod();
var gbm = new GBM(seed: 99);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
TValue lastResult = default;
foreach (var bar in bars)
{
lastResult = ht.Update(new TValue(bar.Time, bar.Close));
}
Assert.True(double.IsFinite(lastResult.Value));
}
[Fact]
public void Update_PeriodInValidRange()
{
// The dominant cycle period should be clamped between 6 and 50
var ht = new HtDcperiod();
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
bool anyHot = false;
foreach (var bar in bars)
{
var result = ht.Update(new TValue(bar.Time, bar.Close));
if (ht.IsHot)
{
anyHot = true;
// Period output should be in a reasonable range
Assert.True(double.IsFinite(result.Value),
$"Period should be finite, got {result.Value}");
}
}
Assert.True(anyHot);
}
// ── Bar Correction (isNew=false) ────────────────────────────────────
[Fact]
public void SameBarUpdate_ReturnsSameValue()
{
var ht = new HtDcperiod();
var now = DateTime.UtcNow;
// Prime with data
for (int i = 0; i < 50; i++)
{
ht.Update(new TValue(now.AddMinutes(i), 100 + Math.Sin(i * 0.1) * 10));
}
Assert.True(ht.IsHot);
// First update (new bar)
var result1 = ht.Update(new TValue(now.AddMinutes(50), 105), isNew: true);
// Same bar update with different price
var result2 = ht.Update(new TValue(now.AddMinutes(50), 106), isNew: false);
// isNew=false should rollback and reapply - result should equal result1 since
// bar correction restores previous state first
Assert.Equal(result1.Value, result2.Value);
}
[Fact]
public void BarCorrection_DoesNotAdvanceState()
{
var ht = new HtDcperiod();
var now = DateTime.UtcNow;
for (int i = 0; i < 50; i++)
{
ht.Update(new TValue(now.AddMinutes(i), 100 + i));
}
// New bar
ht.Update(new TValue(now.AddMinutes(50), 150), isNew: true);
var afterNew = ht.Last;
// Multiple corrections should not change the state relative to the new bar
ht.Update(new TValue(now.AddMinutes(50), 151), isNew: false);
ht.Update(new TValue(now.AddMinutes(50), 152), isNew: false);
ht.Update(new TValue(now.AddMinutes(50), 150), isNew: false);
var afterCorrections = ht.Last;
Assert.Equal(afterNew.Value, afterCorrections.Value);
}
// ── NaN handling ────────────────────────────────────────────────────
[Fact]
public void Update_NaN_BeforeAnyValidData_ReturnsZero()
{
var ht = new HtDcperiod();
var result = ht.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.Equal(0.0, result.Value);
}
[Fact]
public void Update_NaN_UsesLastValidPrice()
{
var ht = new HtDcperiod();
var now = DateTime.UtcNow;
// Feed valid data to warm up
for (int i = 0; i < 50; i++)
{
ht.Update(new TValue(now.AddMinutes(i), 100 + Math.Sin(i * 0.2) * 5));
}
Assert.True(ht.IsHot);
// Feed NaN - should use last valid price
var result = ht.Update(new TValue(now.AddMinutes(50), double.NaN));
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Update_Infinity_UsesLastValidPrice()
{
var ht = new HtDcperiod();
var now = DateTime.UtcNow;
for (int i = 0; i < 50; i++)
{
ht.Update(new TValue(now.AddMinutes(i), 100 + i * 0.5));
}
var result = ht.Update(new TValue(now.AddMinutes(50), double.PositiveInfinity));
Assert.True(double.IsFinite(result.Value));
}
// ── Reset ───────────────────────────────────────────────────────────
[Fact]
public void Reset_ClearsState()
{
@@ -46,4 +245,237 @@ public class HtDcperiodTests
Assert.False(ht.IsHot);
Assert.Equal(default, ht.Last);
}
[Fact]
public void Reset_AllowsReuse()
{
var ht = new HtDcperiod();
var now = DateTime.UtcNow;
// First use
for (int i = 0; i < 50; i++)
{
ht.Update(new TValue(now.AddMinutes(i), 100 + Math.Sin(i * 0.2) * 5));
}
Assert.True(ht.IsHot);
var firstResult = ht.Last.Value;
// Reset and reuse
ht.Reset();
Assert.False(ht.IsHot);
for (int i = 0; i < 50; i++)
{
ht.Update(new TValue(now.AddMinutes(i), 100 + Math.Sin(i * 0.2) * 5));
}
Assert.True(ht.IsHot);
Assert.Equal(firstResult, ht.Last.Value);
}
// ── Batch/TSeries Update ────────────────────────────────────────────
[Fact]
public void Update_TSeries_ReturnsCorrectCount()
{
var ht = new HtDcperiod();
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var result = ht.Update(bars.Close);
Assert.Equal(100, result.Count);
}
[Fact]
public void Update_EmptyTSeries_ReturnsEmpty()
{
var ht = new HtDcperiod();
var result = ht.Update(new TSeries());
Assert.Empty(result);
}
[Fact]
public void Batch_TSeries_MatchesStreaming()
{
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
// Batch
var batchResult = HtDcperiod.Batch(series);
// Streaming
var streaming = new HtDcperiod();
var streamingResults = new TSeries();
foreach (var item in series)
{
streamingResults.Add(streaming.Update(item));
}
// Compare
Assert.Equal(batchResult.Count, streamingResults.Count);
for (int i = 0; i < batchResult.Count; i++)
{
Assert.Equal(batchResult[i].Value, streamingResults[i].Value, 1e-10);
}
}
[Fact]
public void Batch_Span_MatchesStreaming()
{
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var values = bars.Close.Values.ToArray();
// Span batch
var spanOutput = new double[values.Length];
HtDcperiod.Batch(values, spanOutput);
// Streaming
var streaming = new HtDcperiod();
var streamingResults = new double[values.Length];
for (int i = 0; i < values.Length; i++)
{
streamingResults[i] = streaming.Update(new TValue(DateTime.UtcNow.AddTicks(i), values[i])).Value;
}
// Compare
for (int i = 0; i < values.Length; i++)
{
Assert.Equal(streamingResults[i], spanOutput[i], 1e-10);
}
}
[Fact]
public void Batch_Span_OutputTooShort_Throws()
{
var source = new double[10];
var output = new double[5];
Assert.Throws<ArgumentException>(() => HtDcperiod.Batch(source, output));
}
// ── Calculate ───────────────────────────────────────────────────────
[Fact]
public void Calculate_ReturnsBothResultsAndIndicator()
{
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var (results, indicator) = HtDcperiod.Calculate(bars.Close);
Assert.Equal(100, results.Count);
Assert.True(indicator.IsHot);
}
// ── Prime ───────────────────────────────────────────────────────────
[Fact]
public void Prime_WamsUpIndicator()
{
var ht = new HtDcperiod();
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(80, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var values = bars.Close.Values.ToArray();
ht.Prime(values);
Assert.True(ht.IsHot);
}
[Fact]
public void Prime_WithStepParameter()
{
var ht = new HtDcperiod();
var values = new double[50];
for (int i = 0; i < 50; i++)
{
values[i] = 100 + Math.Sin(i * 0.2) * 5;
}
ht.Prime(values, TimeSpan.FromMinutes(5));
Assert.True(ht.IsHot);
}
// ── Determinism ─────────────────────────────────────────────────────
[Fact]
public void TwoInstances_SameInput_SameOutput()
{
var ht1 = new HtDcperiod();
var ht2 = new HtDcperiod();
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
foreach (var bar in bars)
{
var tv = new TValue(bar.Time, bar.Close);
var r1 = ht1.Update(tv);
var r2 = ht2.Update(tv);
Assert.Equal(r1.Value, r2.Value);
}
}
// ── Constant price ──────────────────────────────────────────────────
[Fact]
public void ConstantPrice_ProducesFiniteOutput()
{
var ht = new HtDcperiod();
var now = DateTime.UtcNow;
for (int i = 0; i < 80; i++)
{
var result = ht.Update(new TValue(now.AddMinutes(i), 100.0));
Assert.True(double.IsFinite(result.Value),
$"Bar {i}: Expected finite, got {result.Value}");
}
}
// ── Sinusoidal input ────────────────────────────────────────────────
[Fact]
public void SinusoidalInput_DetectsApproximatePeriod()
{
var ht = new HtDcperiod();
var now = DateTime.UtcNow;
// Feed a clean sinusoidal with period ~20 bars
int inputPeriod = 20;
double omega = 2.0 * Math.PI / inputPeriod;
for (int i = 0; i < 300; i++)
{
ht.Update(new TValue(now.AddMinutes(i), 100 + 10 * Math.Sin(omega * i)));
}
// After sufficient data, the detected period should be
// somewhere in the ballpark of the input period
Assert.True(ht.IsHot);
double detected = ht.Last.Value;
Assert.True(double.IsFinite(detected));
// The Hilbert transform period detection is approximate
Assert.True(detected >= 6.0 && detected <= 50.0,
$"Detected period {detected} should be in [6, 50] range");
}
// ── Dispose ─────────────────────────────────────────────────────────
[Fact]
public void Dispose_DoesNotThrow()
{
var ht = new HtDcperiod();
ht.Update(new TValue(DateTime.UtcNow, 100));
ht.Dispose();
Assert.True(true); // S2699: explicit assertion for dispose-only test
}
[Fact]
public void Dispose_WithPublisher_DoesNotThrow()
{
// HtDcperiod subscribes to source but does not track the source
// reference for unsubscription — Dispose still must not throw
var source = new TSeries();
var ht = new HtDcperiod(source);
source.Add(new TValue(DateTime.UtcNow, 100));
Assert.True(double.IsFinite(ht.Last.Value) || ht.Last.Value == 0.0);
ht.Dispose();
}
}
+325 -8
View File
@@ -6,6 +6,8 @@ namespace QuanTAlib.Tests.Cycles;
public class HtDcphaseTests
{
// ── Constructor ──────────────────────────────────────────────────────
[Fact]
public void Constructor_SetsDefaults()
{
@@ -15,6 +17,38 @@ public class HtDcphaseTests
Assert.False(ht.IsHot);
}
[Fact]
public void Constructor_WithPublisher_Subscribes()
{
var source = new TSeries();
var ht = new HtDcphase(source);
Assert.False(ht.IsHot);
// Feed data through publisher
for (int i = 0; i < 80; i++)
{
source.Add(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + Math.Sin(i * 0.3) * 10));
}
Assert.True(ht.IsHot);
Assert.True(double.IsFinite(ht.Last.Value));
}
[Fact]
public void Constructor_NullPublisher_Throws()
{
Assert.Throws<ArgumentNullException>(() => new HtDcphase(null!));
}
[Fact]
public void Last_DefaultBeforeAnyUpdate()
{
var ht = new HtDcphase();
Assert.Equal(default, ht.Last);
}
// ── IsHot & Warmup ──────────────────────────────────────────────────
[Fact]
public void Update_BecomesHotAfterWarmup()
{
@@ -32,19 +66,24 @@ public class HtDcphaseTests
}
[Fact]
public void Reset_ClearsState()
public void IsHot_FalseBeforeWarmup()
{
var ht = new HtDcphase();
var now = DateTime.UtcNow;
for (int i = 0; i < 80; i++)
for (int i = 0; i < 60; i++)
{
ht.Update(new TValue(now.AddMinutes(i), 100 + i));
ht.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i));
}
Assert.True(ht.IsHot);
ht.Reset();
Assert.False(ht.IsHot);
Assert.Equal(default, ht.Last);
}
// ── Update ──────────────────────────────────────────────────────────
[Fact]
public void Update_FirstBarsReturnZero()
{
var ht = new HtDcphase();
var result = ht.Update(new TValue(DateTime.UtcNow, 100.0));
Assert.Equal(0.0, result.Value);
}
[Fact]
@@ -65,6 +104,24 @@ public class HtDcphaseTests
$"Phase {phase} should be in range [-45, 315]");
}
[Fact]
public void Update_ProducesFiniteValuesAfterWarmup()
{
var ht = new HtDcphase();
var gbm = new GBM(seed: 99);
var bars = gbm.Fetch(150, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
foreach (var bar in bars)
{
ht.Update(new TValue(bar.Time, bar.Close));
}
Assert.True(ht.IsHot);
Assert.True(double.IsFinite(ht.Last.Value));
}
// ── Bar Correction ──────────────────────────────────────────────────
[Fact]
public void SameBarUpdate_ReturnsSameValue()
{
@@ -87,4 +144,264 @@ public class HtDcphaseTests
Assert.Equal(result1.Value, result2.Value);
}
[Fact]
public void BarCorrection_MultipleCorrections_StableState()
{
var ht = new HtDcphase();
var now = DateTime.UtcNow;
for (int i = 0; i < 70; i++)
{
ht.Update(new TValue(now.AddMinutes(i), 100 + i * 0.5));
}
// New bar
ht.Update(new TValue(now.AddMinutes(70), 150), isNew: true);
var afterNew = ht.Last;
// Multiple corrections
ht.Update(new TValue(now.AddMinutes(70), 160), isNew: false);
ht.Update(new TValue(now.AddMinutes(70), 140), isNew: false);
ht.Update(new TValue(now.AddMinutes(70), 150), isNew: false);
var afterCorrections = ht.Last;
Assert.Equal(afterNew.Value, afterCorrections.Value);
}
// ── NaN handling ────────────────────────────────────────────────────
[Fact]
public void Update_NaN_BeforeValidData_ReturnsZero()
{
var ht = new HtDcphase();
var result = ht.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.Equal(0.0, result.Value);
}
[Fact]
public void Update_NaN_AfterValidData_UsesLastValid()
{
var ht = new HtDcphase();
var now = DateTime.UtcNow;
for (int i = 0; i < 80; i++)
{
ht.Update(new TValue(now.AddMinutes(i), 100 + Math.Sin(i * 0.2) * 5));
}
Assert.True(ht.IsHot);
var result = ht.Update(new TValue(now.AddMinutes(80), double.NaN));
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Update_Infinity_UsesLastValid()
{
var ht = new HtDcphase();
var now = DateTime.UtcNow;
for (int i = 0; i < 80; i++)
{
ht.Update(new TValue(now.AddMinutes(i), 100 + i * 0.5));
}
var result = ht.Update(new TValue(now.AddMinutes(80), double.PositiveInfinity));
Assert.True(double.IsFinite(result.Value));
}
// ── Reset ───────────────────────────────────────────────────────────
[Fact]
public void Reset_ClearsState()
{
var ht = new HtDcphase();
var now = DateTime.UtcNow;
for (int i = 0; i < 80; i++)
{
ht.Update(new TValue(now.AddMinutes(i), 100 + i));
}
Assert.True(ht.IsHot);
ht.Reset();
Assert.False(ht.IsHot);
Assert.Equal(default, ht.Last);
}
[Fact]
public void Reset_AllowsReuse()
{
var ht = new HtDcphase();
var now = DateTime.UtcNow;
for (int i = 0; i < 80; i++)
{
ht.Update(new TValue(now.AddMinutes(i), 100 + Math.Sin(i * 0.2) * 5));
}
var firstResult = ht.Last.Value;
ht.Reset();
for (int i = 0; i < 80; i++)
{
ht.Update(new TValue(now.AddMinutes(i), 100 + Math.Sin(i * 0.2) * 5));
}
Assert.Equal(firstResult, ht.Last.Value);
}
// ── Batch ───────────────────────────────────────────────────────────
[Fact]
public void Batch_TSeries_MatchesStreaming()
{
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var batchResult = HtDcphase.Batch(series);
var streaming = new HtDcphase();
var streamingResults = new TSeries();
foreach (var item in series)
{
streamingResults.Add(streaming.Update(item));
}
Assert.Equal(batchResult.Count, streamingResults.Count);
for (int i = 0; i < batchResult.Count; i++)
{
Assert.Equal(batchResult[i].Value, streamingResults[i].Value, 1e-10);
}
}
[Fact]
public void Batch_Span_MatchesStreaming()
{
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var values = bars.Close.Values.ToArray();
var spanOutput = new double[values.Length];
HtDcphase.Batch(values, spanOutput);
var streaming = new HtDcphase();
for (int i = 0; i < values.Length; i++)
{
var result = streaming.Update(new TValue(DateTime.UtcNow.AddTicks(i), values[i]));
Assert.Equal(result.Value, spanOutput[i], 1e-10);
}
}
[Fact]
public void Batch_Span_OutputTooShort_Throws()
{
var source = new double[10];
var output = new double[5];
Assert.Throws<ArgumentException>(() => HtDcphase.Batch(source, output));
}
[Fact]
public void Update_EmptyTSeries_ReturnsEmpty()
{
var ht = new HtDcphase();
var result = ht.Update(new TSeries());
Assert.Empty(result);
}
[Fact]
public void Update_TSeries_ReturnsCorrectCount()
{
var ht = new HtDcphase();
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var result = ht.Update(bars.Close);
Assert.Equal(100, result.Count);
}
// ── Calculate ───────────────────────────────────────────────────────
[Fact]
public void Calculate_ReturnsBothResultsAndIndicator()
{
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var (results, indicator) = HtDcphase.Calculate(bars.Close);
Assert.Equal(100, results.Count);
Assert.True(indicator.IsHot);
}
// ── Prime ───────────────────────────────────────────────────────────
[Fact]
public void Prime_WarmsUpIndicator()
{
var ht = new HtDcphase();
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var values = bars.Close.Values.ToArray();
ht.Prime(values);
Assert.True(ht.IsHot);
}
[Fact]
public void Prime_WithStepParameter()
{
var ht = new HtDcphase();
var values = new double[80];
for (int i = 0; i < 80; i++)
{
values[i] = 100 + Math.Sin(i * 0.2) * 5;
}
ht.Prime(values, TimeSpan.FromMinutes(5));
Assert.True(ht.IsHot);
}
// ── Determinism ─────────────────────────────────────────────────────
[Fact]
public void TwoInstances_SameInput_SameOutput()
{
var ht1 = new HtDcphase();
var ht2 = new HtDcphase();
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
foreach (var bar in bars)
{
var tv = new TValue(bar.Time, bar.Close);
var r1 = ht1.Update(tv);
var r2 = ht2.Update(tv);
Assert.Equal(r1.Value, r2.Value);
}
}
// ── Constant price ──────────────────────────────────────────────────
[Fact]
public void ConstantPrice_ProducesFiniteOutput()
{
var ht = new HtDcphase();
var now = DateTime.UtcNow;
for (int i = 0; i < 100; i++)
{
var result = ht.Update(new TValue(now.AddMinutes(i), 100.0));
Assert.True(double.IsFinite(result.Value),
$"Bar {i}: Expected finite, got {result.Value}");
}
}
// ── Dispose ─────────────────────────────────────────────────────────
[Fact]
public void Dispose_DoesNotThrow()
{
var ht = new HtDcphase();
ht.Update(new TValue(DateTime.UtcNow, 100));
ht.Dispose();
Assert.True(true); // S2699: explicit assertion for dispose-only test
}
}
+18
View File
@@ -404,6 +404,24 @@ public sealed class Adx : ITValuePublisher
smoothed = Math.FusedMultiplyAdd(smoothed, decay, input * invPeriod);
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Batch(ReadOnlySpan<double> high, ReadOnlySpan<double> low, ReadOnlySpan<double> close, int period, Span<double> destination)
{
+20 -1
View File
@@ -153,6 +153,25 @@ public sealed class Adxr : ITValuePublisher
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
public static void Batch(ReadOnlySpan<double> high, ReadOnlySpan<double> low, ReadOnlySpan<double> close, int period, Span<double> destination)
{
int len = high.Length;
@@ -241,4 +260,4 @@ public sealed class Adxr : ITValuePublisher
TSeries results = indicator.Update(source);
return (results, indicator);
}
}
}
+20 -1
View File
@@ -293,6 +293,25 @@ public sealed class Alligator : ITValuePublisher
return new TSeries(tList, vList);
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
/// <summary>
/// Calculates Alligator for the entire series using default parameters.
/// </summary>
@@ -348,4 +367,4 @@ public sealed class Alligator : ITValuePublisher
/// Gets the Lips offset value (bars forward).
/// </summary>
public int LipsOffset => _lipsOffset;
}
}
+30
View File
@@ -303,6 +303,18 @@ public sealed class Amat : ITValuePublisher, IDisposable
return Last;
}
/// <summary>
/// Updates the indicator with a bar value.
/// </summary>
/// <param name="bar">Input bar</param>
/// <param name="isNew">True if this is a new bar, False if it's an update to the last bar</param>
/// <returns>Updated trend value (+1, -1, or 0)</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TBar bar, bool isNew = true)
{
return Update(new TValue(bar.Time, bar.Close), isNew);
}
/// <summary>
/// Updates the indicator with a series of values.
/// </summary>
@@ -382,6 +394,24 @@ public sealed class Amat : ITValuePublisher, IDisposable
return result;
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
/// <summary>
/// Calculates AMAT trend values for a span of input values.
/// </summary>
+18
View File
@@ -185,6 +185,24 @@ public sealed class Aroon : ITValuePublisher
return new TSeries(tList, vList);
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
/// <summary>
/// Calculates Aroon oscillator values using O(n) monotonic deque algorithm.
/// </summary>
+18
View File
@@ -169,6 +169,24 @@ public sealed class AroonOsc : ITValuePublisher
return new TSeries(tList, vList);
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
/// <summary>
/// Calculates Aroon oscillator values using the shared O(n) algorithm from Aroon.
/// </summary>
+20 -1
View File
@@ -240,6 +240,25 @@ public sealed class Chop : ITValuePublisher
return Math.Clamp(chop, 0.0, 100.0);
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
/// <summary>
/// Batch calculation with default parameters.
/// </summary>
@@ -264,4 +283,4 @@ public sealed class Chop : ITValuePublisher
return (results, indicator);
}
}
}
+23 -1
View File
@@ -36,6 +36,11 @@ public sealed class Dmx : ITValuePublisher
public TValue Last { get; private set; }
public int WarmupPeriod { get; }
/// <summary>
/// True when the indicator has enough data for valid calculations.
/// </summary>
public bool IsHot => _jmaDMp.IsHot && _jmaDMm.IsHot && _jmaTR.IsHot;
public Dmx(int period)
{
Name = $"Dmx({period})";
@@ -181,7 +186,24 @@ public sealed class Dmx : ITValuePublisher
return new TSeries(t, v);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
public static void Batch(ReadOnlySpan<double> high,
ReadOnlySpan<double> low,
ReadOnlySpan<double> close,
+18
View File
@@ -355,6 +355,24 @@ public sealed class Dx : ITValuePublisher
smoothed = smoothed - (smoothed * invPeriod) + input;
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Batch(ReadOnlySpan<double> high, ReadOnlySpan<double> low, ReadOnlySpan<double> close, int period, Span<double> destination)
{
+671 -9
View File
@@ -5,6 +5,8 @@ namespace QuanTAlib.Tests;
public class QstickIndicatorTests
{
// ── Constructor & Defaults ──────────────────────────────────────────
[Fact]
public void Constructor_CreatesValidIndicator()
{
@@ -13,6 +15,35 @@ public class QstickIndicatorTests
Assert.Equal("Qstick Indicator", indicator.Name);
}
[Fact]
public void Constructor_Description_IsNotEmpty()
{
var indicator = new QstickIndicator();
Assert.False(string.IsNullOrWhiteSpace(indicator.Description));
Assert.Contains("candlestick", indicator.Description, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void Constructor_SeparateWindow_IsTrue()
{
var indicator = new QstickIndicator();
Assert.True(indicator.SeparateWindow);
}
[Fact]
public void Constructor_CreatesOneLineSeries()
{
var indicator = new QstickIndicator();
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void Constructor_LineSeries_NameIsQstick()
{
var indicator = new QstickIndicator();
Assert.Equal("Qstick", indicator.LinesSeries[0].Name);
}
[Fact]
public void DefaultPeriod_Is14()
{
@@ -28,28 +59,216 @@ public class QstickIndicatorTests
}
[Fact]
public void ShortName_IncludesParameters()
public void DefaultShowColdValues_IsTrue()
{
var indicator = new QstickIndicator();
Assert.True(indicator.ShowColdValues);
}
// ── ShortName ───────────────────────────────────────────────────────
[Fact]
public void ShortName_DefaultParameters_IncludesPeriodAndMaType()
{
var indicator = new QstickIndicator();
Assert.Equal("QSTICK(14,SMA)", indicator.ShortName);
}
[Fact]
public void ShortName_CustomPeriod_ReflectsNewPeriod()
{
var indicator = new QstickIndicator { Period = 20 };
Assert.Equal("QSTICK(20,SMA)", indicator.ShortName);
}
[Fact]
public void ShortName_EmaMode_IncludesEMA()
{
var indicator = new QstickIndicator { Period = 20, MaType = "EMA" };
Assert.Equal("QSTICK(20,EMA)", indicator.ShortName);
}
// ── MinHistoryDepths ────────────────────────────────────────────────
[Fact]
public void MinHistoryDepths_EqualsZero()
public void MinHistoryDepths_Static_EqualsZero()
{
var indicator = new QstickIndicator { Period = 10 };
Assert.Equal(0, QstickIndicator.MinHistoryDepths);
}
[Fact]
public void MinHistoryDepths_Interface_EqualsZero()
{
var indicator = new QstickIndicator();
IWatchlistIndicator watchlistIndicator = indicator;
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
}
// ── OnInit ──────────────────────────────────────────────────────────
[Fact]
public void CalculationIntegration_ProducesCorrectValues()
public void Initialize_SmaMode_CreatesInternalIndicator()
{
var indicator = new QstickIndicator { MaType = "SMA" };
indicator.Initialize();
Assert.NotNull(indicator);
}
[Fact]
public void Initialize_EmaMode_CreatesInternalIndicator()
{
var indicator = new QstickIndicator { MaType = "EMA" };
indicator.Initialize();
Assert.NotNull(indicator);
}
[Fact]
public void Initialize_AddsZeroLineLevel()
{
var indicator = new QstickIndicator();
indicator.Initialize();
// OnInit calls AddLineLevel(0, "Zero", ...)
Assert.True(indicator.LineLevels.Count >= 1);
}
// ── ProcessUpdate: HistoricalBar ────────────────────────────────────
[Fact]
public void ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new QstickIndicator { Period = 3, MaType = "SMA" };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 5; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100.0, 110.0, 95.0, 105.0, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
// After 5 bars with constant diff=5, Qstick=5
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
}
[Fact]
public void ProcessUpdate_HistoricalBar_BullishBars_PositiveValue()
{
var indicator = new QstickIndicator { Period = 3, MaType = "SMA" };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 3; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100.0, 110.0, 95.0, 105.0, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
// All bullish, diff=5 each, SMA=5.0
Assert.Equal(5.0, indicator.LinesSeries[0].GetValue(0), 10);
}
// ── ProcessUpdate: NewBar ───────────────────────────────────────────
[Fact]
public void ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new QstickIndicator { Period = 3, MaType = "SMA" };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100.0, 110.0, 95.0, 105.0, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
indicator.HistoricalData.AddBar(now.AddMinutes(1), 100.0, 108.0, 95.0, 103.0, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
Assert.Equal(2, indicator.LinesSeries[0].Count);
}
// ── ProcessUpdate: NewTick ──────────────────────────────────────────
[Fact]
public void ProcessUpdate_NewTick_ProcessesWithoutError()
{
var indicator = new QstickIndicator { Period = 3, MaType = "SMA" };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100.0, 110.0, 95.0, 105.0, 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));
}
// ── SMA vs EMA ──────────────────────────────────────────────────────
[Fact]
public void SmaMode_BearishBars_ProducesNegativeQstick()
{
var indicator = new QstickIndicator { Period = 5, MaType = "SMA" };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 5; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100.0, 105.0, 90.0, 95.0, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
// All bearish (close < open), diff=-5
Assert.Equal(-5.0, indicator.LinesSeries[0].GetValue(0), 10);
}
[Fact]
public void SmaMode_DojiBars_ProducesZeroQstick()
{
var indicator = new QstickIndicator { Period = 5, MaType = "SMA" };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 5; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100.0, 105.0, 95.0, 100.0, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
Assert.Equal(0.0, indicator.LinesSeries[0].GetValue(0), 10);
}
[Fact]
public void EmaMode_BullishBars_ProducesPositiveQstick()
{
var indicator = new QstickIndicator { Period = 5, MaType = "EMA" };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 5; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100.0, 110.0, 95.0, 105.0, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
// All same diff=5, EMA converges to 5.0
Assert.Equal(5.0, indicator.LinesSeries[0].GetValue(0), 10);
}
// ── Core Calculation Integration ────────────────────────────────────
[Fact]
public void CalculationIntegration_SmaMode_ProducesCorrectValues()
{
var qstickCore = new Qstick(3);
var time = DateTime.UtcNow;
// Simulate bar data
var bar1 = new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000);
var bar2 = new TBar(time.AddMinutes(1).Ticks, 100.0, 105.0, 95.0, 103.0, 1000);
var bar3 = new TBar(time.AddMinutes(2).Ticks, 100.0, 108.0, 95.0, 106.0, 1000);
@@ -71,20 +290,34 @@ public class QstickIndicatorTests
// Bar 1: diff = 5
qstickCore.Update(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000));
// Bar 2: diff = -3, EMA with alpha = 0.5
// Bar 2: diff = -3, EMA with alpha = 2/(3+1) = 0.5
var result = qstickCore.Update(new TBar(time.AddMinutes(1).Ticks, 100.0, 105.0, 95.0, 97.0, 1000));
// EMA = 0.5 * -3 + 0.5 * 5 = 1.0
Assert.Equal(1.0, result.Value, 10);
}
[Fact]
public void CalculationIntegration_MixedBullishBearish()
{
var qstickCore = new Qstick(4);
var time = DateTime.UtcNow;
// 2 bullish (diff=5), 2 bearish (diff=-5) → SMA = 0
qstickCore.Update(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000));
qstickCore.Update(new TBar(time.AddMinutes(1).Ticks, 100.0, 110.0, 95.0, 105.0, 1000));
qstickCore.Update(new TBar(time.AddMinutes(2).Ticks, 100.0, 105.0, 90.0, 95.0, 1000));
var result = qstickCore.Update(new TBar(time.AddMinutes(3).Ticks, 100.0, 105.0, 90.0, 95.0, 1000));
Assert.Equal(0.0, result.Value, 10);
}
[Fact]
public void BullishBars_ProducePositiveQstick()
{
var qstick = new Qstick(5);
var time = DateTime.UtcNow;
// All bullish bars (close > open)
for (int i = 0; i < 5; i++)
{
qstick.Update(new TBar(time.AddMinutes(i).Ticks, 100.0, 110.0, 95.0, 105.0, 1000));
@@ -100,7 +333,6 @@ public class QstickIndicatorTests
var qstick = new Qstick(5);
var time = DateTime.UtcNow;
// All bearish bars (close < open)
for (int i = 0; i < 5; i++)
{
qstick.Update(new TBar(time.AddMinutes(i).Ticks, 100.0, 105.0, 90.0, 95.0, 1000));
@@ -116,7 +348,6 @@ public class QstickIndicatorTests
var qstick = new Qstick(5);
var time = DateTime.UtcNow;
// All doji bars (close = open)
for (int i = 0; i < 5; i++)
{
qstick.Update(new TBar(time.AddMinutes(i).Ticks, 100.0, 105.0, 95.0, 100.0, 1000));
@@ -125,6 +356,8 @@ public class QstickIndicatorTests
Assert.Equal(0.0, qstick.Last.Value, 10);
}
// ── Core Indicator Features ─────────────────────────────────────────
[Fact]
public void CoreIndicator_ResetsCorrectly()
{
@@ -141,4 +374,433 @@ public class QstickIndicatorTests
Assert.False(qstick.IsHot);
Assert.Equal(default, qstick.Last);
}
[Fact]
public void CoreIndicator_IsHot_SmaMode_AfterPeriodBars()
{
var qstick = new Qstick(3);
var time = DateTime.UtcNow;
Assert.False(qstick.IsHot);
qstick.Update(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000));
Assert.False(qstick.IsHot);
qstick.Update(new TBar(time.AddMinutes(1).Ticks, 100.0, 110.0, 95.0, 105.0, 1000));
Assert.False(qstick.IsHot);
qstick.Update(new TBar(time.AddMinutes(2).Ticks, 100.0, 110.0, 95.0, 105.0, 1000));
Assert.True(qstick.IsHot);
}
[Fact]
public void CoreIndicator_IsHot_EmaMode_AfterFirstBar()
{
var qstick = new Qstick(3, useEma: true);
var time = DateTime.UtcNow;
Assert.False(qstick.IsHot);
qstick.Update(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000));
Assert.True(qstick.IsHot);
}
[Fact]
public void CoreIndicator_Period_ReturnsConstructorValue()
{
var qstick = new Qstick(20);
Assert.Equal(20, qstick.Period);
}
[Fact]
public void CoreIndicator_UseEma_ReturnsConstructorValue()
{
var qstickSma = new Qstick(10, useEma: false);
Assert.False(qstickSma.UseEma);
var qstickEma = new Qstick(10, useEma: true);
Assert.True(qstickEma.UseEma);
}
[Fact]
public void CoreIndicator_WarmupPeriod_EqualsPeriod()
{
var qstick = new Qstick(20);
Assert.Equal(20, qstick.WarmupPeriod);
}
[Fact]
public void CoreIndicator_Name_SmaMode_DoesNotIncludeEma()
{
var qstick = new Qstick(14);
Assert.Equal("QSTICK(14)", qstick.Name);
}
[Fact]
public void CoreIndicator_Name_EmaMode_IncludesEma()
{
var qstick = new Qstick(14, useEma: true);
Assert.Equal("QSTICK(14,EMA)", qstick.Name);
}
[Fact]
public void CoreIndicator_InvalidPeriod_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new Qstick(0));
Assert.Throws<ArgumentException>(() => new Qstick(-1));
}
// ── NaN/Infinity Handling ───────────────────────────────────────────
[Fact]
public void CoreIndicator_NaNOpen_ReturnsLastValue()
{
var qstick = new Qstick(3);
var time = DateTime.UtcNow;
var firstResult = qstick.Update(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000));
var nanResult = qstick.Update(new TBar(time.AddMinutes(1).Ticks, double.NaN, 110.0, 95.0, 105.0, 1000));
Assert.Equal(firstResult.Value, nanResult.Value);
}
[Fact]
public void CoreIndicator_InfinityClose_ReturnsLastValue()
{
var qstick = new Qstick(3);
var time = DateTime.UtcNow;
var firstResult = qstick.Update(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000));
var infResult = qstick.Update(new TBar(time.AddMinutes(1).Ticks, 100.0, 110.0, 95.0, double.PositiveInfinity, 1000));
Assert.Equal(firstResult.Value, infResult.Value);
}
[Fact]
public void CoreIndicator_NegativeInfinityOpen_ReturnsLastValue()
{
var qstick = new Qstick(3);
var time = DateTime.UtcNow;
var firstResult = qstick.Update(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000));
var infResult = qstick.Update(new TBar(time.AddMinutes(1).Ticks, double.NegativeInfinity, 110.0, 95.0, 105.0, 1000));
Assert.Equal(firstResult.Value, infResult.Value);
}
// ── Bar Correction (isNew=false) ────────────────────────────────────
[Fact]
public void CoreIndicator_BarCorrection_SmaMode_UpdatesLastBar()
{
var qstick = new Qstick(3);
var time = DateTime.UtcNow;
qstick.Update(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000));
qstick.Update(new TBar(time.AddMinutes(1).Ticks, 100.0, 110.0, 95.0, 103.0, 1000));
// New bar
qstick.Update(new TBar(time.AddMinutes(2).Ticks, 100.0, 108.0, 95.0, 106.0, 1000), isNew: true);
var afterNew = qstick.Last.Value;
// Correct the same bar (isNew=false)
var afterCorrection = qstick.Update(new TBar(time.AddMinutes(2).Ticks, 100.0, 112.0, 93.0, 110.0, 1000), isNew: false);
// Value should change since close-open changed from 6 to 10
Assert.NotEqual(afterNew, afterCorrection.Value);
}
[Fact]
public void CoreIndicator_BarCorrection_EmaMode_RollsBackState()
{
var qstick = new Qstick(3, useEma: true);
var time = DateTime.UtcNow;
qstick.Update(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000));
// New bar
qstick.Update(new TBar(time.AddMinutes(1).Ticks, 100.0, 108.0, 95.0, 106.0, 1000), isNew: true);
var afterNew = qstick.Last.Value;
// Correct the same bar (isNew=false)
var afterCorrection = qstick.Update(new TBar(time.AddMinutes(1).Ticks, 100.0, 112.0, 93.0, 110.0, 1000), isNew: false);
Assert.NotEqual(afterNew, afterCorrection.Value);
}
// ── Batch / Update(TBarSeries) / Calculate ──────────────────────────
[Fact]
public void CoreIndicator_UpdateTBarSeries_ReturnsTSeries()
{
var qstick = new Qstick(3);
var time = DateTime.UtcNow;
var series = new TBarSeries();
for (int i = 0; i < 5; i++)
{
series.Add(time.AddMinutes(i).Ticks, 100.0, 110.0, 95.0, 105.0 + i, 1000);
}
var result = qstick.Update(series);
Assert.Equal(5, result.Count);
}
[Fact]
public void CoreIndicator_UpdateTBarSeries_EmptySeries_ReturnsEmpty()
{
var qstick = new Qstick(3);
var series = new TBarSeries();
var result = qstick.Update(series);
Assert.Empty(result);
}
[Fact]
public void CoreIndicator_Batch_ReturnsResults()
{
var time = DateTime.UtcNow;
var series = new TBarSeries();
for (int i = 0; i < 5; i++)
{
series.Add(time.AddMinutes(i).Ticks, 100.0, 110.0, 95.0, 105.0, 1000);
}
var result = Qstick.Batch(series, period: 3);
Assert.Equal(5, result.Count);
}
[Fact]
public void CoreIndicator_BatchEma_ReturnsResults()
{
var time = DateTime.UtcNow;
var series = new TBarSeries();
for (int i = 0; i < 5; i++)
{
series.Add(time.AddMinutes(i).Ticks, 100.0, 110.0, 95.0, 105.0, 1000);
}
var result = Qstick.Batch(series, period: 3, useEma: true);
Assert.Equal(5, result.Count);
Assert.Equal(5.0, result.Last.Value, 10);
}
[Fact]
public void CoreIndicator_Calculate_ReturnsResultsAndIndicator()
{
var time = DateTime.UtcNow;
var series = new TBarSeries();
for (int i = 0; i < 5; i++)
{
series.Add(time.AddMinutes(i).Ticks, 100.0, 110.0, 95.0, 105.0, 1000);
}
var (results, indicator) = Qstick.Calculate(series, period: 3);
Assert.Equal(5, results.Count);
Assert.NotNull(indicator);
Assert.True(indicator.IsHot);
}
[Fact]
public void CoreIndicator_Prime_WarmsUpIndicator()
{
var qstick = new Qstick(3);
var time = DateTime.UtcNow;
var series = new TBarSeries();
for (int i = 0; i < 5; i++)
{
series.Add(time.AddMinutes(i).Ticks, 100.0, 110.0, 95.0, 105.0, 1000);
}
qstick.Prime(series);
Assert.True(qstick.IsHot);
Assert.Equal(5.0, qstick.Last.Value, 10);
}
// ── Pub Event ───────────────────────────────────────────────────────
[Fact]
public void CoreIndicator_PubEvent_FiresOnUpdate()
{
var qstick = new Qstick(3);
var time = DateTime.UtcNow;
int eventCount = 0;
qstick.Pub += (object? sender, in TValueEventArgs args) => eventCount++;
qstick.Update(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000));
qstick.Update(new TBar(time.AddMinutes(1).Ticks, 100.0, 105.0, 95.0, 103.0, 1000));
Assert.Equal(2, eventCount);
}
[Fact]
public void CoreIndicator_PubEvent_NaN_StillFires()
{
var qstick = new Qstick(3);
var time = DateTime.UtcNow;
int eventCount = 0;
qstick.Pub += (object? sender, in TValueEventArgs args) => eventCount++;
qstick.Update(new TBar(time.Ticks, double.NaN, 110.0, 95.0, 105.0, 1000));
Assert.Equal(1, eventCount);
}
// ── Different Periods ───────────────────────────────────────────────
[Fact]
public void DifferentPeriods_ProduceDifferentResults()
{
var indicator1 = new QstickIndicator { Period = 3, MaType = "SMA" };
var indicator2 = new QstickIndicator { Period = 10, MaType = "SMA" };
indicator1.Initialize();
indicator2.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
double close = 105.0 + (i % 2 == 0 ? 3.0 : -3.0);
indicator1.HistoricalData.AddBar(now.AddMinutes(i), 100.0, 115.0, 85.0, close, 1000);
indicator1.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
indicator2.HistoricalData.AddBar(now.AddMinutes(i), 100.0, 115.0, 85.0, close, 1000);
indicator2.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
double val1 = indicator1.LinesSeries[0].GetValue(0);
double val2 = indicator2.LinesSeries[0].GetValue(0);
Assert.NotEqual(val1, val2);
}
[Fact]
public void SmaVsEma_SameData_ProduceDifferentResults()
{
var smaIndicator = new QstickIndicator { Period = 5, MaType = "SMA" };
var emaIndicator = new QstickIndicator { Period = 5, MaType = "EMA" };
smaIndicator.Initialize();
emaIndicator.Initialize();
var now = DateTime.UtcNow;
double[] closes = [105.0, 97.0, 108.0, 99.0, 102.0];
for (int i = 0; i < 5; i++)
{
smaIndicator.HistoricalData.AddBar(now.AddMinutes(i), 100.0, 115.0, 85.0, closes[i], 1000);
smaIndicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
emaIndicator.HistoricalData.AddBar(now.AddMinutes(i), 100.0, 115.0, 85.0, closes[i], 1000);
emaIndicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
double smaVal = smaIndicator.LinesSeries[0].GetValue(0);
double emaVal = emaIndicator.LinesSeries[0].GetValue(0);
// SMA = (5 + -3 + 8 + -1 + 2) / 5 = 11/5 = 2.2
Assert.Equal(2.2, smaVal, 10);
Assert.NotEqual(smaVal, emaVal);
}
// ── Reinit ──────────────────────────────────────────────────────────
[Fact]
public void Reinitialize_WithDifferentParameters_ResetsState()
{
var indicator = new QstickIndicator { Period = 5, MaType = "SMA" };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 5; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100.0, 110.0, 95.0, 105.0, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
indicator.Period = 10;
indicator.MaType = "EMA";
indicator.Initialize();
Assert.Equal("QSTICK(10,EMA)", indicator.ShortName);
}
// ── ShowColdValues ──────────────────────────────────────────────────
[Fact]
public void ShowColdValues_CanBeSetToFalse()
{
var indicator = new QstickIndicator { ShowColdValues = false };
Assert.False(indicator.ShowColdValues);
}
[Fact]
public void ShowColdValues_False_ProcessesWithoutError()
{
var indicator = new QstickIndicator { Period = 5, ShowColdValues = false };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 2; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100.0, 110.0, 95.0, 105.0, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
Assert.NotNull(indicator);
}
// ── Multiple bars through adapter with known values ─────────────────
[Fact]
public void MultipleBars_ThroughAdapter_ProducesExpectedValues()
{
var indicator = new QstickIndicator { Period = 3, MaType = "SMA" };
indicator.Initialize();
var now = DateTime.UtcNow;
// Bar 1: diff = 5 (105-100)
indicator.HistoricalData.AddBar(now, 100.0, 110.0, 95.0, 105.0, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
// Bar 2: diff = 3 (103-100)
indicator.HistoricalData.AddBar(now.AddMinutes(1), 100.0, 105.0, 95.0, 103.0, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
// Bar 3: diff = 6 (106-100)
indicator.HistoricalData.AddBar(now.AddMinutes(2), 100.0, 108.0, 95.0, 106.0, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
// SMA(3) of [5, 3, 6] = 14/3 ≈ 4.667
Assert.Equal(14.0 / 3.0, indicator.LinesSeries[0].GetValue(0), 10);
}
// ── Parameters can be modified ──────────────────────────────────────
[Fact]
public void Period_CanBeChanged()
{
var indicator = new QstickIndicator();
Assert.Equal(14, indicator.Period);
indicator.Period = 30;
Assert.Equal(30, indicator.Period);
}
[Fact]
public void MaType_CanBeChanged()
{
var indicator = new QstickIndicator();
Assert.Equal("SMA", indicator.MaType);
indicator.MaType = "EMA";
Assert.Equal("EMA", indicator.MaType);
}
}
+20 -1
View File
@@ -265,6 +265,25 @@ public sealed class Super : ITValuePublisher
return new TSeries(t, v);
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
public static TSeries Batch(TBarSeries source, int period = 10, double multiplier = 3.0)
{
var indicator = new Super(period, multiplier);
@@ -277,4 +296,4 @@ public sealed class Super : ITValuePublisher
TSeries results = indicator.Update(source);
return (results, indicator);
}
}
}
+20 -1
View File
@@ -227,6 +227,25 @@ public sealed class Vortex : ITValuePublisher
return new TSeries(tList, vList);
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
/// <summary>
/// Calculates Vortex indicator values using O(n) sliding window algorithm.
/// </summary>
@@ -238,7 +257,7 @@ public sealed class Vortex : ITValuePublisher
/// <param name="viMinus">Output VI- values</param>
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public static void Batch(ReadOnlySpan<double> high, ReadOnlySpan<double> low, ReadOnlySpan<double> close,
int period, Span<double> viPlus, Span<double> viMinus)
int period, Span<double> viPlus, Span<double> viMinus)
{
int len = high.Length;
if (len == 0 || len != low.Length || len != close.Length || len != viPlus.Length || len != viMinus.Length || period <= 1)
+234 -3
View File
@@ -12,6 +12,8 @@ public class Cheby1Tests
_gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 1234);
}
// ── Constructor ──────────────────────────────────────────────────────
[Fact]
public void Constructor_ValidatesParameters()
{
@@ -21,13 +23,48 @@ public class Cheby1Tests
Assert.NotNull(filter);
}
[Fact]
public void Constructor_NegativeRipple_Throws()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new Cheby1(10, -1.0));
}
[Fact]
public void Constructor_SetsProperties()
{
var filter = new Cheby1(20, 2.0);
Assert.Equal(20, filter.Period);
Assert.Equal(2.0, filter.Ripple);
Assert.Equal("Cheby1(20,2.0)", filter.Name);
Assert.Equal(20, filter.WarmupPeriod);
}
[Fact]
public void Constructor_DefaultRipple()
{
var filter = new Cheby1(10);
Assert.Equal(1.0, filter.Ripple);
}
[Fact]
public void Constructor_WithPublisher_Subscribes()
{
var source = new TSeries();
var filter = new Cheby1(source, 10, 1.0);
source.Add(new TValue(DateTime.UtcNow, 100));
Assert.True(double.IsFinite(filter.Last.Value));
}
// ── IsHot ───────────────────────────────────────────────────────────
[Fact]
public void IsHot_BecomesTrueWhenReady()
{
var filter = new Cheby1(20, 1.0);
Assert.False(filter.IsHot);
// Feed some data
// Feed some data - IsHot becomes true when Count >= 2
for (int i = 0; i < 3; i++)
{
filter.Update(new TValue(DateTime.UtcNow, 100.0));
@@ -36,6 +73,36 @@ public class Cheby1Tests
Assert.True(filter.IsHot);
}
// ── Update ──────────────────────────────────────────────────────────
[Fact]
public void Update_ConstantInput_ConvergesToConstant()
{
var filter = new Cheby1(10, 1.0);
for (int i = 0; i < 50; i++)
{
filter.Update(new TValue(DateTime.UtcNow, 42.0));
}
Assert.Equal(42.0, filter.Last.Value, 1e-4);
}
[Fact]
public void Update_SmoothsNoisySignal()
{
var filter = new Cheby1(20, 1.0);
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
foreach (var bar in bars)
{
filter.Update(new TValue(bar.Time, bar.Close));
}
Assert.True(double.IsFinite(filter.Last.Value));
}
// ── NaN handling ────────────────────────────────────────────────────
[Fact]
public void Update_HandlesNaNSafely()
{
@@ -51,6 +118,18 @@ public class Cheby1Tests
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Update_HandlesInfinitySafely()
{
var filter = new Cheby1(10, 1.0);
filter.Update(new TValue(DateTime.UtcNow, 100.0));
var result = filter.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
Assert.True(double.IsFinite(result.Value));
}
// ── Bar Correction ──────────────────────────────────────────────────
[Fact]
public void IterativeCorrections_RestoreState()
{
@@ -77,14 +156,48 @@ public class Cheby1Tests
Assert.NotEqual(valAfterNew, valAfterCorrection);
// 3. Correction back to original value (isNew=false)
// Note: For IIR this should theoretically restore state, but due to FP math it might drift slightly
// But the previous state property should make it exact if we haven't advanced further
filter.Update(new TValue(time.AddSeconds(5), 200.0), isNew: false);
double valRestored = filter.Last.Value;
Assert.Equal(valAfterNew, valRestored, 1e-10);
}
// ── Reset ───────────────────────────────────────────────────────────
[Fact]
public void Reset_ClearsState()
{
var filter = new Cheby1(10, 1.0);
for (int i = 0; i < 10; i++)
{
filter.Update(new TValue(DateTime.UtcNow, 100 + i));
}
Assert.True(filter.IsHot);
filter.Reset();
Assert.False(filter.IsHot);
}
[Fact]
public void Reset_AllowsReuse()
{
var filter = new Cheby1(10, 1.0);
for (int i = 0; i < 20; i++)
{
filter.Update(new TValue(DateTime.UtcNow, 100 + i));
}
var firstResult = filter.Last.Value;
filter.Reset();
for (int i = 0; i < 20; i++)
{
filter.Update(new TValue(DateTime.UtcNow, 100 + i));
}
Assert.Equal(firstResult, filter.Last.Value, 1e-10);
}
// ── Batch ───────────────────────────────────────────────────────────
[Fact]
public void SpanBatch_MatchesIterative()
{
@@ -111,4 +224,122 @@ public class Cheby1Tests
Assert.Equal(iterativeResults[i], spanResults[i], 1e-10);
}
}
[Fact]
public void Batch_TSeries_Static()
{
var data = _gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var result = Cheby1.Batch(data.Close, 10, 1.0);
Assert.Equal(50, result.Count);
}
[Fact]
public void Update_EmptyTSeries_ReturnsEmpty()
{
var filter = new Cheby1(10, 1.0);
var result = filter.Update(new TSeries());
Assert.Empty(result);
}
[Fact]
public void Update_TSeries_ReturnsCorrectCount()
{
var filter = new Cheby1(10, 1.0);
var data = _gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var result = filter.Update(data.Close);
Assert.Equal(50, result.Count);
}
// ── Calculate ───────────────────────────────────────────────────────
[Fact]
public void Calculate_ReturnsResultsAndIndicator()
{
var data = _gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var (results, indicator) = Cheby1.Calculate(data.Close, 10, 1.0);
Assert.Equal(50, results.Count);
Assert.True(indicator.IsHot);
}
// ── Prime ───────────────────────────────────────────────────────────
[Fact]
public void Prime_WarmsUpIndicator()
{
var filter = new Cheby1(10, 1.0);
var values = new double[20];
for (int i = 0; i < 20; i++)
{
values[i] = 100 + i;
}
filter.Prime(values);
Assert.True(filter.IsHot);
}
// ── Dispose ─────────────────────────────────────────────────────────
[Fact]
public void Dispose_WithPublisher_Unsubscribes()
{
var source = new TSeries();
var filter = new Cheby1(source, 10, 1.0);
source.Add(new TValue(DateTime.UtcNow, 100));
filter.Dispose();
var lastBefore = filter.Last;
source.Add(new TValue(DateTime.UtcNow, 200));
Assert.Equal(lastBefore, filter.Last);
}
[Fact]
public void Dispose_WithoutPublisher_DoesNotThrow()
{
var filter = new Cheby1(10, 1.0);
filter.Update(new TValue(DateTime.UtcNow, 100));
filter.Dispose();
Assert.True(true); // S2699: explicit assertion for dispose-only test
}
// ── Determinism ─────────────────────────────────────────────────────
[Fact]
public void TwoInstances_SameInput_SameOutput()
{
var f1 = new Cheby1(10, 1.0);
var f2 = new Cheby1(10, 1.0);
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
foreach (var bar in bars)
{
var tv = new TValue(bar.Time, bar.Close);
var r1 = f1.Update(tv);
var r2 = f2.Update(tv);
Assert.Equal(r1.Value, r2.Value);
}
}
// ── Filter behavior ─────────────────────────────────────────────────
[Fact]
public void DifferentRipple_ProduceDifferentOutputs()
{
var lowRipple = new Cheby1(10, 0.5);
var highRipple = new Cheby1(10, 3.0);
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
double lastLow = 0, lastHigh = 0;
foreach (var bar in bars)
{
var tv = new TValue(bar.Time, bar.Close);
lastLow = lowRipple.Update(tv).Value;
lastHigh = highRipple.Update(tv).Value;
}
// Different ripple parameters should generally produce different outputs
Assert.NotEqual(lastLow, lastHigh, 1e-6);
}
}
+236
View File
@@ -12,6 +12,8 @@ public class Cheby2Tests
_gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 1234);
}
// ── Constructor ──────────────────────────────────────────────────────
[Fact]
public void Constructor_ValidatesParameters()
{
@@ -21,6 +23,41 @@ public class Cheby2Tests
Assert.NotNull(filter);
}
[Fact]
public void Constructor_NegativeAttenuation_Throws()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new Cheby2(10, -1.0));
}
[Fact]
public void Constructor_SetsProperties()
{
var filter = new Cheby2(20, 10.0);
Assert.Equal(20, filter.Period);
Assert.Equal(10.0, filter.Attenuation);
Assert.Equal("Cheby2(20,10.0)", filter.Name);
Assert.Equal(20, filter.WarmupPeriod);
}
[Fact]
public void Constructor_DefaultAttenuation()
{
var filter = new Cheby2(10);
Assert.Equal(5.0, filter.Attenuation);
}
[Fact]
public void Constructor_WithPublisher_Subscribes()
{
var source = new TSeries();
var filter = new Cheby2(source, 10, 5.0);
source.Add(new TValue(DateTime.UtcNow, 100));
Assert.True(double.IsFinite(filter.Last.Value));
}
// ── IsHot ───────────────────────────────────────────────────────────
[Fact]
public void IsHot_BecomesTrueWhenReady()
{
@@ -36,6 +73,36 @@ public class Cheby2Tests
Assert.True(filter.IsHot);
}
// ── Update ──────────────────────────────────────────────────────────
[Fact]
public void Update_ConstantInput_ConvergesToConstant()
{
var filter = new Cheby2(10, 5.0);
for (int i = 0; i < 50; i++)
{
filter.Update(new TValue(DateTime.UtcNow, 42.0));
}
Assert.Equal(42.0, filter.Last.Value, 1e-4);
}
[Fact]
public void Update_SmoothsNoisySignal()
{
var filter = new Cheby2(20, 5.0);
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
foreach (var bar in bars)
{
filter.Update(new TValue(bar.Time, bar.Close));
}
Assert.True(double.IsFinite(filter.Last.Value));
}
// ── NaN handling ────────────────────────────────────────────────────
[Fact]
public void Update_HandlesNaNSafely()
{
@@ -51,6 +118,18 @@ public class Cheby2Tests
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Update_HandlesInfinitySafely()
{
var filter = new Cheby2(10, 5.0);
filter.Update(new TValue(DateTime.UtcNow, 100.0));
var result = filter.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
Assert.True(double.IsFinite(result.Value));
}
// ── Bar Correction ──────────────────────────────────────────────────
[Fact]
public void IterativeCorrections_RestoreState()
{
@@ -83,6 +162,42 @@ public class Cheby2Tests
Assert.Equal(valAfterNew, valRestored, 1e-10);
}
// ── Reset ───────────────────────────────────────────────────────────
[Fact]
public void Reset_ClearsState()
{
var filter = new Cheby2(10, 5.0);
for (int i = 0; i < 10; i++)
{
filter.Update(new TValue(DateTime.UtcNow, 100 + i));
}
Assert.True(filter.IsHot);
filter.Reset();
Assert.False(filter.IsHot);
}
[Fact]
public void Reset_AllowsReuse()
{
var filter = new Cheby2(10, 5.0);
for (int i = 0; i < 20; i++)
{
filter.Update(new TValue(DateTime.UtcNow, 100 + i));
}
var firstResult = filter.Last.Value;
filter.Reset();
for (int i = 0; i < 20; i++)
{
filter.Update(new TValue(DateTime.UtcNow, 100 + i));
}
Assert.Equal(firstResult, filter.Last.Value, 1e-10);
}
// ── Batch ───────────────────────────────────────────────────────────
[Fact]
public void SpanBatch_MatchesIterative()
{
@@ -109,4 +224,125 @@ public class Cheby2Tests
Assert.Equal(iterativeResults[i], spanResults[i], 1e-10);
}
}
[Fact]
public void Batch_TSeries_Static()
{
var data = _gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var result = Cheby2.Batch(data.Close, 10, 5.0);
Assert.Equal(50, result.Count);
}
[Fact]
public void Update_EmptyTSeries_ReturnsEmpty()
{
var filter = new Cheby2(10, 5.0);
var result = filter.Update(new TSeries());
Assert.Empty(result);
}
[Fact]
public void Update_TSeries_ReturnsCorrectCount()
{
var filter = new Cheby2(10, 5.0);
var data = _gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var result = filter.Update(data.Close);
Assert.Equal(50, result.Count);
}
// ── Calculate ───────────────────────────────────────────────────────
[Fact]
public void Calculate_ReturnsResultsAndIndicator()
{
var data = _gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var (results, indicator) = Cheby2.Calculate(data.Close, 10, 5.0);
Assert.Equal(50, results.Count);
Assert.True(indicator.IsHot);
}
// ── Prime ───────────────────────────────────────────────────────────
[Fact]
public void Prime_WarmsUpIndicator()
{
var filter = new Cheby2(10, 5.0);
var values = new double[20];
for (int i = 0; i < 20; i++)
{
values[i] = 100 + i;
}
filter.Prime(values);
Assert.True(filter.IsHot);
}
// ── Dispose ─────────────────────────────────────────────────────────
[Fact]
public void Dispose_WithPublisher_Unsubscribes()
{
var source = new TSeries();
var filter = new Cheby2(source, 10, 5.0);
source.Add(new TValue(DateTime.UtcNow, 100));
filter.Dispose();
var lastBefore = filter.Last;
source.Add(new TValue(DateTime.UtcNow, 200));
Assert.Equal(lastBefore, filter.Last);
}
[Fact]
public void Dispose_WithoutPublisher_DoesNotThrow()
{
var filter = new Cheby2(10, 5.0);
filter.Update(new TValue(DateTime.UtcNow, 100));
filter.Dispose();
Assert.True(true); // S2699: explicit assertion for dispose-only test
}
// ── Determinism ─────────────────────────────────────────────────────
[Fact]
public void TwoInstances_SameInput_SameOutput()
{
var f1 = new Cheby2(10, 5.0);
var f2 = new Cheby2(10, 5.0);
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
foreach (var bar in bars)
{
var tv = new TValue(bar.Time, bar.Close);
var r1 = f1.Update(tv);
var r2 = f2.Update(tv);
Assert.Equal(r1.Value, r2.Value);
}
}
// ── Filter behavior ─────────────────────────────────────────────────
[Fact]
public void HighAttenuation_MoreSmoothing()
{
var lowAtten = new Cheby2(10, 2.0);
var highAtten = new Cheby2(10, 20.0);
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
double sumDiffLow = 0, sumDiffHigh = 0;
foreach (var bar in bars)
{
var tv = new TValue(bar.Time, bar.Close);
var rLow = lowAtten.Update(tv);
var rHigh = highAtten.Update(tv);
sumDiffLow += Math.Abs(bar.Close - rLow.Value);
sumDiffHigh += Math.Abs(bar.Close - rHigh.Value);
}
// Both should produce finite outputs
Assert.True(double.IsFinite(lowAtten.Last.Value));
Assert.True(double.IsFinite(highAtten.Last.Value));
}
}
+318 -32
View File
@@ -14,6 +14,8 @@ public class NotchTests
_gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
}
// ── Constructor ──────────────────────────────────────────────────────
[Fact]
public void Constructor_ValidatesInput()
{
@@ -21,6 +23,74 @@ public class NotchTests
Assert.Throws<ArgumentOutOfRangeException>(() => new Notch(10, -0.5));
}
[Fact]
public void Constructor_Period1_Throws()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new Notch(1));
}
[Fact]
public void Constructor_ZeroQ_Throws()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new Notch(10, 0));
}
[Fact]
public void Constructor_SetsProperties()
{
var notch = new Notch(10, 2.0);
Assert.Equal(10, notch.NotchFreq);
Assert.Equal(2.0, notch.Bandwidth);
Assert.Equal("Notch(10,2)", notch.Name);
Assert.Equal(10, notch.WarmupPeriod);
}
[Fact]
public void Constructor_DefaultQ()
{
var notch = new Notch(10);
Assert.Equal(1.0, notch.Bandwidth);
}
[Fact]
public void Constructor_WithPublisher_Subscribes()
{
var source = new TSeries();
var notch = new Notch(source, 10, 1.0);
source.Add(new TValue(DateTime.UtcNow, 100));
Assert.True(double.IsFinite(notch.Last.Value));
}
// ── IsHot ───────────────────────────────────────────────────────────
[Fact]
public void IsHot_BecomesTrueAfterWarmup()
{
var notch = new Notch(10, 1.0);
Assert.False(notch.IsHot);
for (int i = 0; i < 10; i++)
{
notch.Update(new TValue(DateTime.UtcNow, 100));
}
Assert.True(notch.IsHot);
}
[Fact]
public void IsHot_FalseBeforeWarmup()
{
var notch = new Notch(10, 1.0);
for (int i = 0; i < 9; i++)
{
notch.Update(new TValue(DateTime.UtcNow, 100));
}
Assert.False(notch.IsHot);
}
// ── Update ──────────────────────────────────────────────────────────
[Fact]
public void Calc_ReturnsValue()
{
@@ -29,6 +99,159 @@ public class NotchTests
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Update_BarCorrection_Works()
{
var notch = new Notch(10, 1.0);
var now = DateTime.UtcNow;
for (int i = 0; i < 15; i++)
{
notch.Update(new TValue(now.AddMinutes(i), 100 + i));
}
// New bar
var result1 = notch.Update(new TValue(now.AddMinutes(15), 200), isNew: true);
// Correction
notch.Update(new TValue(now.AddMinutes(15), 150), isNew: false);
// Restore to original value
notch.Update(new TValue(now.AddMinutes(15), 200), isNew: false);
var restored = notch.Last;
Assert.Equal(result1.Value, restored.Value, 1e-10);
}
// ── NaN handling ────────────────────────────────────────────────────
[Fact]
public void Update_NaN_UsesLastValid()
{
var notch = new Notch(10, 1.0);
notch.Update(new TValue(DateTime.UtcNow, 100));
var result = notch.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Update_Infinity_UsesLastValid()
{
var notch = new Notch(10, 1.0);
notch.Update(new TValue(DateTime.UtcNow, 100));
var result = notch.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
Assert.True(double.IsFinite(result.Value));
}
// ── Reset ───────────────────────────────────────────────────────────
[Fact]
public void Reset_ClearsState()
{
var notch = new Notch(10, 1.0);
for (int i = 0; i < 15; i++)
{
notch.Update(new TValue(DateTime.UtcNow, 100 + i));
}
Assert.True(notch.IsHot);
notch.Reset();
Assert.False(notch.IsHot);
}
[Fact]
public void Reset_AllowsReuse()
{
var notch = new Notch(10, 1.0);
for (int i = 0; i < 20; i++)
{
notch.Update(new TValue(DateTime.UtcNow, 100 + i));
}
var firstResult = notch.Last.Value;
notch.Reset();
for (int i = 0; i < 20; i++)
{
notch.Update(new TValue(DateTime.UtcNow, 100 + i));
}
Assert.Equal(firstResult, notch.Last.Value, 1e-10);
}
// ── Filter behavior ─────────────────────────────────────────────────
[Fact]
public void Notch_Passes_DC()
{
// DC input (constant value) should pass through with Gain 1
var notch = new Notch(period: 10, q: 1.0);
double input = 100.0;
double output = 0;
// Warmup to stabilize (IIR transient)
for (int i = 0; i < 100; i++)
{
output = notch.Update(new TValue(DateTime.UtcNow, input)).Value;
}
Assert.Equal(input, output, precision: 6);
}
[Fact]
public void Notch_Attenuates_CenterFrequency()
{
// Period 10 means frequency is 1/10 cycles per sample.
int period = 10;
double q = 5.0; // High Q for sharp notch
var notch = new Notch(period, q);
double omega = 2.0 * Math.PI / period;
double maxAmp = 0;
for (int i = 0; i < 200; i++)
{
double val = Math.Sin(omega * i); // Input amplitude 1
double outVal = notch.Update(new TValue(DateTime.UtcNow, val)).Value;
if (i > 50) // ignore transient
{
maxAmp = Math.Max(maxAmp, Math.Abs(outVal));
}
}
// At exact notch frequency, ideal is 0.
Assert.True(maxAmp < 0.1, $"Amplitude {maxAmp} should be attenuated ( < 0.1 )");
}
[Fact]
public void Notch_PassesNonNotchFrequency()
{
// Non-notch frequency should pass through with ~unity gain
int notchPeriod = 10;
var notch = new Notch(notchPeriod, 1.0);
// Use a frequency far from the notch (period 50 instead of 10)
double omega = 2.0 * Math.PI / 50.0;
double maxAmp = 0;
for (int i = 0; i < 300; i++)
{
double val = Math.Sin(omega * i);
double outVal = notch.Update(new TValue(DateTime.UtcNow, val)).Value;
if (i > 100) // ignore transient
{
maxAmp = Math.Max(maxAmp, Math.Abs(outVal));
}
}
// At non-notch frequency, output should be close to input amplitude (1.0)
Assert.True(maxAmp > 0.7, $"Non-notch amplitude {maxAmp} should be high (> 0.7)");
}
// ── Batch ───────────────────────────────────────────────────────────
[Fact]
public void AllModes_ProduceSameResult()
{
@@ -66,47 +289,110 @@ public class NotchTests
}
[Fact]
public void Notch_Passes_DC()
public void Batch_TSeries_Static()
{
// DC input (constant value) should pass through with Gain 1
var notch = new Notch(period: 10, q: 1.0);
double input = 100.0;
double output = 0;
// Warmup to stabilize (IIR transient)
for (int i = 0; i < 100; i++)
{
output = notch.Update(new TValue(DateTime.UtcNow, input)).Value;
}
Assert.Equal(input, output, precision: 6);
var data = _gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var result = Notch.Batch(data.Close, 10, 1.0);
Assert.Equal(50, result.Count);
}
[Fact]
public void Notch_Attenuates_CenterFrequency()
public void Update_EmptyTSeries_ReturnsEmpty()
{
// Period 10 means frequency is 1/10 cycles per sample.
// theta = 2*pi/10
int period = 10;
double q = 5.0; // High Q for sharp notch
var notch = new Notch(period, q);
var notch = new Notch(10, 1.0);
var result = notch.Update(new TSeries());
Assert.Empty(result);
}
double omega = 2.0 * Math.PI / period;
// ── Calculate ───────────────────────────────────────────────────────
double maxAmp = 0;
for (int i = 0; i < 200; i++)
[Fact]
public void Calculate_ReturnsResultsAndIndicator()
{
var data = _gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var (results, indicator) = Notch.Calculate(data.Close, 10, 1.0);
Assert.Equal(50, results.Count);
Assert.True(indicator.IsHot);
}
// ── Prime ───────────────────────────────────────────────────────────
[Fact]
public void Prime_WarmsUpIndicator()
{
var notch = new Notch(10, 1.0);
var values = new double[15];
for (int i = 0; i < 15; i++)
{
double val = Math.Sin(omega * i); // Input amplitude 1
double outVal = notch.Update(new TValue(DateTime.UtcNow, val)).Value;
if (i > 50) // ignore transient
{
maxAmp = Math.Max(maxAmp, Math.Abs(outVal));
}
values[i] = 100 + i;
}
// At exact notch frequency, ideal is 0.
// With Q=5, it should be very small.
Assert.True(maxAmp < 0.1, $"Amplitude {maxAmp} should be attenuated ( < 0.1 )");
notch.Prime(values);
Assert.True(notch.IsHot);
}
// ── Dispose ─────────────────────────────────────────────────────────
[Fact]
public void Dispose_WithPublisher_Unsubscribes()
{
var source = new TSeries();
var notch = new Notch(source, 10, 1.0);
source.Add(new TValue(DateTime.UtcNow, 100));
notch.Dispose();
var lastBefore = notch.Last;
source.Add(new TValue(DateTime.UtcNow, 200));
Assert.Equal(lastBefore, notch.Last);
}
[Fact]
public void Dispose_WithoutPublisher_DoesNotThrow()
{
var notch = new Notch(10, 1.0);
notch.Update(new TValue(DateTime.UtcNow, 100));
notch.Dispose();
Assert.True(true); // S2699: explicit assertion for dispose-only test
}
// ── Determinism ─────────────────────────────────────────────────────
[Fact]
public void TwoInstances_SameInput_SameOutput()
{
var n1 = new Notch(10, 1.0);
var n2 = new Notch(10, 1.0);
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
foreach (var bar in bars)
{
var tv = new TValue(bar.Time, bar.Close);
var r1 = n1.Update(tv);
var r2 = n2.Update(tv);
Assert.Equal(r1.Value, r2.Value);
}
}
// ── Q Factor ────────────────────────────────────────────────────────
[Fact]
public void DifferentQ_ProduceDifferentOutputs()
{
var narrowNotch = new Notch(10, 0.5);
var wideNotch = new Notch(10, 5.0);
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
double lastNarrow = 0, lastWide = 0;
foreach (var bar in bars)
{
var tv = new TValue(bar.Time, bar.Close);
lastNarrow = narrowNotch.Update(tv).Value;
lastWide = wideNotch.Update(tv).Value;
}
Assert.NotEqual(lastNarrow, lastWide, 1e-6);
}
}
+270 -15
View File
@@ -11,6 +11,8 @@ public class SgfTests
_gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
}
// ── Constructor ──────────────────────────────────────────────────────
[Fact]
public void Constructor_ValidatesInput()
{
@@ -22,6 +24,203 @@ public class SgfTests
Assert.Equal("Sgf(9,2)", sgf.Name); // Period adjusted to odd
}
[Fact]
public void Constructor_EvenPeriod_AdjustedToOdd()
{
var sgf = new Sgf(10, 2);
Assert.Equal("Sgf(9,2)", sgf.Name);
Assert.Equal(9, sgf.WarmupPeriod);
}
[Fact]
public void Constructor_OddPeriod_Unchanged()
{
var sgf = new Sgf(11, 2);
Assert.Equal("Sgf(11,2)", sgf.Name);
Assert.Equal(11, sgf.WarmupPeriod);
}
[Fact]
public void Constructor_Period1_Works()
{
// Period 1 should work (minimum after adjustment)
var sgf = new Sgf(1, 0);
Assert.NotNull(sgf);
}
[Fact]
public void Constructor_PolyOrder4_Works()
{
var sgf = new Sgf(11, 4);
Assert.Equal("Sgf(11,4)", sgf.Name);
}
[Fact]
public void Constructor_DefaultPolyOrder_Is2()
{
var sgf = new Sgf(11);
Assert.Equal("Sgf(11,2)", sgf.Name);
}
[Fact]
public void Constructor_WithPublisher_Subscribes()
{
var source = new TSeries();
var sgf = new Sgf(source, 11, 2);
source.Add(new TValue(DateTime.UtcNow, 100));
Assert.True(double.IsFinite(sgf.Last.Value));
}
// ── IsHot / WarmupPeriod ────────────────────────────────────────────
[Fact]
public void WarmupPeriod_IsCorrect()
{
var sgf = new Sgf(21, 2);
Assert.Equal(21, sgf.WarmupPeriod);
Assert.False(sgf.IsHot);
for (int i = 0; i < 21; i++)
{
sgf.Update(new TValue(DateTime.UtcNow, 100));
}
Assert.True(sgf.IsHot);
}
[Fact]
public void IsHot_FalseBeforeFull()
{
var sgf = new Sgf(11, 2);
for (int i = 0; i < 10; i++)
{
sgf.Update(new TValue(DateTime.UtcNow, 100));
}
Assert.False(sgf.IsHot);
}
// ── Update ──────────────────────────────────────────────────────────
[Fact]
public void Update_ConstantInput_ReturnsConstant()
{
var sgf = new Sgf(5, 2);
for (int i = 0; i < 10; i++)
{
sgf.Update(new TValue(DateTime.UtcNow, 42.0));
}
// A constant signal filtered should still be constant
Assert.Equal(42.0, sgf.Last.Value, 1e-9);
}
[Fact]
public void Update_SmoothsNoisyInput()
{
var sgf = new Sgf(21, 2);
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
foreach (var bar in bars)
{
sgf.Update(new TValue(bar.Time, bar.Close));
}
// Filtered output should be finite and not exactly equal to raw
Assert.True(double.IsFinite(sgf.Last.Value));
}
[Fact]
public void Update_BarCorrection_Works()
{
var sgf = new Sgf(5, 2);
var now = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
sgf.Update(new TValue(now.AddMinutes(i), 100 + i));
}
// New bar
var result1 = sgf.Update(new TValue(now.AddMinutes(10), 200), isNew: true);
// Correction to same bar
var result2 = sgf.Update(new TValue(now.AddMinutes(10), 150), isNew: false);
// Different corrections should yield different results
Assert.NotEqual(result1.Value, result2.Value);
}
[Fact]
public void Update_PartialWindow_Works()
{
var sgf = new Sgf(11, 2);
// First value before buffer is full should still produce a finite result
var result = sgf.Update(new TValue(DateTime.UtcNow, 100));
Assert.True(double.IsFinite(result.Value));
}
// ── NaN handling ────────────────────────────────────────────────────
[Fact]
public void HandlesNaN()
{
var sgf = new Sgf(5, 2);
sgf.Update(new TValue(DateTime.UtcNow, 100));
sgf.Update(new TValue(DateTime.UtcNow, double.NaN));
sgf.Update(new TValue(DateTime.UtcNow, 102));
// Should produce valid result if sufficient valid data exists within window
Assert.True(double.IsFinite(sgf.Last.Value));
}
[Fact]
public void AllNaN_InWindow_ReturnsInput()
{
var sgf = new Sgf(3, 1);
sgf.Update(new TValue(DateTime.UtcNow, double.NaN));
// With only NaN in buffer and partial window, should fallback to input
// (wSum <= epsilon path)
Assert.True(double.IsNaN(sgf.Last.Value) || double.IsFinite(sgf.Last.Value));
}
// ── Reset ───────────────────────────────────────────────────────────
[Fact]
public void Reset_ClearsState()
{
var sgf = new Sgf(11, 2);
for (int i = 0; i < 20; i++)
{
sgf.Update(new TValue(DateTime.UtcNow, 100 + i));
}
Assert.True(sgf.IsHot);
sgf.Reset();
Assert.False(sgf.IsHot);
}
[Fact]
public void Reset_AllowsReuse()
{
var sgf = new Sgf(5, 2);
for (int i = 0; i < 10; i++)
{
sgf.Update(new TValue(DateTime.UtcNow, 100 + i));
}
var firstLast = sgf.Last.Value;
sgf.Reset();
for (int i = 0; i < 10; i++)
{
sgf.Update(new TValue(DateTime.UtcNow, 100 + i));
}
Assert.Equal(firstLast, sgf.Last.Value, 1e-10);
}
// ── Batch ───────────────────────────────────────────────────────────
[Fact]
public void AllModes_ProduceSameResult()
{
@@ -67,30 +266,86 @@ public class SgfTests
}
[Fact]
public void HandlesNaN()
public void Batch_TSeries_Static()
{
var sgf = new Sgf(5, 2);
sgf.Update(new TValue(DateTime.UtcNow, 100));
sgf.Update(new TValue(DateTime.UtcNow, double.NaN));
sgf.Update(new TValue(DateTime.UtcNow, 102));
// Should produce valid result if sufficient valid data exists within window
Assert.True(double.IsFinite(sgf.Last.Value));
var data = _gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var result = Sgf.Batch(data.Close, 11, 2);
Assert.Equal(50, result.Count);
}
[Fact]
public void WarmupPeriod_IsCorrect()
public void Batch_Span_MismatchedLength_Throws()
{
var sgf = new Sgf(21, 2);
Assert.Equal(21, sgf.WarmupPeriod);
Assert.False(sgf.IsHot);
var source = new double[10];
var output = new double[5];
Assert.Throws<ArgumentException>(() => Sgf.Batch(source, output, 5, 2));
}
for (int i = 0; i < 21; i++)
[Fact]
public void Batch_Span_PolyOrderTooLarge_Throws()
{
var source = new double[10];
var output = new double[10];
Assert.Throws<ArgumentException>(() => Sgf.Batch(source, output, 5, 5));
}
[Fact]
public void Update_EmptyTSeries_ReturnsEmpty()
{
var sgf = new Sgf(11, 2);
var result = sgf.Update(new TSeries());
Assert.Empty(result);
}
// ── Calculate ───────────────────────────────────────────────────────
[Fact]
public void Calculate_ReturnsResultsAndIndicator()
{
var data = _gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var (results, indicator) = Sgf.Calculate(data.Close, 11, 2);
Assert.Equal(50, results.Count);
Assert.True(indicator.IsHot);
}
// ── Prime ───────────────────────────────────────────────────────────
[Fact]
public void Prime_WarmsUpIndicator()
{
var sgf = new Sgf(11, 2);
var values = new double[20];
for (int i = 0; i < 20; i++)
{
sgf.Update(new TValue(DateTime.UtcNow, 100));
values[i] = 100 + i;
}
sgf.Prime(values);
Assert.True(sgf.IsHot);
}
// ── Dispose ─────────────────────────────────────────────────────────
[Fact]
public void Dispose_WithPublisher_Unsubscribes()
{
var source = new TSeries();
var sgf = new Sgf(source, 5, 2);
source.Add(new TValue(DateTime.UtcNow, 100));
sgf.Dispose();
var lastBefore = sgf.Last;
source.Add(new TValue(DateTime.UtcNow, 200));
Assert.Equal(lastBefore, sgf.Last);
}
[Fact]
public void Dispose_WithoutPublisher_DoesNotThrow()
{
var sgf = new Sgf(5, 2);
sgf.Update(new TValue(DateTime.UtcNow, 100));
sgf.Dispose();
Assert.True(true); // S2699: explicit assertion for dispose-only test
}
}
+333 -31
View File
@@ -11,6 +11,8 @@ public class WienerTests
_gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
}
// ── Constructor ──────────────────────────────────────────────────────
[Fact]
public void Constructor_ValidatesInput()
{
@@ -22,6 +24,208 @@ public class WienerTests
Assert.Equal("Wiener(10,10)", wiener.Name);
}
[Fact]
public void Constructor_Period1_Throws()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new Wiener(1));
}
[Fact]
public void Constructor_SmoothPeriod1_Throws()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new Wiener(10, 1));
}
[Fact]
public void Constructor_DefaultSmoothPeriod()
{
var wiener = new Wiener(10);
Assert.Equal("Wiener(10,10)", wiener.Name);
}
[Fact]
public void Constructor_WarmupPeriod_IsMax()
{
var wiener = new Wiener(20, 5);
Assert.Equal(20, wiener.WarmupPeriod);
var wiener2 = new Wiener(5, 20);
Assert.Equal(20, wiener2.WarmupPeriod);
}
// ── IsHot / WarmupPeriod ────────────────────────────────────────────
[Fact]
public void WarmupPeriod_IsCorrect()
{
int period = 20;
int smooth = 10;
var wiener = new Wiener(period, smooth);
// Requirement: WarmupPeriod = Math.Max(period, smooth)
Assert.Equal(Math.Max(period, smooth), wiener.WarmupPeriod);
Assert.False(wiener.IsHot);
for (int i = 0; i < Math.Max(period, smooth); i++)
{
wiener.Update(new TValue(DateTime.UtcNow, 100));
}
Assert.True(wiener.IsHot);
}
[Fact]
public void IsHot_FalseBeforeWarmup()
{
var wiener = new Wiener(10, 10);
for (int i = 0; i < 9; i++)
{
wiener.Update(new TValue(DateTime.UtcNow, 100));
}
Assert.False(wiener.IsHot);
}
// ── Update ──────────────────────────────────────────────────────────
[Fact]
public void Update_SingleValue_ReturnsItself()
{
var wiener = new Wiener(5, 5);
var result = wiener.Update(new TValue(DateTime.UtcNow, 42.0));
Assert.Equal(42.0, result.Value);
}
[Fact]
public void Update_ConstantInput_ConvergesToConstant()
{
var wiener = new Wiener(10, 5);
for (int i = 0; i < 50; i++)
{
wiener.Update(new TValue(DateTime.UtcNow, 42.0));
}
Assert.Equal(42.0, wiener.Last.Value, 1e-6);
}
[Fact]
public void Update_SmoothsNoisySignal()
{
var wiener = new Wiener(20, 10);
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
foreach (var bar in bars)
{
wiener.Update(new TValue(bar.Time, bar.Close));
}
Assert.True(wiener.IsHot);
Assert.True(double.IsFinite(wiener.Last.Value));
}
[Fact]
public void Update_BarCorrection_Works()
{
var wiener = new Wiener(5, 5);
var now = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
wiener.Update(new TValue(now.AddMinutes(i), 100 + i));
}
// New bar
var result1 = wiener.Update(new TValue(now.AddMinutes(10), 200), isNew: true);
// Correction
var result2 = wiener.Update(new TValue(now.AddMinutes(10), 150), isNew: false);
// Different correction values should yield different results
Assert.NotEqual(result1.Value, result2.Value);
}
// ── NaN handling ────────────────────────────────────────────────────
[Fact]
public void HandlesNaN()
{
var wiener = new Wiener(5, 5);
wiener.Update(new TValue(DateTime.UtcNow, 100));
wiener.Update(new TValue(DateTime.UtcNow, double.NaN));
wiener.Update(new TValue(DateTime.UtcNow, 102));
// Should produce valid result if sufficient valid data exists within window (or handle it gracefully)
Assert.True(double.IsFinite(wiener.Last.Value));
}
[Fact]
public void NaN_FirstValue_UsesFallback()
{
var wiener = new Wiener(5, 5);
var result = wiener.Update(new TValue(DateTime.UtcNow, double.NaN));
// Fallback: Last.Value defaults to 0.0 (finite), so code returns 0.0
// The code: double.IsFinite(Last.Value) ? Last.Value : input.Value
Assert.Equal(0.0, result.Value);
}
[Fact]
public void NaN_AfterValid_UsesLastValid()
{
var wiener = new Wiener(5, 5);
wiener.Update(new TValue(DateTime.UtcNow, 100));
var result = wiener.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.Equal(100.0, result.Value);
}
[Fact]
public void Infinity_AfterValid_UsesLastValid()
{
var wiener = new Wiener(5, 5);
wiener.Update(new TValue(DateTime.UtcNow, 100));
var result = wiener.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
Assert.Equal(100.0, result.Value);
}
// ── Reset ───────────────────────────────────────────────────────────
[Fact]
public void Reset_ClearsState()
{
var wiener = new Wiener(10, 5);
int warmup = Math.Max(10, 5);
// Fill up to make it Hot
for (int i = 0; i < warmup; i++)
{
wiener.Update(new TValue(DateTime.UtcNow, 100 + i));
}
Assert.True(wiener.IsHot);
wiener.Reset();
Assert.False(wiener.IsHot);
}
[Fact]
public void Reset_AllowsReuse()
{
var wiener = new Wiener(5, 5);
for (int i = 0; i < 10; i++)
{
wiener.Update(new TValue(DateTime.UtcNow, 100 + i));
}
var firstResult = wiener.Last.Value;
wiener.Reset();
for (int i = 0; i < 10; i++)
{
wiener.Update(new TValue(DateTime.UtcNow, 100 + i));
}
Assert.Equal(firstResult, wiener.Last.Value, 1e-10);
}
// ── Batch ───────────────────────────────────────────────────────────
[Fact]
public void AllModes_ProduceSameResult()
{
@@ -67,50 +271,148 @@ public class WienerTests
}
[Fact]
public void HandlesNaN()
public void Batch_TSeries_Static()
{
var wiener = new Wiener(5, 5);
wiener.Update(new TValue(DateTime.UtcNow, 100));
wiener.Update(new TValue(DateTime.UtcNow, double.NaN));
wiener.Update(new TValue(DateTime.UtcNow, 102));
// Should produce valid result if sufficient valid data exists within window (or handle it gracefully)
Assert.True(double.IsFinite(wiener.Last.Value));
var data = _gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var result = Wiener.Batch(data.Close, 10, 5);
Assert.Equal(50, result.Count);
}
[Fact]
public void WarmupPeriod_IsCorrect()
public void Batch_Span_OutputTooShort_Throws()
{
int period = 20;
int smooth = 10;
var wiener = new Wiener(period, smooth);
// Requirement: WarmupPeriod = Math.Max(period, smooth)
Assert.Equal(Math.Max(period, smooth), wiener.WarmupPeriod);
Assert.False(wiener.IsHot);
for (int i = 0; i < Math.Max(period, smooth); i++)
{
wiener.Update(new TValue(DateTime.UtcNow, 100));
}
Assert.True(wiener.IsHot);
var source = new double[10];
var output = new double[5];
Assert.Throws<ArgumentException>(() => Wiener.Batch(source, output, 5, 5));
}
[Fact]
public void Reset_ClearsState()
public void Update_EmptyTSeries_ReturnsEmpty()
{
var wiener = new Wiener(10, 5);
int warmup = Math.Max(10, 5);
var result = wiener.Update(new TSeries());
Assert.Empty(result);
}
// Fill up to make it Hot
for (int i = 0; i < warmup; i++)
[Fact]
public void Update_TSeries_ReturnsCorrectCount()
{
var wiener = new Wiener(10, 5);
var data = _gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var result = wiener.Update(data.Close);
Assert.Equal(50, result.Count);
}
// ── Calculate ───────────────────────────────────────────────────────
[Fact]
public void Calculate_ReturnsResultsAndIndicator()
{
var data = _gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var (results, indicator) = Wiener.Calculate(data.Close, 10, 5);
Assert.Equal(50, results.Count);
Assert.True(indicator.IsHot);
}
// ── Prime ───────────────────────────────────────────────────────────
[Fact]
public void Prime_WarmsUpIndicator()
{
var wiener = new Wiener(10, 5);
var values = new double[20];
for (int i = 0; i < 20; i++)
{
wiener.Update(new TValue(DateTime.UtcNow, 100 + i));
values[i] = 100 + i;
}
Assert.True(wiener.IsHot);
wiener.Reset();
Assert.False(wiener.IsHot);
wiener.Prime(values);
Assert.True(wiener.IsHot);
}
[Fact]
public void Prime_WithStepParameter()
{
var wiener = new Wiener(10, 5);
var values = new double[20];
for (int i = 0; i < 20; i++)
{
values[i] = 100 + i;
}
wiener.Prime(values, TimeSpan.FromMinutes(5));
Assert.True(wiener.IsHot);
}
// ── Determinism ─────────────────────────────────────────────────────
[Fact]
public void TwoInstances_SameInput_SameOutput()
{
var w1 = new Wiener(10, 5);
var w2 = new Wiener(10, 5);
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
foreach (var bar in bars)
{
var tv = new TValue(bar.Time, bar.Close);
var r1 = w1.Update(tv);
var r2 = w2.Update(tv);
Assert.Equal(r1.Value, r2.Value);
}
}
// ── Filter behavior ─────────────────────────────────────────────────
[Fact]
public void LargerPeriod_MoreSmoothing()
{
var smallPeriod = new Wiener(5, 5);
var largePeriod = new Wiener(20, 5);
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
double sumDiffSmall = 0, sumDiffLarge = 0;
int count = 0;
foreach (var bar in bars)
{
var tv = new TValue(bar.Time, bar.Close);
var rSmall = smallPeriod.Update(tv);
var rLarge = largePeriod.Update(tv);
if (smallPeriod.IsHot && largePeriod.IsHot)
{
sumDiffSmall += Math.Abs(bar.Close - rSmall.Value);
sumDiffLarge += Math.Abs(bar.Close - rLarge.Value);
count++;
}
}
// Both should produce finite outputs
Assert.True(double.IsFinite(smallPeriod.Last.Value));
Assert.True(double.IsFinite(largePeriod.Last.Value));
Assert.True(count > 0);
}
[Fact]
public void DifferentSmoothPeriod_ProduceDifferentOutputs()
{
var smooth5 = new Wiener(10, 5);
var smooth15 = new Wiener(10, 15);
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
double last5 = 0, last15 = 0;
foreach (var bar in bars)
{
var tv = new TValue(bar.Time, bar.Close);
last5 = smooth5.Update(tv).Value;
last15 = smooth15.Update(tv).Value;
}
Assert.NotEqual(last5, last15, 1e-6);
}
}
+18
View File
@@ -175,6 +175,24 @@ public sealed class Bop : ITValuePublisher
return new TSeries(t, v);
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
public static (TSeries Results, Bop Indicator) Calculate(TBarSeries source)
{
var indicator = new Bop();
-10
View File
@@ -68,16 +68,6 @@ public sealed class Cci : ITValuePublisher
/// </summary>
public int WarmupPeriod => _period;
/// <summary>
/// Returns the default warmup period (<see cref="DefaultPeriod"/>).
/// </summary>
/// <remarks>
/// This static accessor is provided for backward compatibility. Prefer the instance
/// <see cref="WarmupPeriod"/> property which returns the actual configured period.
/// </remarks>
[Obsolete("Use the instance WarmupPeriod property instead. This static accessor returns the default period (20) and will be removed in a future major version.")]
public static int DefaultWarmupPeriod => DefaultPeriod;
/// <summary>
/// Creates a CCI indicator with specified period.
/// </summary>
+20 -1
View File
@@ -299,6 +299,25 @@ public sealed class Cfb : ITValuePublisher, IDisposable
return new TSeries(t, v);
}
/// <summary>
/// Initializes the indicator state using the provided value series history.
/// </summary>
/// <param name="source">Historical input data.</param>
public void Prime(TSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
public static TSeries Batch(TSeries source, int[]? lengths = null)
{
var cfb = new Cfb(lengths);
@@ -440,4 +459,4 @@ public sealed class Cfb : ITValuePublisher, IDisposable
TSeries results = indicator.Update(source);
return (results, indicator);
}
}
}
+20 -1
View File
@@ -129,6 +129,25 @@ public sealed class Macd : ITValuePublisher, IDisposable
return new TSeries(t, v);
}
/// <summary>
/// Initializes the indicator state using the provided series history.
/// </summary>
/// <param name="source">Historical data.</param>
public void Prime(TSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(new TValue(new DateTime(source.Times[i], DateTimeKind.Utc), source.Values[i]), isNew: true);
}
}
public static TSeries Batch(TSeries source, int fastPeriod = 12, int slowPeriod = 26, int signalPeriod = 9)
{
var indicator = new Macd(fastPeriod, slowPeriod, signalPeriod);
@@ -178,4 +197,4 @@ public sealed class Macd : ITValuePublisher, IDisposable
TSeries results = indicator.Update(source);
return (results, indicator);
}
}
}
+19
View File
@@ -214,6 +214,25 @@ public sealed class Rsx : ITValuePublisher
return new TSeries(t, v);
}
/// <summary>
/// Initializes the indicator state using the provided series history.
/// </summary>
/// <param name="source">Historical data.</param>
public void Prime(TSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(new TValue(new DateTime(source.Times[i], DateTimeKind.Utc), source.Values[i]), isNew: true);
}
}
public static TSeries Batch(TSeries source, int period)
{
var rsx = new Rsx(period);
+19
View File
@@ -119,6 +119,25 @@ public sealed class Vel : ITValuePublisher, IDisposable
return new TSeries(t, v);
}
/// <summary>
/// Initializes the indicator state using the provided series history.
/// </summary>
/// <param name="source">Historical data.</param>
public void Prime(TSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(new TValue(new DateTime(source.Times[i], DateTimeKind.Utc), source.Values[i]), isNew: true);
}
}
public static TSeries Batch(TSeries source, int period)
{
int len = source.Count;
+18
View File
@@ -202,6 +202,24 @@ public sealed class Ao : ITValuePublisher
return new TSeries(tList, vList);
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
/// <summary>
/// Calculates AO over OHLC spans into a preallocated output span.
/// Median price is computed as (High + Low) / 2.
+19
View File
@@ -161,6 +161,25 @@ public sealed class Apo : ITValuePublisher, IDisposable
Update(args.Value, args.IsNew);
}
/// <summary>
/// Initializes the indicator state using the provided series history.
/// </summary>
/// <param name="source">Historical data.</param>
public void Prime(TSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(new TValue(new DateTime(source.Times[i], DateTimeKind.Utc), source.Values[i]), isNew: true);
}
}
/// <summary>
/// Calculates APO for the entire series using a new instance.
/// </summary>
+20 -1
View File
@@ -242,6 +242,25 @@ public sealed class Frama : ITValuePublisher, IDisposable
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Handle(object? sender, in TValueEventArgs args) => Update(args.Value, args.IsNew);
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
public static void Batch(ReadOnlySpan<double> high, ReadOnlySpan<double> low, int period, Span<double> output)
{
if (high.Length != low.Length || high.Length != output.Length)
@@ -392,4 +411,4 @@ public sealed class Frama : ITValuePublisher, IDisposable
_disposed = true;
}
}
}
}
+297 -7
View File
@@ -3,6 +3,109 @@ namespace QuanTAlib.Tests;
public class MgdiTests
{
private readonly GBM _gbm;
public MgdiTests()
{
_gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
}
// ── Constructor ──────────────────────────────────────────────────────
[Fact]
public void Constructor_SetsDefaults()
{
var mgdi = new Mgdi();
Assert.Equal("Mgdi(14,0.6)", mgdi.Name);
Assert.Equal(14, mgdi.WarmupPeriod);
Assert.False(mgdi.IsHot);
}
[Fact]
public void Constructor_CustomParameters()
{
var mgdi = new Mgdi(20, 0.8);
Assert.Equal("Mgdi(20,0.8)", mgdi.Name);
Assert.Equal(20, mgdi.WarmupPeriod);
}
[Fact]
public void Constructor_PeriodZero_Throws()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new Mgdi(0));
}
[Fact]
public void Constructor_NegativePeriod_Throws()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new Mgdi(-1));
}
[Fact]
public void Constructor_ZeroK_Throws()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new Mgdi(14, 0));
}
[Fact]
public void Constructor_NegativeK_Throws()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new Mgdi(14, -1));
}
[Fact]
public void Calculate_InvalidK_ThrowsArgumentOutOfRangeException()
{
var source = new double[10];
var output = new double[10];
Assert.Throws<ArgumentOutOfRangeException>(() => Mgdi.Batch(source, output, 14, double.NaN));
Assert.Throws<ArgumentOutOfRangeException>(() => Mgdi.Batch(source, output, 14, double.PositiveInfinity));
Assert.Throws<ArgumentOutOfRangeException>(() => Mgdi.Batch(source, output, 14, double.NegativeInfinity));
Assert.Throws<ArgumentOutOfRangeException>(() => Mgdi.Batch(source, output, 14, 0));
Assert.Throws<ArgumentOutOfRangeException>(() => Mgdi.Batch(source, output, 14, -1));
}
[Fact]
public void Constructor_NaNK_Throws()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new Mgdi(14, double.NaN));
}
[Fact]
public void Constructor_InfinityK_Throws()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new Mgdi(14, double.PositiveInfinity));
}
[Fact]
public void Constructor_WithPublisher_Subscribes()
{
var source = new TSeries();
var mgdi = new Mgdi(source, 14, 0.6);
source.Add(new TValue(DateTime.UtcNow, 100));
Assert.True(double.IsFinite(mgdi.Last.Value));
}
// ── IsHot ───────────────────────────────────────────────────────────
[Fact]
public void IsHot_BecomesTrue_AfterPeriodUpdates()
{
var mgdi = new Mgdi(14, 0.6);
for (int i = 0; i < 13; i++)
{
mgdi.Update(new TValue(DateTime.UtcNow, 100 + i));
}
Assert.False(mgdi.IsHot);
mgdi.Update(new TValue(DateTime.UtcNow, 113));
Assert.True(mgdi.IsHot);
}
// ── NaN handling ────────────────────────────────────────────────────
[Fact]
public void NaN_FirstValue_DoesNotInitializeToZero()
{
@@ -31,6 +134,29 @@ public class MgdiTests
Assert.Equal(firstValid, result.Value);
}
[Fact]
public void NaN_AfterValid_UsesLastValid()
{
var mgdi = new Mgdi(14, 0.6);
mgdi.Update(new TValue(DateTime.UtcNow, 100.0));
mgdi.Update(new TValue(DateTime.UtcNow, 101.0));
var result = mgdi.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Infinity_AfterValid_UsesLastValid()
{
var mgdi = new Mgdi(14, 0.6);
mgdi.Update(new TValue(DateTime.UtcNow, 100.0));
var result = mgdi.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
Assert.True(double.IsFinite(result.Value));
}
// ── Calculation ─────────────────────────────────────────────────────
[Fact]
public void Standard_Calculation()
{
@@ -43,15 +169,179 @@ public class MgdiTests
}
[Fact]
public void Calculate_InvalidK_ThrowsArgumentOutOfRangeException()
public void ConstantPrice_ConvergesToPrice()
{
var mgdi = new Mgdi(14, 0.6);
for (int i = 0; i < 100; i++)
{
mgdi.Update(new TValue(DateTime.UtcNow, 50.0));
}
Assert.Equal(50.0, mgdi.Last.Value, 1e-6);
}
[Fact]
public void RisingPrices_MgdiFollowsBelow()
{
var mgdi = new Mgdi(14, 0.6);
double lastPrice = 0;
for (int i = 1; i <= 50; i++)
{
lastPrice = 100 + i;
mgdi.Update(new TValue(DateTime.UtcNow, lastPrice));
}
// MGDI is a lagging indicator - in a rising market it should be below price
Assert.True(mgdi.Last.Value < lastPrice,
$"MGDI {mgdi.Last.Value} should lag below price {lastPrice}");
}
// ── Bar Correction ──────────────────────────────────────────────────
[Fact]
public void BarCorrection_RestoresState()
{
var mgdi = new Mgdi(14, 0.6);
var now = DateTime.UtcNow;
for (int i = 0; i < 20; i++)
{
mgdi.Update(new TValue(now.AddMinutes(i), 100 + i));
}
// New bar
var result1 = mgdi.Update(new TValue(now.AddMinutes(20), 200), isNew: true);
// Correction back to same value
mgdi.Update(new TValue(now.AddMinutes(20), 150), isNew: false);
mgdi.Update(new TValue(now.AddMinutes(20), 200), isNew: false);
var restored = mgdi.Last;
Assert.Equal(result1.Value, restored.Value, 1e-10);
}
// ── Reset ───────────────────────────────────────────────────────────
[Fact]
public void Reset_ClearsState()
{
var mgdi = new Mgdi(14, 0.6);
for (int i = 0; i < 20; i++)
{
mgdi.Update(new TValue(DateTime.UtcNow, 100 + i));
}
Assert.True(mgdi.IsHot);
mgdi.Reset();
Assert.False(mgdi.IsHot);
Assert.Equal(default, mgdi.Last);
}
[Fact]
public void Reset_AllowsReuse()
{
var mgdi = new Mgdi(14, 0.6);
for (int i = 0; i < 20; i++)
{
mgdi.Update(new TValue(DateTime.UtcNow, 100 + i));
}
var firstResult = mgdi.Last.Value;
mgdi.Reset();
for (int i = 0; i < 20; i++)
{
mgdi.Update(new TValue(DateTime.UtcNow, 100 + i));
}
Assert.Equal(firstResult, mgdi.Last.Value, 1e-10);
}
// ── Batch ───────────────────────────────────────────────────────────
[Fact]
public void Batch_TSeries_MatchesStreaming()
{
var data = _gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = data.Close;
var batchResult = Mgdi.Batch(series, 14, 0.6);
var streaming = new Mgdi(14, 0.6);
var streamingResults = new TSeries();
foreach (var item in series)
{
streamingResults.Add(streaming.Update(item));
}
for (int i = 0; i < series.Count; i++)
{
Assert.Equal(batchResult[i].Value, streamingResults[i].Value, 1e-10);
}
}
[Fact]
public void Batch_Span_MatchesStreaming()
{
var data = _gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var values = data.Close.Values.ToArray();
var spanOutput = new double[values.Length];
Mgdi.Batch(values, spanOutput, 14, 0.6);
var streaming = new Mgdi(14, 0.6);
for (int i = 0; i < values.Length; i++)
{
var result = streaming.Update(new TValue(DateTime.UtcNow, values[i]));
Assert.Equal(result.Value, spanOutput[i], 1e-10);
}
}
[Fact]
public void Batch_Span_MismatchedLength_Throws()
{
var source = new double[10];
var output = new double[10];
var output = new double[5];
Assert.Throws<ArgumentException>(() => Mgdi.Batch(source, output, 14, 0.6));
}
Assert.Throws<ArgumentOutOfRangeException>(() => Mgdi.Batch(source, output, 14, double.NaN));
Assert.Throws<ArgumentOutOfRangeException>(() => Mgdi.Batch(source, output, 14, double.PositiveInfinity));
Assert.Throws<ArgumentOutOfRangeException>(() => Mgdi.Batch(source, output, 14, double.NegativeInfinity));
Assert.Throws<ArgumentOutOfRangeException>(() => Mgdi.Batch(source, output, 14, 0));
Assert.Throws<ArgumentOutOfRangeException>(() => Mgdi.Batch(source, output, 14, -1));
[Fact]
public void Batch_Span_Empty_NoThrow()
{
var source = Array.Empty<double>();
var output = Array.Empty<double>();
Mgdi.Batch(source, output, 14, 0.6); // Should not throw
Assert.True(true); // S2699: explicit assertion for no-throw test
}
[Fact]
public void Update_EmptyTSeries_ReturnsEmpty()
{
var mgdi = new Mgdi(14, 0.6);
var result = mgdi.Update(new TSeries());
Assert.Empty(result);
}
// ── Calculate ───────────────────────────────────────────────────────
[Fact]
public void Calculate_ReturnsResultsAndIndicator()
{
var data = _gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var (results, indicator) = Mgdi.Calculate(data.Close, 14, 0.6);
Assert.Equal(50, results.Count);
Assert.True(indicator.IsHot);
}
// ── Prime ───────────────────────────────────────────────────────────
[Fact]
public void Prime_WarmsUpIndicator()
{
var mgdi = new Mgdi(14, 0.6);
var values = new double[20];
for (int i = 0; i < 20; i++)
{
values[i] = 100 + i;
}
mgdi.Prime(values);
Assert.True(mgdi.IsHot);
}
}
+24
View File
@@ -34,6 +34,11 @@ public sealed class Adl : ITValuePublisher
/// </summary>
public TValue Last { get; private set; }
/// <summary>
/// Minimum number of data points required before the indicator becomes valid.
/// </summary>
public int WarmupPeriod { get; } = 1;
/// <summary>
/// True if the indicator has processed at least one bar.
/// </summary>
@@ -121,6 +126,25 @@ public sealed class Adl : ITValuePublisher
return new TSeries(t, v);
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
public static TSeries Batch(TBarSeries source)
{
if (source.Count == 0)
+19
View File
@@ -139,6 +139,25 @@ public sealed class Adosc : ITValuePublisher
// EMA compensator threshold (same as in Ema.cs)
private const double COMPENSATOR_THRESHOLD = 1e-10;
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
/// <summary>
/// Calculates ADOSC for the entire series using a new instance.
/// </summary>
+19
View File
@@ -256,6 +256,25 @@ public sealed class Aobv : ITValuePublisher
return (new TSeries(tFast, vFast), new TSeries(tSlow, vSlow));
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
public static (TSeries Fast, TSeries Slow) Calculate(TBarSeries source)
{
if (source.Count == 0)
+19
View File
@@ -174,6 +174,25 @@ public sealed class Cmf : ITValuePublisher
return new TSeries(t, v);
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
public static TSeries Batch(TBarSeries source, int period = 20)
{
if (source.Count == 0)
+19
View File
@@ -225,6 +225,25 @@ public sealed class Efi : ITValuePublisher
return new TSeries(t, v);
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
public static TSeries Batch(TBarSeries source, int period = 13)
{
if (source.Count == 0)
+19
View File
@@ -201,6 +201,25 @@ public sealed class Eom : ITValuePublisher
Last = default;
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
/// <summary>
/// Calculates EOM for a series of bars.
/// </summary>
+19
View File
@@ -204,6 +204,25 @@ public sealed class Iii : ITValuePublisher
Last = default;
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
/// <summary>
/// Calculates III for a series of bars.
/// </summary>
+19
View File
@@ -295,6 +295,25 @@ public sealed class Kvo : ITValuePublisher
Signal = default;
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
/// <summary>
/// Calculates KVO for a series of bars.
/// </summary>
+19
View File
@@ -212,6 +212,25 @@ public sealed class Mfi : ITValuePublisher
return new TSeries(t, v);
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
public static TSeries Batch(TBarSeries source, int period = 14)
{
if (source.Count == 0)
+19
View File
@@ -181,6 +181,25 @@ public sealed class Nvi : ITValuePublisher
return new TSeries(t, v);
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
public static TSeries Batch(TBarSeries source, double startValue = 100.0)
{
if (source.Count == 0)
+19
View File
@@ -175,6 +175,25 @@ public sealed class Obv : ITValuePublisher
return new TSeries(t, v);
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
public static TSeries Batch(TBarSeries source)
{
if (source.Count == 0)
+19
View File
@@ -250,6 +250,25 @@ public sealed class Pvd : ITValuePublisher
return new TSeries(t, v);
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
public static TSeries Batch(TBarSeries source, int pricePeriod = 14, int volumePeriod = 14, int smoothingPeriod = 3)
{
if (source.Count == 0)
+19
View File
@@ -188,6 +188,25 @@ public sealed class Pvi : ITValuePublisher
return new TSeries(t, v);
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
public static TSeries Batch(TBarSeries source, double startValue = 100.0)
{
if (source.Count == 0)
+19
View File
@@ -260,6 +260,25 @@ public sealed class Pvo : ITValuePublisher
Histogram = default;
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
/// <summary>
/// Calculates PVO for a series of bars.
/// </summary>
+19
View File
@@ -169,6 +169,25 @@ public sealed class Pvr : ITValuePublisher
IsHot = false;
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
/// <summary>
/// Calculates PVR for a series of bars.
/// </summary>
+19
View File
@@ -230,6 +230,25 @@ public sealed class Pvt : ITValuePublisher
return new TSeries(t, v);
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
public static TSeries Batch(TBarSeries source)
{
if (source.Count == 0)
+19
View File
@@ -207,6 +207,25 @@ public sealed class Tvi : ITValuePublisher
return new TSeries(t, v);
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
public static TSeries Batch(TBarSeries source, double minTick = 0.125)
{
if (source.Count == 0)
+19
View File
@@ -218,6 +218,25 @@ public sealed class Twap : ITValuePublisher
return result;
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
/// <summary>
/// Calculates TWAP for a series of bars (static batch mode).
/// </summary>
+19
View File
@@ -179,6 +179,25 @@ public sealed class Va : ITValuePublisher
return new TSeries(t, v);
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
/// <summary>
/// Calculates VA for a series of bars (static batch mode).
/// </summary>
+19
View File
@@ -196,6 +196,25 @@ public sealed class Vf : ITValuePublisher
return new TSeries(t, v);
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
/// <summary>
/// Calculates VF for a series of bars (static batch mode).
/// </summary>
+19
View File
@@ -271,6 +271,25 @@ public sealed class Vo : ITValuePublisher
return new TSeries(t, v);
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
/// <summary>
/// Calculates VO for a series of bars (static batch mode).
/// </summary>
+19
View File
@@ -211,6 +211,25 @@ public sealed class Vroc : ITValuePublisher
return new TSeries(t, v);
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
/// <summary>
/// Calculates VROC for a series of bars (static batch mode).
/// </summary>
+19
View File
@@ -223,6 +223,25 @@ public sealed class Vwad : ITValuePublisher
return new TSeries(t, v);
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
/// <summary>
/// Static calculation returning TSeries.
/// </summary>
+19
View File
@@ -214,6 +214,25 @@ public sealed class Vwap : ITValuePublisher
return new TSeries(t, v);
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
/// <summary>
/// Static calculation returning TSeries.
/// </summary>
+19
View File
@@ -269,6 +269,25 @@ public sealed class Vwma : ITValuePublisher
return Last;
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
/// <summary>
/// Static calculation returning TSeries.
/// </summary>
+24
View File
@@ -36,6 +36,11 @@ public sealed class Wad : ITValuePublisher
/// </summary>
public TValue Last { get; private set; }
/// <summary>
/// Minimum number of data points required before the indicator becomes valid.
/// </summary>
public int WarmupPeriod { get; } = 1;
/// <summary>
/// True if the indicator has processed at least one bar.
/// </summary>
@@ -159,6 +164,25 @@ public sealed class Wad : ITValuePublisher
return new TSeries(t, v);
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
public static TSeries Batch(TBarSeries source)
{
if (source.Count == 0)
+3 -3
View File
@@ -159,7 +159,7 @@ public class IndicatorBenchmarks
// ==================== ADOSC ====================
[BenchmarkCategory("ADOSC")]
[Benchmark(Description = "QuanTAlib ADOSC (Span)")]
public void QuanTAlib_Adosc_Span() => Adosc.Calculate(_highValues.AsSpan(), _lowValues.AsSpan(), _closeValues.AsSpan(), _volumeValues.AsSpan(), _quantalibOutput.AsSpan(), 3, 10);
public void QuanTAlib_Adosc_Span() => Adosc.Batch(_highValues.AsSpan(), _lowValues.AsSpan(), _closeValues.AsSpan(), _volumeValues.AsSpan(), _quantalibOutput.AsSpan(), 3, 10);
[BenchmarkCategory("ADOSC")]
[Benchmark(Description = "QuanTAlib ADOSC (Batch)")]
@@ -342,7 +342,7 @@ public class IndicatorBenchmarks
// ==================== HMA ====================
[BenchmarkCategory("HMA")]
[Benchmark(Description = "QuanTAlib HMA (Span)")]
public void QuanTAlib_Hma_Span() => Hma.Calculate(_closeValues.AsSpan(), _quantalibOutput.AsSpan(), Period);
public void QuanTAlib_Hma_Span() => Hma.Batch(_closeValues.AsSpan(), _quantalibOutput.AsSpan(), Period);
[BenchmarkCategory("HMA")]
[Benchmark(Description = "QuanTAlib HMA (Batch)")]
@@ -391,7 +391,7 @@ public class IndicatorBenchmarks
[BenchmarkCategory("SKEW")]
[Benchmark(Description = "QuanTAlib Skew (Batch)")]
public TSeries QuanTAlib_Skew_TSeries() => Skew.Calculate(_closeTseries, Period);
public TSeries QuanTAlib_Skew_TSeries() => Skew.Batch(_closeTseries, Period);
[BenchmarkCategory("SKEW")]
[Benchmark(Description = "QuanTAlib Skew (Streaming)")]