mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-07-27 17:27:43 +00:00
feat: Add Prime method to various indicators for initializing state with historical data
- Implemented Prime method in Vel, Ao, Apo, Frama, Adl, Adosc, Aobv, Cmf, Efi, Eom, Iii, Kvo, Mfi, Nvi, Obv, Pvd, Pvi, Pvo, Pvr, Pvt, Tvi, Twap, Va, Vf, Vo, Vroc, Vwad, Vwap, and Vwma classes. - The Prime method resets the indicator state and processes the provided historical bar data to initialize the indicator. - Added warmup period property to Adl and Wad classes to define the minimum number of data points required for validity. - Updated benchmark tests to use Batch methods for performance evaluation.
This commit is contained in:
+2
-3
@@ -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
|
||||
}
|
||||
|
||||
@@ -549,4 +549,355 @@ public class ApchannelTests
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Default Constructor
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultAlpha_UsesPointTwo()
|
||||
{
|
||||
var apc = new Apchannel(); // default alpha = 0.2
|
||||
Assert.Equal(15, apc.WarmupPeriod); // ceil(3.0 / 0.2) = 15
|
||||
Assert.Contains("0.20", apc.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Dispose Tests
|
||||
|
||||
[Fact]
|
||||
public void Dispose_UnsubscribesFromSource()
|
||||
{
|
||||
var source = new TBarSeries();
|
||||
var apc = new Apchannel(source, 0.2);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Verify subscription works
|
||||
source.Add(new TBar(time, 100, 105, 95, 100, 1000));
|
||||
Assert.NotEqual(0, apc.Last.Value);
|
||||
|
||||
double valueBeforeDispose = apc.Last.Value;
|
||||
|
||||
// Dispose should unsubscribe
|
||||
apc.Dispose();
|
||||
|
||||
// Adding to source after dispose should NOT update the indicator
|
||||
source.Add(new TBar(time.AddMinutes(1), 200, 210, 190, 200, 5000));
|
||||
Assert.Equal(valueBeforeDispose, apc.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_DoubleDispose_DoesNotThrow()
|
||||
{
|
||||
var source = new TBarSeries();
|
||||
var apc = new Apchannel(source, 0.2);
|
||||
|
||||
apc.Dispose();
|
||||
var ex = Record.Exception(() => apc.Dispose());
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_WithoutSource_DoesNotThrow()
|
||||
{
|
||||
var apc = new Apchannel(0.2);
|
||||
var ex = Record.Exception(() => apc.Dispose());
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Update(TBarSeries) Tests
|
||||
|
||||
[Fact]
|
||||
public void UpdateTBarSeries_ReturnsTSeries()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
var bars = gbm.Fetch(30, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var apc = new Apchannel(0.2);
|
||||
var results = apc.Update(bars);
|
||||
|
||||
Assert.Equal(30, results.Count);
|
||||
Assert.True(apc.IsHot); // 30 > WarmupPeriod(15)
|
||||
|
||||
// Verify all results are finite
|
||||
for (int i = 0; i < results.Count; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(results[i].Value));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateTBarSeries_MatchesIterative()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Batch via Update(TBarSeries)
|
||||
var apcBatch = new Apchannel(0.3);
|
||||
var batchResults = apcBatch.Update(bars);
|
||||
|
||||
// Iterative — fresh instance so both start from same state
|
||||
var apcIter = new Apchannel(0.3);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var val = apcIter.Add(bars[i]);
|
||||
Assert.Equal(val.Value, batchResults[i].Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateTBarSeries_EmptySource_ReturnsEmpty()
|
||||
{
|
||||
var apc = new Apchannel(0.2);
|
||||
var emptyBars = new TBarSeries();
|
||||
|
||||
var results = apc.Update(emptyBars);
|
||||
|
||||
Assert.Empty(results);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Update(TValue) Tests
|
||||
|
||||
[Fact]
|
||||
public void UpdateTValue_UsesValueForBothHighAndLow()
|
||||
{
|
||||
var apc = new Apchannel(0.5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// When using TValue, value is used for both high and low
|
||||
var result = apc.Update(new TValue(time.Ticks, 100.0));
|
||||
|
||||
// Upper and lower bands should equal the value (first bar)
|
||||
Assert.Equal(100.0, apc.UpperBand, Tolerance);
|
||||
Assert.Equal(100.0, apc.LowerBand, Tolerance);
|
||||
Assert.Equal(100.0, result.Value, Tolerance); // mid = (100+100)/2
|
||||
|
||||
// Second value
|
||||
var result2 = apc.Update(new TValue(time.AddMinutes(1).Ticks, 110.0));
|
||||
|
||||
// EMA: 0.5 * 100 + 0.5 * 110 = 105
|
||||
Assert.Equal(105.0, apc.UpperBand, Tolerance);
|
||||
Assert.Equal(105.0, apc.LowerBand, Tolerance);
|
||||
Assert.Equal(105.0, result2.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateTValue_IsNew_False_RestoresState()
|
||||
{
|
||||
var apc = new Apchannel(0.2);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
apc.Update(new TValue(time.Ticks, 100.0), isNew: true);
|
||||
double valueAfterOne = apc.Last.Value;
|
||||
|
||||
apc.Update(new TValue(time.AddMinutes(1).Ticks, 110.0), isNew: true);
|
||||
double valueAfterTwo = apc.Last.Value;
|
||||
Assert.NotEqual(valueAfterOne, valueAfterTwo);
|
||||
|
||||
// Correction with same value restores prior state then reapplies
|
||||
apc.Update(new TValue(time.AddMinutes(1).Ticks, 110.0), isNew: false);
|
||||
double valueAfterSameCorrection = apc.Last.Value;
|
||||
|
||||
// Correction with different value produces different result
|
||||
apc.Update(new TValue(time.AddMinutes(1).Ticks, 120.0), isNew: false);
|
||||
double valueAfterDifferent = apc.Last.Value;
|
||||
|
||||
Assert.NotEqual(valueAfterSameCorrection, valueAfterDifferent);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Update(TSeries) Tests
|
||||
|
||||
[Fact]
|
||||
public void UpdateTSeries_ReturnsTSeries()
|
||||
{
|
||||
var times = new List<long>();
|
||||
var values = new List<double>();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
times.Add(time.AddMinutes(i).Ticks);
|
||||
values.Add(100.0 + i);
|
||||
}
|
||||
|
||||
var source = new TSeries(times, values);
|
||||
var apc = new Apchannel(0.2);
|
||||
var results = apc.Update(source);
|
||||
|
||||
Assert.Equal(30, results.Count);
|
||||
Assert.True(apc.IsHot);
|
||||
|
||||
for (int i = 0; i < results.Count; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(results[i].Value));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateTSeries_EmptySource_ReturnsEmpty()
|
||||
{
|
||||
var apc = new Apchannel(0.2);
|
||||
var emptySource = new TSeries([], []);
|
||||
|
||||
var results = apc.Update(emptySource);
|
||||
|
||||
Assert.Empty(results);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateTSeries_MatchesUpdateTValue()
|
||||
{
|
||||
var times = new List<long>();
|
||||
var values = new List<double>();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
times.Add(time.AddMinutes(i).Ticks);
|
||||
values.Add(100.0 + (i * 2.5));
|
||||
}
|
||||
|
||||
var source = new TSeries(times, values);
|
||||
|
||||
// Batch via Update(TSeries)
|
||||
var apcBatch = new Apchannel(0.3);
|
||||
var batchResults = apcBatch.Update(source);
|
||||
|
||||
// Iterative via Update(TValue)
|
||||
var apcIter = new Apchannel(0.3);
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
var val = apcIter.Update(source[i], isNew: true);
|
||||
Assert.Equal(val.Value, batchResults[i].Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Prime Tests
|
||||
|
||||
[Fact]
|
||||
public void Prime_SetsIndicatorToHot()
|
||||
{
|
||||
var apc = new Apchannel(0.2); // WarmupPeriod = 15
|
||||
|
||||
double[] data = new double[20];
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
data[i] = 100.0 + i;
|
||||
}
|
||||
|
||||
Assert.False(apc.IsHot);
|
||||
|
||||
apc.Prime(data);
|
||||
|
||||
Assert.True(apc.IsHot);
|
||||
Assert.True(double.IsFinite(apc.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prime_EmptySpan_DoesNotThrow()
|
||||
{
|
||||
var apc = new Apchannel(0.2);
|
||||
var ex = Record.Exception(() => apc.Prime(ReadOnlySpan<double>.Empty));
|
||||
Assert.Null(ex);
|
||||
Assert.False(apc.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prime_ResetsBeforeProcessing()
|
||||
{
|
||||
var apc = new Apchannel(0.5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Feed some bars first
|
||||
apc.Add(new TBar(time, 200, 210, 190, 200, 1000));
|
||||
apc.Add(new TBar(time.AddMinutes(1), 205, 215, 195, 205, 1000));
|
||||
|
||||
double[] primeData = [100, 101, 102, 103, 104, 105, 106, 107, 108, 109];
|
||||
apc.Prime(primeData);
|
||||
|
||||
// After Prime, upper=lower (since TValue uses same value for both)
|
||||
// and bands should track primeData, not the old bars
|
||||
Assert.Equal(apc.UpperBand, apc.LowerBand, Tolerance);
|
||||
Assert.True(apc.Last.Value < 150); // Should be near primeData values, not 200
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prime_WithStep_UsesCorrectTimeSpacing()
|
||||
{
|
||||
var apc = new Apchannel(0.2);
|
||||
double[] data = [100, 102, 104, 106, 108];
|
||||
var step = TimeSpan.FromHours(1);
|
||||
|
||||
apc.Prime(data, step);
|
||||
|
||||
Assert.True(double.IsFinite(apc.Last.Value));
|
||||
// Verify that the time in Last reflects the step spacing
|
||||
Assert.True(apc.Last.Time > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prime_ThenUpdate_ContinuesCorrectly()
|
||||
{
|
||||
var apc = new Apchannel(0.2);
|
||||
|
||||
// Prime with 20 values to reach IsHot
|
||||
double[] primeData = new double[20];
|
||||
for (int i = 0; i < primeData.Length; i++)
|
||||
{
|
||||
primeData[i] = 100.0 + i;
|
||||
}
|
||||
|
||||
apc.Prime(primeData);
|
||||
Assert.True(apc.IsHot);
|
||||
|
||||
double valueAfterPrime = apc.Last.Value;
|
||||
|
||||
// Continue with Update — should build on primed state
|
||||
var time = DateTime.UtcNow;
|
||||
apc.Add(new TBar(time, 125, 130, 120, 125, 1000));
|
||||
|
||||
Assert.NotEqual(valueAfterPrime, apc.Last.Value);
|
||||
Assert.True(double.IsFinite(apc.Last.Value));
|
||||
Assert.True(double.IsFinite(apc.UpperBand));
|
||||
Assert.True(double.IsFinite(apc.LowerBand));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Batch(Span) Edge Cases
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_EmptyArrays_DoesNotThrow()
|
||||
{
|
||||
double[] high = [];
|
||||
double[] low = [];
|
||||
double[] upper = [];
|
||||
double[] lower = [];
|
||||
|
||||
var ex = Record.Exception(() => Apchannel.Batch(high, low, upper, lower, 0.2));
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_SingleElement_ReturnsInputValues()
|
||||
{
|
||||
double[] high = [110];
|
||||
double[] low = [90];
|
||||
double[] upper = new double[1];
|
||||
double[] lower = new double[1];
|
||||
|
||||
Apchannel.Batch(high, low, upper, lower, 0.2);
|
||||
|
||||
Assert.Equal(110, upper[0], Tolerance);
|
||||
Assert.Equal(90, lower[0], Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -318,4 +318,451 @@ public class BbandsTests
|
||||
Assert.Equal(batchResult[^1].Value, streamingBbands.Middle.Value, precision: 8);
|
||||
Assert.Equal(middleArray[^1], streamingBbands.Middle.Value, precision: 8);
|
||||
}
|
||||
|
||||
#region Default Constructor
|
||||
|
||||
[Fact]
|
||||
public void Bbands_Constructor_DefaultParameters()
|
||||
{
|
||||
// Default: period=20, multiplier=2.0
|
||||
Bbands bbands = new();
|
||||
|
||||
Assert.Equal("Bbands(20,2.0)", bbands.Name);
|
||||
Assert.Equal(20, bbands.WarmupPeriod);
|
||||
Assert.False(bbands.IsHot);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Prime Tests
|
||||
|
||||
[Fact]
|
||||
public void Bbands_Prime_SetsIndicatorToHot()
|
||||
{
|
||||
Bbands bbands = new(period: 5, multiplier: 2.0);
|
||||
|
||||
double[] data = [100, 101, 102, 103, 104, 105, 106, 107, 108, 109];
|
||||
|
||||
Assert.False(bbands.IsHot);
|
||||
|
||||
bbands.Prime(data);
|
||||
|
||||
Assert.True(bbands.IsHot);
|
||||
Assert.True(double.IsFinite(bbands.Middle.Value));
|
||||
Assert.True(double.IsFinite(bbands.Upper.Value));
|
||||
Assert.True(double.IsFinite(bbands.Lower.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bbands_Prime_EmptySpan_DoesNotThrow()
|
||||
{
|
||||
Bbands bbands = new(period: 5, multiplier: 2.0);
|
||||
var ex = Record.Exception(() => bbands.Prime(ReadOnlySpan<double>.Empty));
|
||||
Assert.Null(ex);
|
||||
Assert.False(bbands.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bbands_Prime_WithStep_UsesCorrectSpacing()
|
||||
{
|
||||
Bbands bbands = new(period: 3, multiplier: 2.0);
|
||||
double[] data = [100, 102, 104, 106, 108];
|
||||
var step = TimeSpan.FromHours(1);
|
||||
|
||||
bbands.Prime(data, step);
|
||||
|
||||
Assert.True(bbands.IsHot);
|
||||
Assert.True(double.IsFinite(bbands.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bbands_Prime_ThenUpdate_ContinuesCorrectly()
|
||||
{
|
||||
Bbands bbands = new(period: 5, multiplier: 2.0);
|
||||
|
||||
double[] primeData = [100, 101, 102, 103, 104, 105, 106, 107, 108, 109];
|
||||
bbands.Prime(primeData);
|
||||
Assert.True(bbands.IsHot);
|
||||
|
||||
double middleAfterPrime = bbands.Middle.Value;
|
||||
|
||||
// Continue with streaming
|
||||
bbands.Update(new TValue(DateTime.UtcNow, 120.0), isNew: true);
|
||||
|
||||
Assert.NotEqual(middleAfterPrime, bbands.Middle.Value);
|
||||
Assert.True(double.IsFinite(bbands.Middle.Value));
|
||||
Assert.True(double.IsFinite(bbands.Upper.Value));
|
||||
Assert.True(double.IsFinite(bbands.Lower.Value));
|
||||
Assert.True(bbands.Upper.Value > bbands.Middle.Value);
|
||||
Assert.True(bbands.Lower.Value < bbands.Middle.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bbands_Prime_MatchesStreamingResults()
|
||||
{
|
||||
double[] data = [100, 102, 98, 105, 103, 107, 101, 99, 106, 104];
|
||||
|
||||
// Via Prime
|
||||
Bbands primedBbands = new(period: 5, multiplier: 2.0);
|
||||
primedBbands.Prime(data);
|
||||
|
||||
// Via streaming Update
|
||||
Bbands streamBbands = new(period: 5, multiplier: 2.0);
|
||||
DateTime startTime = DateTime.UtcNow;
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
streamBbands.Update(new TValue(startTime + i * TimeSpan.FromSeconds(1), data[i]), isNew: true);
|
||||
}
|
||||
|
||||
Assert.Equal(streamBbands.Middle.Value, primedBbands.Middle.Value, precision: 10);
|
||||
Assert.Equal(streamBbands.Upper.Value, primedBbands.Upper.Value, precision: 10);
|
||||
Assert.Equal(streamBbands.Lower.Value, primedBbands.Lower.Value, precision: 10);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Calculate Tests
|
||||
|
||||
[Fact]
|
||||
public void Bbands_Calculate_ReturnsResultsAndHotIndicator()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
TSeries source = bars.Close;
|
||||
|
||||
var (results, indicator) = Bbands.Calculate(source, period: 5, multiplier: 2.0);
|
||||
|
||||
// Check results
|
||||
Assert.Equal(50, results.Count);
|
||||
|
||||
// Check indicator is hot and has valid state
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.True(double.IsFinite(indicator.Middle.Value));
|
||||
Assert.True(double.IsFinite(indicator.Upper.Value));
|
||||
Assert.True(double.IsFinite(indicator.Lower.Value));
|
||||
|
||||
// Verify indicator can continue streaming
|
||||
indicator.Update(new TValue(DateTime.UtcNow, 110.0), isNew: true);
|
||||
Assert.True(double.IsFinite(indicator.Middle.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bbands_Calculate_DefaultParameters()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
var bars = gbm.Fetch(30, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
TSeries source = bars.Close;
|
||||
|
||||
var (results, indicator) = Bbands.Calculate(source);
|
||||
|
||||
Assert.Equal(30, results.Count);
|
||||
Assert.Equal(20, indicator.WarmupPeriod);
|
||||
Assert.True(indicator.IsHot); // 30 > 20
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Update(TSeries) Edge Cases
|
||||
|
||||
[Fact]
|
||||
public void Bbands_UpdateTSeries_NullSource_ThrowsArgumentNullException()
|
||||
{
|
||||
Bbands bbands = new(period: 5, multiplier: 2.0);
|
||||
|
||||
Assert.Throws<ArgumentNullException>(() => bbands.Update((TSeries)null!));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Infinity Handling
|
||||
|
||||
[Fact]
|
||||
public void Bbands_Infinity_HandledGracefully()
|
||||
{
|
||||
Bbands bbands = new(period: 3, multiplier: 2.0);
|
||||
DateTime time = DateTime.UtcNow;
|
||||
|
||||
bbands.Update(new TValue(time, 10.0), isNew: true);
|
||||
bbands.Update(new TValue(time.AddSeconds(1), 12.0), isNew: true);
|
||||
|
||||
// PositiveInfinity should use last valid value
|
||||
bbands.Update(new TValue(time.AddSeconds(2), double.PositiveInfinity), isNew: true);
|
||||
Assert.True(double.IsFinite(bbands.Middle.Value));
|
||||
Assert.True(double.IsFinite(bbands.Upper.Value));
|
||||
Assert.True(double.IsFinite(bbands.Lower.Value));
|
||||
|
||||
// NegativeInfinity should also be handled
|
||||
bbands.Update(new TValue(time.AddSeconds(3), double.NegativeInfinity), isNew: true);
|
||||
Assert.True(double.IsFinite(bbands.Middle.Value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PercentB Edge Cases
|
||||
|
||||
[Fact]
|
||||
public void Bbands_PercentB_ZeroWidth_ReturnsZero()
|
||||
{
|
||||
// When all values are the same, stddev = 0, width = 0, percentB should be 0
|
||||
Bbands bbands = new(period: 3, multiplier: 2.0);
|
||||
DateTime time = DateTime.UtcNow;
|
||||
|
||||
bbands.Update(new TValue(time, 100.0), isNew: true);
|
||||
bbands.Update(new TValue(time.AddSeconds(1), 100.0), isNew: true);
|
||||
bbands.Update(new TValue(time.AddSeconds(2), 100.0), isNew: true);
|
||||
|
||||
Assert.Equal(0.0, bbands.Width.Value, precision: 10);
|
||||
Assert.Equal(0.0, bbands.PercentB.Value, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bbands_PercentB_AtMiddle_IsFiftyPercent()
|
||||
{
|
||||
// When price equals the middle band, %B should be ≈ 0.5
|
||||
Bbands bbands = new(period: 3, multiplier: 2.0);
|
||||
DateTime time = DateTime.UtcNow;
|
||||
|
||||
bbands.Update(new TValue(time, 10.0), isNew: true);
|
||||
bbands.Update(new TValue(time.AddSeconds(1), 12.0), isNew: true);
|
||||
bbands.Update(new TValue(time.AddSeconds(2), 14.0), isNew: true);
|
||||
|
||||
// SMA = 12.0, feeding 12.0 next — it becomes middle of [12, 14, 12] = SMA ≈ 12.67
|
||||
// Need to check the actual calculation rather than assume
|
||||
// The point: when price = middle, %B = (price - lower) / (upper - lower)
|
||||
// which would be 0.5 since middle is equidistant from upper and lower
|
||||
bbands.Update(new TValue(time.AddSeconds(3), bbands.Middle.Value), isNew: true);
|
||||
// After this update, the SMA shifts, but %B should be ≈ 0.5
|
||||
Assert.True(bbands.PercentB.Value > 0.3 && bbands.PercentB.Value < 0.7);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reset State Tests
|
||||
|
||||
[Fact]
|
||||
public void Bbands_Reset_ClearsAllProperties()
|
||||
{
|
||||
Bbands bbands = new(period: 3, multiplier: 2.0);
|
||||
DateTime time = DateTime.UtcNow;
|
||||
|
||||
bbands.Update(new TValue(time, 10.0));
|
||||
bbands.Update(new TValue(time.AddSeconds(1), 12.0));
|
||||
bbands.Update(new TValue(time.AddSeconds(2), 14.0));
|
||||
|
||||
Assert.True(bbands.IsHot);
|
||||
Assert.NotEqual(0, bbands.Middle.Value);
|
||||
Assert.NotEqual(0, bbands.Upper.Value);
|
||||
|
||||
bbands.Reset();
|
||||
|
||||
Assert.False(bbands.IsHot);
|
||||
Assert.Equal(0, bbands.Middle.Value);
|
||||
Assert.Equal(0, bbands.Upper.Value);
|
||||
Assert.Equal(0, bbands.Lower.Value);
|
||||
Assert.Equal(0, bbands.Width.Value);
|
||||
Assert.Equal(0, bbands.PercentB.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bbands_Reset_ThenReuse_ProducesSameResults()
|
||||
{
|
||||
Bbands bbands = new(period: 3, multiplier: 2.0);
|
||||
DateTime time = DateTime.UtcNow;
|
||||
double[] prices = [10.0, 12.0, 14.0];
|
||||
|
||||
// First pass
|
||||
for (int i = 0; i < prices.Length; i++)
|
||||
{
|
||||
bbands.Update(new TValue(time.AddSeconds(i), prices[i]));
|
||||
}
|
||||
double firstMiddle = bbands.Middle.Value;
|
||||
double firstUpper = bbands.Upper.Value;
|
||||
double firstLower = bbands.Lower.Value;
|
||||
|
||||
// Reset and second pass with same data
|
||||
bbands.Reset();
|
||||
for (int i = 0; i < prices.Length; i++)
|
||||
{
|
||||
bbands.Update(new TValue(time.AddSeconds(i), prices[i]));
|
||||
}
|
||||
|
||||
Assert.Equal(firstMiddle, bbands.Middle.Value, precision: 10);
|
||||
Assert.Equal(firstUpper, bbands.Upper.Value, precision: 10);
|
||||
Assert.Equal(firstLower, bbands.Lower.Value, precision: 10);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Span Batch Edge Cases
|
||||
|
||||
[Fact]
|
||||
public void Bbands_SpanBatch_EmptyArrays_DoesNotThrow()
|
||||
{
|
||||
double[] source = [];
|
||||
double[] middle = [];
|
||||
double[] upper = [];
|
||||
double[] lower = [];
|
||||
|
||||
var ex = Record.Exception(() => Bbands.Batch(
|
||||
source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan()));
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bbands_SpanBatch_InvalidPeriod_ThrowsArgumentOutOfRangeException()
|
||||
{
|
||||
double[] source = new double[10];
|
||||
double[] middle = new double[10];
|
||||
double[] upper = new double[10];
|
||||
double[] lower = new double[10];
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||
Bbands.Batch(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(),
|
||||
period: 1, multiplier: 2.0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bbands_SpanBatch_InvalidMultiplier_ThrowsArgumentOutOfRangeException()
|
||||
{
|
||||
double[] source = new double[10];
|
||||
double[] middle = new double[10];
|
||||
double[] upper = new double[10];
|
||||
double[] lower = new double[10];
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||
Bbands.Batch(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(),
|
||||
period: 5, multiplier: 0.05));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bbands_SpanBatch_ShorterThanPeriod_SetsNaN()
|
||||
{
|
||||
// Source shorter than period — all upper/lower should be NaN
|
||||
double[] source = [100, 101, 102];
|
||||
double[] middle = new double[3];
|
||||
double[] upper = new double[3];
|
||||
double[] lower = new double[3];
|
||||
|
||||
Bbands.Batch(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(),
|
||||
period: 5, multiplier: 2.0);
|
||||
|
||||
// First (period-1) values should be NaN for upper/lower
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
Assert.True(double.IsNaN(upper[i]));
|
||||
Assert.True(double.IsNaN(lower[i]));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bbands_SpanBatch_NaN_InWindow_EmitsNaN()
|
||||
{
|
||||
double[] source = [100, 101, double.NaN, 103, 104, 105, 106, 107, 108, 109];
|
||||
double[] middle = new double[10];
|
||||
double[] upper = new double[10];
|
||||
double[] lower = new double[10];
|
||||
|
||||
Bbands.Batch(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(),
|
||||
period: 5, multiplier: 2.0);
|
||||
|
||||
// Index 4 (first complete window [100,101,NaN,103,104]) contains NaN
|
||||
// So upper/lower at index 4 should be NaN
|
||||
Assert.True(double.IsNaN(upper[4]));
|
||||
Assert.True(double.IsNaN(lower[4]));
|
||||
|
||||
// Once NaN exits the window, values should become finite again
|
||||
// Window at index 7: [103, 104, 105, 106, 107] — all finite
|
||||
Assert.True(double.IsFinite(upper[7]));
|
||||
Assert.True(double.IsFinite(lower[7]));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Band Relationship Tests
|
||||
|
||||
[Fact]
|
||||
public void Bbands_UpperAlwaysAboveLower()
|
||||
{
|
||||
Bbands bbands = new(period: 5, multiplier: 2.0);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
DateTime time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
bbands.Update(new TValue(time.AddMinutes(i), bar.Close), isNew: true);
|
||||
|
||||
if (bbands.IsHot)
|
||||
{
|
||||
Assert.True(bbands.Upper.Value >= bbands.Lower.Value,
|
||||
$"Upper ({bbands.Upper.Value}) should be >= Lower ({bbands.Lower.Value}) at step {i}");
|
||||
Assert.True(bbands.Width.Value >= 0,
|
||||
$"Width ({bbands.Width.Value}) should be >= 0 at step {i}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bbands_MiddleIsBetweenBands()
|
||||
{
|
||||
Bbands bbands = new(period: 5, multiplier: 2.0);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
DateTime time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
bbands.Update(new TValue(time.AddMinutes(i), bar.Close), isNew: true);
|
||||
|
||||
if (bbands.IsHot)
|
||||
{
|
||||
Assert.True(bbands.Middle.Value >= bbands.Lower.Value,
|
||||
$"Middle ({bbands.Middle.Value}) should be >= Lower ({bbands.Lower.Value})");
|
||||
Assert.True(bbands.Middle.Value <= bbands.Upper.Value,
|
||||
$"Middle ({bbands.Middle.Value}) should be <= Upper ({bbands.Upper.Value})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bbands_MultiplierAffectsBandWidth()
|
||||
{
|
||||
DateTime time = DateTime.UtcNow;
|
||||
double[] prices = [100, 102, 98, 105, 103, 107, 101, 99, 106, 104];
|
||||
|
||||
Bbands narrow = new(period: 5, multiplier: 1.0);
|
||||
Bbands wide = new(period: 5, multiplier: 3.0);
|
||||
|
||||
for (int i = 0; i < prices.Length; i++)
|
||||
{
|
||||
narrow.Update(new TValue(time.AddSeconds(i), prices[i]), isNew: true);
|
||||
wide.Update(new TValue(time.AddSeconds(i), prices[i]), isNew: true);
|
||||
}
|
||||
|
||||
// Wider multiplier should produce wider bands
|
||||
Assert.True(wide.Width.Value > narrow.Width.Value);
|
||||
// Middle should be the same (same SMA)
|
||||
Assert.Equal(narrow.Middle.Value, wide.Middle.Value, precision: 10);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Last Property
|
||||
|
||||
[Fact]
|
||||
public void Bbands_Last_EqualsMiddle()
|
||||
{
|
||||
Bbands bbands = new(period: 3, multiplier: 2.0);
|
||||
DateTime time = DateTime.UtcNow;
|
||||
|
||||
bbands.Update(new TValue(time, 10.0));
|
||||
bbands.Update(new TValue(time.AddSeconds(1), 12.0));
|
||||
bbands.Update(new TValue(time.AddSeconds(2), 14.0));
|
||||
|
||||
// Last should be the Middle band value
|
||||
Assert.Equal(bbands.Middle.Value, bbands.Last.Value, precision: 10);
|
||||
Assert.Equal(bbands.Middle.Time, bbands.Last.Time);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ namespace QuanTAlib.Tests;
|
||||
|
||||
public class JbandsTests
|
||||
{
|
||||
#region Constructor Tests
|
||||
|
||||
[Fact]
|
||||
public void Jbands_Constructor_ValidatesInput()
|
||||
{
|
||||
@@ -16,6 +18,42 @@ public class JbandsTests
|
||||
Assert.True(j.WarmupPeriod > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jbands_Constructor_InfinityPower_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Jbands(14, 0, double.PositiveInfinity));
|
||||
Assert.Throws<ArgumentException>(() => new Jbands(14, 0, double.NegativeInfinity));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jbands_Period1_EdgeCase()
|
||||
{
|
||||
var j = new Jbands(1);
|
||||
Assert.True(j.WarmupPeriod > 0);
|
||||
|
||||
j.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.Equal(100.0, j.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jbands_ConstructorWithSource_ReceivesUpdates()
|
||||
{
|
||||
var source = new Sma(3);
|
||||
using var j = new Jbands(source, 14);
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
source.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(j.Last.Value));
|
||||
Assert.NotEqual(0, j.Last.Value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Initial State Tests
|
||||
|
||||
[Fact]
|
||||
public void Jbands_InitialState_Defaults()
|
||||
{
|
||||
@@ -27,6 +65,10 @@ public class JbandsTests
|
||||
Assert.False(j.IsHot);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region First Bar Tests
|
||||
|
||||
[Fact]
|
||||
public void Jbands_FirstBar_AllBandsEqual()
|
||||
{
|
||||
@@ -38,6 +80,21 @@ public class JbandsTests
|
||||
Assert.Equal(100.0, j.Lower.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jbands_NaN_FirstBar_ReturnsNaN()
|
||||
{
|
||||
var j = new Jbands(14);
|
||||
var result = j.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
|
||||
Assert.True(double.IsNaN(result.Value));
|
||||
Assert.True(double.IsNaN(j.Upper.Value));
|
||||
Assert.True(double.IsNaN(j.Lower.Value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Band Behavior Tests
|
||||
|
||||
[Fact]
|
||||
public void Jbands_UpperBand_SnapToNewHigh()
|
||||
{
|
||||
@@ -81,6 +138,43 @@ public class JbandsTests
|
||||
Assert.True(j.Upper.Value > 100.0); // But not below price yet
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jbands_UpperAlwaysAboveLower()
|
||||
{
|
||||
var j = new Jbands(14);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.2, seed: 77);
|
||||
|
||||
for (int i = 0; i < 500; i++)
|
||||
{
|
||||
j.Update(new TValue(DateTime.UtcNow, gbm.Next().Close), isNew: true);
|
||||
Assert.True(j.Upper.Value >= j.Lower.Value);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jbands_MiddleBand_IsSmoothed()
|
||||
{
|
||||
// JMA middle band is an IIR-smoothed value that can briefly
|
||||
// exceed the envelope bands during fast transitions. Verify
|
||||
// it converges close to price over time.
|
||||
var j = new Jbands(14);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.0, sigma: 0.01, seed: 55);
|
||||
|
||||
// Feed steady data — middle should stay within a reasonable range
|
||||
for (int i = 0; i < 500; i++)
|
||||
{
|
||||
j.Update(new TValue(DateTime.UtcNow, gbm.Next().Close), isNew: true);
|
||||
}
|
||||
|
||||
// After convergence with low-volatility data, middle should be close to price
|
||||
Assert.True(double.IsFinite(j.Last.Value));
|
||||
Assert.True(j.Upper.Value >= j.Lower.Value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IsHot / WarmupPeriod Tests
|
||||
|
||||
[Fact]
|
||||
public void Jbands_IsHot_TurnsTrueAfterWarmup()
|
||||
{
|
||||
@@ -98,6 +192,10 @@ public class JbandsTests
|
||||
Assert.True(j.IsHot);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region State Management Tests
|
||||
|
||||
[Fact]
|
||||
public void Jbands_IsNewFalse_RestoresState()
|
||||
{
|
||||
@@ -130,6 +228,10 @@ public class JbandsTests
|
||||
Assert.Equal(lo, j.Lower.Value, 1e-10);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region NaN / Infinity Handling Tests
|
||||
|
||||
[Fact]
|
||||
public void Jbands_NaN_UsesLastValid()
|
||||
{
|
||||
@@ -147,6 +249,10 @@ public class JbandsTests
|
||||
Assert.True(double.IsFinite(result2.Value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reset Tests
|
||||
|
||||
[Fact]
|
||||
public void Jbands_Reset_Clears()
|
||||
{
|
||||
@@ -167,6 +273,243 @@ public class JbandsTests
|
||||
Assert.Equal(50.0, j.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jbands_Reset_ThenReuse_ProducesSameResults()
|
||||
{
|
||||
var j = new Jbands(14, 0, 0.45);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.1, seed: 88);
|
||||
double[] prices = new double[100];
|
||||
for (int i = 0; i < prices.Length; i++)
|
||||
{
|
||||
prices[i] = gbm.Next().Close;
|
||||
}
|
||||
|
||||
// First pass
|
||||
for (int i = 0; i < prices.Length; i++)
|
||||
{
|
||||
j.Update(new TValue(DateTime.UtcNow.AddMinutes(i), prices[i]), isNew: true);
|
||||
}
|
||||
double midFirst = j.Last.Value;
|
||||
double upFirst = j.Upper.Value;
|
||||
double loFirst = j.Lower.Value;
|
||||
|
||||
// Reset and second pass
|
||||
j.Reset();
|
||||
for (int i = 0; i < prices.Length; i++)
|
||||
{
|
||||
j.Update(new TValue(DateTime.UtcNow.AddMinutes(i), prices[i]), isNew: true);
|
||||
}
|
||||
|
||||
Assert.Equal(midFirst, j.Last.Value, 1e-10);
|
||||
Assert.Equal(upFirst, j.Upper.Value, 1e-10);
|
||||
Assert.Equal(loFirst, j.Lower.Value, 1e-10);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Dispose Tests
|
||||
|
||||
[Fact]
|
||||
public void Jbands_Dispose_UnsubscribesFromSource()
|
||||
{
|
||||
var source = new Sma(3);
|
||||
var j = new Jbands(source, 14);
|
||||
|
||||
// Feed some data through source
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
source.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i));
|
||||
}
|
||||
double valueBeforeDispose = j.Last.Value;
|
||||
|
||||
// Dispose - should unsubscribe
|
||||
j.Dispose();
|
||||
|
||||
// Feed more data - Jbands should NOT update
|
||||
for (int i = 20; i < 40; i++)
|
||||
{
|
||||
source.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 200 + i));
|
||||
}
|
||||
|
||||
Assert.Equal(valueBeforeDispose, j.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jbands_Dispose_Idempotent()
|
||||
{
|
||||
var source = new Sma(3);
|
||||
var j = new Jbands(source, 14);
|
||||
|
||||
j.Dispose();
|
||||
j.Dispose(); // Should not throw
|
||||
|
||||
// Verify indicator is still in a valid state after double dispose
|
||||
Assert.True(double.IsFinite(j.Last.Value) || j.Last.Value == 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jbands_Dispose_WithoutSource_DoesNotThrow()
|
||||
{
|
||||
var j = new Jbands(14);
|
||||
j.Dispose(); // No source subscription, should not throw
|
||||
|
||||
Assert.Equal(0, j.Last.Value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Prime Tests
|
||||
|
||||
[Fact]
|
||||
public void Jbands_Prime_SetsIndicatorToHot()
|
||||
{
|
||||
var j = new Jbands(14);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 42);
|
||||
var series = new TSeries();
|
||||
|
||||
int count = j.WarmupPeriod + 10;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var bar = gbm.Next();
|
||||
series.Add(bar.Time, bar.Close);
|
||||
}
|
||||
|
||||
j.Prime(series);
|
||||
|
||||
Assert.True(j.IsHot);
|
||||
Assert.True(double.IsFinite(j.Last.Value));
|
||||
Assert.True(double.IsFinite(j.Upper.Value));
|
||||
Assert.True(double.IsFinite(j.Lower.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jbands_Prime_EmptySeries_DoesNotThrow()
|
||||
{
|
||||
var j = new Jbands(14);
|
||||
var empty = new TSeries();
|
||||
|
||||
j.Prime(empty); // Should not throw
|
||||
|
||||
Assert.False(j.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jbands_Prime_ThenUpdate_ContinuesCorrectly()
|
||||
{
|
||||
var j = new Jbands(14);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 42);
|
||||
var series = new TSeries();
|
||||
|
||||
int primeCount = j.WarmupPeriod + 5;
|
||||
for (int i = 0; i < primeCount; i++)
|
||||
{
|
||||
var bar = gbm.Next();
|
||||
series.Add(bar.Time, bar.Close);
|
||||
}
|
||||
|
||||
j.Prime(series);
|
||||
Assert.True(j.IsHot);
|
||||
|
||||
// Continue streaming
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var bar = gbm.Next();
|
||||
j.Update(new TValue(bar.Time, bar.Close), isNew: true);
|
||||
}
|
||||
|
||||
Assert.True(j.IsHot);
|
||||
Assert.True(double.IsFinite(j.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jbands_Prime_MatchesStreamingResults()
|
||||
{
|
||||
var jPrime = new Jbands(14, 0, 0.45);
|
||||
var jStream = new Jbands(14, 0, 0.45);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 42);
|
||||
|
||||
var series = new TSeries();
|
||||
int count = 100;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var bar = gbm.Next();
|
||||
series.Add(bar.Time, bar.Close);
|
||||
jStream.Update(new TValue(bar.Time, bar.Close), isNew: true);
|
||||
}
|
||||
|
||||
jPrime.Prime(series);
|
||||
|
||||
Assert.Equal(jStream.Last.Value, jPrime.Last.Value, 1e-10);
|
||||
Assert.Equal(jStream.Upper.Value, jPrime.Upper.Value, 1e-10);
|
||||
Assert.Equal(jStream.Lower.Value, jPrime.Lower.Value, 1e-10);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Calculate Tests
|
||||
|
||||
[Fact]
|
||||
public void Jbands_Calculate_ReturnsResultsAndHotIndicator()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 42);
|
||||
var series = new TSeries();
|
||||
for (int i = 0; i < 300; i++)
|
||||
{
|
||||
var bar = gbm.Next();
|
||||
series.Add(bar.Time, bar.Close);
|
||||
}
|
||||
|
||||
var (results, indicator) = Jbands.Calculate(series, 14, 0, 0.45);
|
||||
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.Equal(300, results.Middle.Count);
|
||||
Assert.Equal(300, results.Upper.Count);
|
||||
Assert.Equal(300, results.Lower.Count);
|
||||
Assert.True(double.IsFinite(indicator.Last.Value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Update(TSeries) Tests
|
||||
|
||||
[Fact]
|
||||
public void Jbands_UpdateTSeries_Direct()
|
||||
{
|
||||
var j = new Jbands(14);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 42);
|
||||
var series = new TSeries();
|
||||
for (int i = 0; i < 300; i++)
|
||||
{
|
||||
var bar = gbm.Next();
|
||||
series.Add(bar.Time, bar.Close);
|
||||
}
|
||||
|
||||
var (middle, upper, lower) = j.Update(series);
|
||||
|
||||
Assert.Equal(300, middle.Count);
|
||||
Assert.Equal(300, upper.Count);
|
||||
Assert.Equal(300, lower.Count);
|
||||
Assert.True(j.IsHot);
|
||||
Assert.True(double.IsFinite(j.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jbands_UpdateTSeries_EmptySeries_ReturnsEmptyTuples()
|
||||
{
|
||||
var j = new Jbands(14);
|
||||
var empty = new TSeries();
|
||||
|
||||
var (middle, upper, lower) = j.Update(empty);
|
||||
|
||||
Assert.Empty(middle);
|
||||
Assert.Empty(upper);
|
||||
Assert.Empty(lower);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Batch Tests
|
||||
|
||||
[Fact]
|
||||
public void Jbands_BatchVsStreaming_Match()
|
||||
{
|
||||
@@ -236,6 +579,24 @@ public class JbandsTests
|
||||
Assert.Equal(jStream.Lower.Value, lower[^1], 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jbands_SpanBatch_EmptySource_DoesNotThrow()
|
||||
{
|
||||
double[] source = [];
|
||||
double[] middle = [];
|
||||
double[] upper = [];
|
||||
double[] lower = [];
|
||||
|
||||
Jbands.Batch(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 14);
|
||||
|
||||
Assert.Empty(source);
|
||||
Assert.Empty(middle);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event / Chaining Tests
|
||||
|
||||
[Fact]
|
||||
public void Jbands_Event_Publishes()
|
||||
{
|
||||
@@ -265,6 +626,10 @@ public class JbandsTests
|
||||
Assert.True(double.IsFinite(downstream.Last.Value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Middle Band / JMA Tests
|
||||
|
||||
[Fact]
|
||||
public void Jbands_MiddleBand_MatchesJma()
|
||||
{
|
||||
@@ -284,6 +649,10 @@ public class JbandsTests
|
||||
Assert.Equal(jma.Last.Value, jbands.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Phase Parameter Tests
|
||||
|
||||
[Fact]
|
||||
public void Jbands_Phase_AffectsBehavior()
|
||||
{
|
||||
@@ -307,15 +676,44 @@ public class JbandsTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jbands_UpperAlwaysAboveLower()
|
||||
public void Jbands_Phase_ClampingBelowMinus100()
|
||||
{
|
||||
var j = new Jbands(14);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.2, seed: 77);
|
||||
// Phase < -100 should clamp phaseParam to 0.5
|
||||
var jClamped = new Jbands(14, -200);
|
||||
var jEdge = new Jbands(14, -100);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 42);
|
||||
|
||||
for (int i = 0; i < 500; i++)
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
j.Update(new TValue(DateTime.UtcNow, gbm.Next().Close), isNew: true);
|
||||
Assert.True(j.Upper.Value >= j.Lower.Value);
|
||||
double price = gbm.Next().Close;
|
||||
var tv = new TValue(DateTime.UtcNow, price);
|
||||
jClamped.Update(tv, isNew: true);
|
||||
jEdge.Update(tv, isNew: true);
|
||||
}
|
||||
|
||||
// Phase -200 should clamp to same as -100 (both → 0.5)
|
||||
Assert.Equal(jEdge.Last.Value, jClamped.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jbands_Phase_ClampingAbove100()
|
||||
{
|
||||
// Phase > 100 should clamp phaseParam to 2.5
|
||||
var jClamped = new Jbands(14, 200);
|
||||
var jEdge = new Jbands(14, 100);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 42);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double price = gbm.Next().Close;
|
||||
var tv = new TValue(DateTime.UtcNow, price);
|
||||
jClamped.Update(tv, isNew: true);
|
||||
jEdge.Update(tv, isNew: true);
|
||||
}
|
||||
|
||||
// Phase 200 should clamp to same as 100 (both → 2.5)
|
||||
Assert.Equal(jEdge.Last.Value, jClamped.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -425,6 +425,16 @@ public sealed class Jbands : ITValuePublisher, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates Jbands and returns both the results and the indicator instance.
|
||||
/// </summary>
|
||||
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Jbands Indicator) Calculate(TSeries source, int period, int phase = 0, double power = 0.45)
|
||||
{
|
||||
var indicator = new Jbands(period, phase, power);
|
||||
var results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double CalculateTrimmedMean(double fallback)
|
||||
{
|
||||
|
||||
@@ -4,23 +4,31 @@ namespace QuanTAlib.Tests;
|
||||
|
||||
public class StbandsTests
|
||||
{
|
||||
#region Constructor Tests
|
||||
|
||||
[Fact]
|
||||
public void Stbands_Constructor_ValidParameters()
|
||||
{
|
||||
// Arrange & Act
|
||||
Stbands stbands = new(period: 10, multiplier: 3.0);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(stbands);
|
||||
Assert.Equal("Stbands(10,3.0)", stbands.Name);
|
||||
Assert.Equal(10, stbands.WarmupPeriod);
|
||||
Assert.False(stbands.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stbands_Constructor_DefaultParameters()
|
||||
{
|
||||
Stbands stbands = new();
|
||||
|
||||
Assert.Equal("Stbands(10,3.0)", stbands.Name);
|
||||
Assert.Equal(10, stbands.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stbands_Constructor_InvalidPeriod_ThrowsArgumentOutOfRangeException()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
ArgumentOutOfRangeException exception = Assert.Throws<ArgumentOutOfRangeException>(
|
||||
() => new Stbands(period: 0));
|
||||
Assert.Equal("period", exception.ParamName);
|
||||
@@ -29,24 +37,30 @@ public class StbandsTests
|
||||
[Fact]
|
||||
public void Stbands_Constructor_InvalidMultiplier_ThrowsArgumentOutOfRangeException()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
ArgumentOutOfRangeException exception = Assert.Throws<ArgumentOutOfRangeException>(
|
||||
() => new Stbands(period: 10, multiplier: 0.0));
|
||||
Assert.Equal("multiplier", exception.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stbands_Constructor_NegativeMultiplier_ThrowsArgumentOutOfRangeException()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Stbands(period: 10, multiplier: -1.0));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Update TBar Tests
|
||||
|
||||
[Fact]
|
||||
public void Stbands_Update_TBar_ReturnsValue()
|
||||
{
|
||||
// Arrange
|
||||
Stbands stbands = new(period: 3, multiplier: 2.0);
|
||||
DateTime time = DateTime.UtcNow;
|
||||
|
||||
// Act
|
||||
TBar bar = new(time, 100, 105, 95, 102, 1000);
|
||||
TValue result = stbands.Update(bar);
|
||||
|
||||
// Assert
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.True(double.IsFinite(stbands.Upper.Value));
|
||||
Assert.True(double.IsFinite(stbands.Lower.Value));
|
||||
@@ -55,30 +69,29 @@ public class StbandsTests
|
||||
[Fact]
|
||||
public void Stbands_BandCalculations_CorrectValues()
|
||||
{
|
||||
// Arrange
|
||||
Stbands stbands = new(period: 3, multiplier: 2.0);
|
||||
DateTime time = DateTime.UtcNow;
|
||||
|
||||
// Act - Feed some bars
|
||||
stbands.Update(new TBar(time, 100, 105, 95, 102, 1000), isNew: true);
|
||||
stbands.Update(new TBar(time.AddMinutes(1), 102, 108, 100, 106, 1000), isNew: true);
|
||||
stbands.Update(new TBar(time.AddMinutes(2), 106, 110, 104, 108, 1000), isNew: true);
|
||||
|
||||
// Assert
|
||||
Assert.True(stbands.Upper.Value > stbands.Lower.Value);
|
||||
Assert.True(stbands.Width.Value > 0);
|
||||
Assert.True(stbands.Trend.Value == 1 || stbands.Trend.Value == -1);
|
||||
Assert.True(stbands.IsHot);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Band Behavior Tests
|
||||
|
||||
[Fact]
|
||||
public void Stbands_UpperBand_OnlyMovesDown_InDowntrend()
|
||||
{
|
||||
// Arrange
|
||||
Stbands stbands = new(period: 3, multiplier: 1.0);
|
||||
DateTime time = DateTime.UtcNow;
|
||||
|
||||
// Act - Create downtrend scenario
|
||||
stbands.Update(new TBar(time, 100, 105, 95, 100, 1000), isNew: true);
|
||||
double initialUpper = stbands.Upper.Value;
|
||||
|
||||
@@ -88,18 +101,15 @@ public class StbandsTests
|
||||
stbands.Update(new TBar(time.AddMinutes(2), 94, 98, 90, 92, 1000), isNew: true);
|
||||
_ = stbands.Upper.Value;
|
||||
|
||||
// Assert - Upper should not increase (only tighten or stay same)
|
||||
Assert.True(secondUpper <= initialUpper || secondUpper == stbands.Upper.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stbands_LowerBand_OnlyMovesUp_InUptrend()
|
||||
{
|
||||
// Arrange
|
||||
Stbands stbands = new(period: 3, multiplier: 1.0);
|
||||
DateTime time = DateTime.UtcNow;
|
||||
|
||||
// Act - Create uptrend scenario
|
||||
stbands.Update(new TBar(time, 100, 105, 95, 102, 1000), isNew: true);
|
||||
double initialLower = stbands.Lower.Value;
|
||||
|
||||
@@ -109,94 +119,112 @@ public class StbandsTests
|
||||
stbands.Update(new TBar(time.AddMinutes(2), 110, 115, 108, 114, 1000), isNew: true);
|
||||
double thirdLower = stbands.Lower.Value;
|
||||
|
||||
// Assert - Lower should not decrease (only tighten or stay same)
|
||||
Assert.True(secondLower >= initialLower || thirdLower >= secondLower);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stbands_Last_EqualsTrendAppropiateBand()
|
||||
{
|
||||
Stbands stbands = new(period: 3, multiplier: 2.0);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.15, seed: 42);
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
stbands.Update(bar, isNew: true);
|
||||
|
||||
double trend = stbands.Trend.Value;
|
||||
if (trend > 0)
|
||||
{
|
||||
Assert.Equal(stbands.Lower.Value, stbands.Last.Value, 1e-10);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Equal(stbands.Upper.Value, stbands.Last.Value, 1e-10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Trend Tests
|
||||
|
||||
[Fact]
|
||||
public void Stbands_TrendDirection_ChangesOnBreakout()
|
||||
{
|
||||
// Arrange
|
||||
Stbands stbands = new(period: 3, multiplier: 1.0);
|
||||
DateTime time = DateTime.UtcNow;
|
||||
|
||||
// Act - Start with some bars
|
||||
stbands.Update(new TBar(time, 100, 105, 95, 100, 1000), isNew: true);
|
||||
stbands.Update(new TBar(time.AddMinutes(1), 100, 105, 95, 100, 1000), isNew: true);
|
||||
stbands.Update(new TBar(time.AddMinutes(2), 100, 105, 95, 100, 1000), isNew: true);
|
||||
_ = (int)stbands.Trend.Value;
|
||||
|
||||
// Create a large breakout above upper band
|
||||
stbands.Update(new TBar(time.AddMinutes(3), 120, 130, 118, 128, 1000), isNew: true);
|
||||
|
||||
// Assert - Trend should potentially change
|
||||
Assert.True(stbands.Trend.Value == 1 || stbands.Trend.Value == -1);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region State Management Tests
|
||||
|
||||
[Fact]
|
||||
public void Stbands_IsNew_False_RollsBackCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
Stbands stbands = new(period: 3, multiplier: 2.0);
|
||||
DateTime time = DateTime.UtcNow;
|
||||
|
||||
// Act
|
||||
stbands.Update(new TBar(time, 100, 105, 95, 100, 1000), isNew: true);
|
||||
stbands.Update(new TBar(time.AddMinutes(1), 102, 108, 100, 106, 1000), isNew: true);
|
||||
stbands.Update(new TBar(time.AddMinutes(2), 106, 112, 104, 110, 1000), isNew: true);
|
||||
double upperBefore = stbands.Upper.Value;
|
||||
_ = stbands.Lower.Value;
|
||||
|
||||
// Update with different value, isNew = false
|
||||
stbands.Update(new TBar(time.AddMinutes(2), 90, 95, 85, 88, 1000), isNew: false);
|
||||
double upperAfter = stbands.Upper.Value;
|
||||
_ = stbands.Lower.Value;
|
||||
|
||||
// Assert - Values should change due to bar correction
|
||||
Assert.NotEqual(upperBefore, upperAfter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stbands_IsNew_False_IterativeCorrections_Restore()
|
||||
{
|
||||
// Arrange
|
||||
Stbands stbands = new(period: 3, multiplier: 2.0);
|
||||
DateTime time = DateTime.UtcNow;
|
||||
|
||||
// Act - Build up state
|
||||
stbands.Update(new TBar(time, 100, 105, 95, 100, 1000), isNew: true);
|
||||
stbands.Update(new TBar(time.AddMinutes(1), 102, 108, 100, 106, 1000), isNew: true);
|
||||
stbands.Update(new TBar(time.AddMinutes(2), 106, 112, 104, 110, 1000), isNew: true);
|
||||
double originalUpper = stbands.Upper.Value;
|
||||
double originalLower = stbands.Lower.Value;
|
||||
|
||||
// Make multiple corrections
|
||||
stbands.Update(new TBar(time.AddMinutes(2), 90, 95, 85, 88, 1000), isNew: false);
|
||||
stbands.Update(new TBar(time.AddMinutes(2), 80, 85, 75, 78, 1000), isNew: false);
|
||||
|
||||
// Restore original bar
|
||||
stbands.Update(new TBar(time.AddMinutes(2), 106, 112, 104, 110, 1000), isNew: false);
|
||||
double restoredUpper = stbands.Upper.Value;
|
||||
double restoredLower = stbands.Lower.Value;
|
||||
|
||||
// Assert - Should restore to original values
|
||||
Assert.Equal(originalUpper, restoredUpper, precision: 10);
|
||||
Assert.Equal(originalLower, restoredLower, precision: 10);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region NaN / Infinity Handling Tests
|
||||
|
||||
[Fact]
|
||||
public void Stbands_NaN_HandledGracefully()
|
||||
{
|
||||
// Arrange
|
||||
Stbands stbands = new(period: 3, multiplier: 2.0);
|
||||
DateTime time = DateTime.UtcNow;
|
||||
|
||||
// Act
|
||||
stbands.Update(new TBar(time, 100, 105, 95, 100, 1000), isNew: true);
|
||||
stbands.Update(new TBar(time.AddMinutes(1), 102, 108, 100, 106, 1000), isNew: true);
|
||||
stbands.Update(new TBar(time.AddMinutes(2), double.NaN, double.NaN, double.NaN, double.NaN, 0), isNew: true);
|
||||
|
||||
// Assert - Should substitute last valid values
|
||||
Assert.True(double.IsFinite(stbands.Upper.Value));
|
||||
Assert.True(double.IsFinite(stbands.Lower.Value));
|
||||
}
|
||||
@@ -204,24 +232,24 @@ public class StbandsTests
|
||||
[Fact]
|
||||
public void Stbands_Infinity_HandledGracefully()
|
||||
{
|
||||
// Arrange
|
||||
Stbands stbands = new(period: 3, multiplier: 2.0);
|
||||
DateTime time = DateTime.UtcNow;
|
||||
|
||||
// Act
|
||||
stbands.Update(new TBar(time, 100, 105, 95, 100, 1000), isNew: true);
|
||||
stbands.Update(new TBar(time.AddMinutes(1), 102, 108, 100, 106, 1000), isNew: true);
|
||||
stbands.Update(new TBar(time.AddMinutes(2), double.PositiveInfinity, double.PositiveInfinity, double.NegativeInfinity, double.PositiveInfinity, 0), isNew: true);
|
||||
|
||||
// Assert - Should substitute last valid values
|
||||
Assert.True(double.IsFinite(stbands.Upper.Value));
|
||||
Assert.True(double.IsFinite(stbands.Lower.Value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reset Tests
|
||||
|
||||
[Fact]
|
||||
public void Stbands_Reset_ClearsState()
|
||||
{
|
||||
// Arrange
|
||||
Stbands stbands = new(period: 3, multiplier: 2.0);
|
||||
DateTime time = DateTime.UtcNow;
|
||||
|
||||
@@ -229,21 +257,56 @@ public class StbandsTests
|
||||
stbands.Update(new TBar(time.AddMinutes(1), 102, 108, 100, 106, 1000), isNew: true);
|
||||
stbands.Update(new TBar(time.AddMinutes(2), 106, 112, 104, 110, 1000), isNew: true);
|
||||
|
||||
// Act
|
||||
stbands.Reset();
|
||||
|
||||
// Assert
|
||||
Assert.False(stbands.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stbands_Reset_ThenReuse_ProducesSameResults()
|
||||
{
|
||||
Stbands stbands = new(period: 3, multiplier: 2.0);
|
||||
DateTime time = DateTime.UtcNow;
|
||||
var bars = new TBar[]
|
||||
{
|
||||
new(time, 100, 105, 95, 100, 1000),
|
||||
new(time.AddMinutes(1), 102, 108, 100, 106, 1000),
|
||||
new(time.AddMinutes(2), 106, 112, 104, 110, 1000),
|
||||
new(time.AddMinutes(3), 108, 115, 105, 112, 1000),
|
||||
new(time.AddMinutes(4), 112, 118, 110, 116, 1000),
|
||||
};
|
||||
|
||||
// First pass
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
stbands.Update(bar, isNew: true);
|
||||
}
|
||||
double upperFirst = stbands.Upper.Value;
|
||||
double lowerFirst = stbands.Lower.Value;
|
||||
double trendFirst = stbands.Trend.Value;
|
||||
|
||||
// Reset and second pass
|
||||
stbands.Reset();
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
stbands.Update(bar, isNew: true);
|
||||
}
|
||||
|
||||
Assert.Equal(upperFirst, stbands.Upper.Value, 1e-10);
|
||||
Assert.Equal(lowerFirst, stbands.Lower.Value, 1e-10);
|
||||
Assert.Equal(trendFirst, stbands.Trend.Value, 1e-10);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region WarmupPeriod / IsHot Tests
|
||||
|
||||
[Fact]
|
||||
public void Stbands_WarmupPeriod_IsHotTransition()
|
||||
{
|
||||
// Arrange
|
||||
Stbands stbands = new(period: 5, multiplier: 2.0);
|
||||
DateTime time = DateTime.UtcNow;
|
||||
|
||||
// Act & Assert
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
stbands.Update(new TBar(time.AddMinutes(i), 100 + i, 105 + i, 95 + i, 102 + i, 1000), isNew: true);
|
||||
@@ -254,41 +317,153 @@ public class StbandsTests
|
||||
Assert.True(stbands.IsHot);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Prime Tests
|
||||
|
||||
[Fact]
|
||||
public void Stbands_Prime_SetsIndicatorToHot()
|
||||
{
|
||||
Stbands stbands = new(period: 5, multiplier: 2.0);
|
||||
double[] prices = [100, 102, 104, 98, 96, 99, 103, 107, 105, 110];
|
||||
|
||||
stbands.Prime(prices.AsSpan());
|
||||
|
||||
Assert.True(stbands.IsHot);
|
||||
Assert.True(double.IsFinite(stbands.Last.Value));
|
||||
Assert.True(double.IsFinite(stbands.Upper.Value));
|
||||
Assert.True(double.IsFinite(stbands.Lower.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stbands_Prime_EmptySpan_DoesNotThrow()
|
||||
{
|
||||
Stbands stbands = new(period: 5, multiplier: 2.0);
|
||||
|
||||
stbands.Prime(ReadOnlySpan<double>.Empty);
|
||||
|
||||
Assert.False(stbands.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stbands_Prime_WithCustomStep()
|
||||
{
|
||||
Stbands stbands = new(period: 3, multiplier: 2.0);
|
||||
double[] prices = [100, 102, 104, 106, 108];
|
||||
|
||||
stbands.Prime(prices.AsSpan(), TimeSpan.FromMinutes(5));
|
||||
|
||||
Assert.True(stbands.IsHot);
|
||||
Assert.True(double.IsFinite(stbands.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stbands_Prime_ThenUpdate_ContinuesCorrectly()
|
||||
{
|
||||
Stbands stbands = new(period: 3, multiplier: 2.0);
|
||||
double[] primeData = [100, 102, 104, 106, 108];
|
||||
|
||||
stbands.Prime(primeData.AsSpan());
|
||||
Assert.True(stbands.IsHot);
|
||||
|
||||
// Continue streaming with TBar
|
||||
stbands.Update(new TBar(DateTime.UtcNow, 108, 112, 106, 110, 1000), isNew: true);
|
||||
|
||||
Assert.True(stbands.IsHot);
|
||||
Assert.True(double.IsFinite(stbands.Last.Value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Calculate Tests
|
||||
|
||||
[Fact]
|
||||
public void Stbands_Calculate_ReturnsResultsAndHotIndicator()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
TBarSeries bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var (results, indicator) = Stbands.Calculate(bars, period: 5, multiplier: 2.0);
|
||||
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.Equal(bars.Count, results.Count);
|
||||
Assert.True(double.IsFinite(indicator.Last.Value));
|
||||
Assert.True(double.IsFinite(indicator.Upper.Value));
|
||||
Assert.True(double.IsFinite(indicator.Lower.Value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Update TSeries Tests
|
||||
|
||||
[Fact]
|
||||
public void Stbands_UpdateTSeries_ReturnsValidSeries()
|
||||
{
|
||||
Stbands stbands = new(period: 5, multiplier: 2.0);
|
||||
var series = new TSeries();
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
series.Add(DateTime.UtcNow.AddMinutes(i), 100 + i * 0.5);
|
||||
}
|
||||
|
||||
TSeries result = stbands.Update(series);
|
||||
|
||||
Assert.Equal(20, result.Count);
|
||||
Assert.True(stbands.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stbands_UpdateTSeries_NullSource_ThrowsArgumentNullException()
|
||||
{
|
||||
Stbands stbands = new(period: 5, multiplier: 2.0);
|
||||
|
||||
Assert.Throws<ArgumentNullException>(() => stbands.Update((TSeries)null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stbands_UpdateTBarSeries_NullSource_ThrowsArgumentNullException()
|
||||
{
|
||||
Stbands stbands = new(period: 5, multiplier: 2.0);
|
||||
|
||||
Assert.Throws<ArgumentNullException>(() => stbands.Update((TBarSeries)null!));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Update TBarSeries Tests
|
||||
|
||||
[Fact]
|
||||
public void Stbands_UpdateTBarSeries_ReturnsValidSeries()
|
||||
{
|
||||
// Arrange
|
||||
int period = 5;
|
||||
Stbands stbands = new(period, multiplier: 2.0);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
TBarSeries bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Act
|
||||
TSeries result = stbands.Update(bars);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(bars.Count, result.Count);
|
||||
Assert.True(stbands.IsHot);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Batch Tests
|
||||
|
||||
[Fact]
|
||||
public void Stbands_StaticCalculate_ReturnsValidSeries()
|
||||
{
|
||||
// Arrange
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
TBarSeries bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Act
|
||||
TSeries result = Stbands.Batch(bars, period: 5, multiplier: 2.0);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(bars.Count, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stbands_SpanCalculate_ProducesValidOutput()
|
||||
{
|
||||
// Arrange
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
TBarSeries bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
int period = 10;
|
||||
@@ -301,10 +476,8 @@ public class StbandsTests
|
||||
double[] lower = new double[bars.Count];
|
||||
double[] trend = new double[bars.Count];
|
||||
|
||||
// Act
|
||||
Stbands.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(), upper.AsSpan(), lower.AsSpan(), trend.AsSpan(), period, multiplier);
|
||||
|
||||
// Assert
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(upper[i]));
|
||||
@@ -317,7 +490,6 @@ public class StbandsTests
|
||||
[Fact]
|
||||
public void Stbands_SpanCalculate_InvalidLength_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange
|
||||
double[] high = new double[10];
|
||||
double[] low = new double[10];
|
||||
double[] close = new double[10];
|
||||
@@ -325,16 +497,49 @@ public class StbandsTests
|
||||
double[] lower = new double[10];
|
||||
double[] trend = new double[9]; // Wrong length
|
||||
|
||||
// Act & Assert
|
||||
ArgumentException exception = Assert.Throws<ArgumentException>(
|
||||
() => Stbands.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(), upper.AsSpan(), lower.AsSpan(), trend.AsSpan()));
|
||||
Assert.Equal("high", exception.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stbands_SpanBatch_InvalidPeriod_ThrowsArgumentOutOfRangeException()
|
||||
{
|
||||
double[] data = new double[10];
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||
Stbands.Batch(data.AsSpan(), data.AsSpan(), data.AsSpan(),
|
||||
data.AsSpan(), data.AsSpan(), data.AsSpan(), period: 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stbands_SpanBatch_InvalidMultiplier_ThrowsArgumentOutOfRangeException()
|
||||
{
|
||||
double[] data = new double[10];
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||
Stbands.Batch(data.AsSpan(), data.AsSpan(), data.AsSpan(),
|
||||
data.AsSpan(), data.AsSpan(), data.AsSpan(), period: 10, multiplier: 0.0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stbands_SpanBatch_EmptyArrays_DoesNotThrow()
|
||||
{
|
||||
double[] empty = [];
|
||||
|
||||
Stbands.Batch(empty.AsSpan(), empty.AsSpan(), empty.AsSpan(),
|
||||
empty.AsSpan(), empty.AsSpan(), empty.AsSpan(), period: 10);
|
||||
|
||||
Assert.Empty(empty);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Consistency Tests
|
||||
|
||||
[Fact]
|
||||
public void Stbands_Consistency_StreamingVsBatch()
|
||||
{
|
||||
// Arrange
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
TBarSeries bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
int period = 10;
|
||||
@@ -350,14 +555,12 @@ public class StbandsTests
|
||||
// Batch
|
||||
TSeries batchResult = Stbands.Batch(bars, period, multiplier);
|
||||
|
||||
// Assert - Last values should match
|
||||
Assert.Equal(batchResult[^1].Value, streamingStbands.Last.Value, precision: 8);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stbands_Consistency_StreamingVsSpan()
|
||||
{
|
||||
// Arrange
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
TBarSeries bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
int period = 10;
|
||||
@@ -379,45 +582,64 @@ public class StbandsTests
|
||||
double[] trend = new double[bars.Count];
|
||||
Stbands.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(), upper.AsSpan(), lower.AsSpan(), trend.AsSpan(), period, multiplier);
|
||||
|
||||
// Assert - Last values should match
|
||||
Assert.Equal(upper[^1], streamingStbands.Upper.Value, precision: 8);
|
||||
Assert.Equal(lower[^1], streamingStbands.Lower.Value, precision: 8);
|
||||
Assert.Equal(trend[^1], streamingStbands.Trend.Value, precision: 8);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TValue Update Tests
|
||||
|
||||
[Fact]
|
||||
public void Stbands_TValue_Update_WorksWithSingleValue()
|
||||
{
|
||||
// Arrange
|
||||
Stbands stbands = new(period: 3, multiplier: 2.0);
|
||||
DateTime time = DateTime.UtcNow;
|
||||
|
||||
// Act - Using TValue (treated as H=L=C=value)
|
||||
stbands.Update(new TValue(time, 100.0), isNew: true);
|
||||
stbands.Update(new TValue(time.AddMinutes(1), 102.0), isNew: true);
|
||||
stbands.Update(new TValue(time.AddMinutes(2), 104.0), isNew: true);
|
||||
|
||||
// Assert
|
||||
Assert.True(stbands.IsHot);
|
||||
Assert.True(double.IsFinite(stbands.Upper.Value));
|
||||
Assert.True(double.IsFinite(stbands.Lower.Value));
|
||||
// With H=L=C, bands should be based on ATR=0 initially, but will have width from multiplier*0
|
||||
// Actually TR will be 0 when H-L=0, so bands may be tight
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Width Tests
|
||||
|
||||
[Fact]
|
||||
public void Stbands_Width_IsUpperMinusLower()
|
||||
{
|
||||
// Arrange
|
||||
Stbands stbands = new(period: 3, multiplier: 2.0);
|
||||
DateTime time = DateTime.UtcNow;
|
||||
|
||||
// Act
|
||||
stbands.Update(new TBar(time, 100, 110, 90, 102, 1000), isNew: true);
|
||||
stbands.Update(new TBar(time.AddMinutes(1), 102, 115, 95, 108, 1000), isNew: true);
|
||||
stbands.Update(new TBar(time.AddMinutes(2), 108, 120, 100, 115, 1000), isNew: true);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(stbands.Upper.Value - stbands.Lower.Value, stbands.Width.Value, precision: 10);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Pub Event Tests
|
||||
|
||||
[Fact]
|
||||
public void Stbands_Pub_DoesNotFireDirectly()
|
||||
{
|
||||
// Stbands overrides Update paths without calling PubEvent —
|
||||
// Pub event is inherited from AbstractBase but not invoked.
|
||||
Stbands stbands = new(period: 3, multiplier: 2.0);
|
||||
bool fired = false;
|
||||
stbands.Pub += (object? sender, in TValueEventArgs args) => fired = true;
|
||||
|
||||
stbands.Update(new TValue(DateTime.UtcNow, 100.0), isNew: true);
|
||||
|
||||
Assert.False(fired);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class UchannelQuantowerTests
|
||||
{
|
||||
#region Constructor Tests
|
||||
|
||||
[Fact]
|
||||
public void UchannelIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
@@ -16,6 +19,19 @@ public class UchannelQuantowerTests
|
||||
Assert.Equal("UCHANNEL - Ehlers Ultimate Channel", indicator.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UchannelIndicator_Constructor_SetsDisplayProperties()
|
||||
{
|
||||
var indicator = new UchannelIndicator();
|
||||
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region MinHistoryDepths Tests
|
||||
|
||||
[Fact]
|
||||
public void UchannelIndicator_MinHistoryDepths_ReturnsMaxOfPeriods()
|
||||
{
|
||||
@@ -29,6 +45,29 @@ public class UchannelQuantowerTests
|
||||
Assert.Equal(25, indicator3.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UchannelIndicator_MinHistoryDepths_ExplicitInterface()
|
||||
{
|
||||
var indicator = new UchannelIndicator { StrPeriod = 15, CenterPeriod = 30 };
|
||||
|
||||
int explicit_value = ((IWatchlistIndicator)indicator).MinHistoryDepths;
|
||||
|
||||
Assert.Equal(30, explicit_value);
|
||||
Assert.Equal(indicator.MinHistoryDepths, explicit_value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UchannelIndicator_MinHistoryDepths_MinPeriods()
|
||||
{
|
||||
var indicator = new UchannelIndicator { StrPeriod = 1, CenterPeriod = 1 };
|
||||
|
||||
Assert.Equal(1, indicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ShortName Tests
|
||||
|
||||
[Fact]
|
||||
public void UchannelIndicator_ShortName_FormatsCorrectly()
|
||||
{
|
||||
@@ -42,6 +81,31 @@ public class UchannelQuantowerTests
|
||||
Assert.Equal("UCHANNEL (15,25,2.5)", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UchannelIndicator_ShortName_DefaultParameters()
|
||||
{
|
||||
var indicator = new UchannelIndicator();
|
||||
|
||||
Assert.Equal("UCHANNEL (20,20,1.0)", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UchannelIndicator_ShortName_UpdatesWithParameters()
|
||||
{
|
||||
var indicator = new UchannelIndicator();
|
||||
Assert.Equal("UCHANNEL (20,20,1.0)", indicator.ShortName);
|
||||
|
||||
indicator.StrPeriod = 10;
|
||||
indicator.CenterPeriod = 30;
|
||||
indicator.Multiplier = 3.0;
|
||||
|
||||
Assert.Equal("UCHANNEL (10,30,3.0)", indicator.ShortName);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region SourceCodeLink Tests
|
||||
|
||||
[Fact]
|
||||
public void UchannelIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
@@ -51,20 +115,9 @@ public class UchannelQuantowerTests
|
||||
Assert.Contains("Uchannel.cs", indicator.SourceCodeLink, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UchannelIndicator_OnInit_CreatesInternalIndicator()
|
||||
{
|
||||
var indicator = new UchannelIndicator
|
||||
{
|
||||
StrPeriod = 10,
|
||||
CenterPeriod = 15,
|
||||
Multiplier = 1.5
|
||||
};
|
||||
#endregion
|
||||
|
||||
// OnInit is protected, but we can verify it doesn't throw
|
||||
// by checking the indicator state after construction
|
||||
Assert.NotNull(indicator);
|
||||
}
|
||||
#region Parameter Tests
|
||||
|
||||
[Fact]
|
||||
public void UchannelIndicator_Parameters_CanBeModified()
|
||||
@@ -82,6 +135,10 @@ public class UchannelQuantowerTests
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Description Tests
|
||||
|
||||
[Fact]
|
||||
public void UchannelIndicator_Description_IsNotEmpty()
|
||||
{
|
||||
@@ -91,12 +148,325 @@ public class UchannelQuantowerTests
|
||||
Assert.Contains("Ultrasmooth", indicator.Description, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region LineSeries Tests
|
||||
|
||||
[Fact]
|
||||
public void UchannelIndicator_HasCorrectLineSeries()
|
||||
public void UchannelIndicator_HasFiveLineSeries()
|
||||
{
|
||||
var indicator = new UchannelIndicator();
|
||||
|
||||
// The indicator should have 5 line series: Middle, Upper, Lower, STR, Width
|
||||
// The constructor adds 5 line series: Middle, Upper, Lower, STR, Width
|
||||
Assert.Equal(5, indicator.LinesSeries.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UchannelIndicator_LineSeries_HaveCorrectNames()
|
||||
{
|
||||
var indicator = new UchannelIndicator();
|
||||
|
||||
Assert.Equal("Middle", indicator.LinesSeries[0].Name);
|
||||
Assert.Equal("Upper", indicator.LinesSeries[1].Name);
|
||||
Assert.Equal("Lower", indicator.LinesSeries[2].Name);
|
||||
Assert.Equal("STR", indicator.LinesSeries[3].Name);
|
||||
Assert.Equal("Width", indicator.LinesSeries[4].Name);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Initialize Tests
|
||||
|
||||
[Fact]
|
||||
public void UchannelIndicator_Initialize_DoesNotThrow()
|
||||
{
|
||||
var indicator = new UchannelIndicator
|
||||
{
|
||||
StrPeriod = 10,
|
||||
CenterPeriod = 15,
|
||||
Multiplier = 1.5
|
||||
};
|
||||
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.NotNull(indicator);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UchannelIndicator_Initialize_PreservesLineSeries()
|
||||
{
|
||||
var indicator = new UchannelIndicator();
|
||||
|
||||
indicator.Initialize();
|
||||
|
||||
// Line series should still be present after init
|
||||
Assert.Equal(5, indicator.LinesSeries.Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ProcessUpdate Tests
|
||||
|
||||
[Fact]
|
||||
public void UchannelIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new UchannelIndicator { StrPeriod = 5, CenterPeriod = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
// All 5 line series should have values
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
Assert.Equal(1, indicator.LinesSeries[i].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[i].GetValue(0)));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UchannelIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new UchannelIndicator { StrPeriod = 5, CenterPeriod = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106, 1500);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UchannelIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new UchannelIndicator { StrPeriod = 5, CenterPeriod = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
double firstValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
double secondValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(firstValue));
|
||||
Assert.True(double.IsFinite(secondValue));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Multiple Updates Tests
|
||||
|
||||
[Fact]
|
||||
public void UchannelIndicator_MultipleUpdates_ProducesCorrectSequence()
|
||||
{
|
||||
var indicator = new UchannelIndicator { StrPeriod = 5, CenterPeriod = 5, Multiplier = 1.5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] closes = { 100, 102, 104, 103, 105, 107, 106, 108, 110, 109 };
|
||||
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
double close = closes[i];
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), close, close + 3, close - 3, close, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// All 5 series should have values for each bar
|
||||
for (int s = 0; s < 5; s++)
|
||||
{
|
||||
Assert.Equal(closes.Length, indicator.LinesSeries[s].Count);
|
||||
}
|
||||
|
||||
// All last values should be finite
|
||||
for (int s = 0; s < 5; s++)
|
||||
{
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[s].GetValue(0)));
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Band Relationship Tests
|
||||
|
||||
[Fact]
|
||||
public void UchannelIndicator_BandRelationships_AreCorrect()
|
||||
{
|
||||
var indicator = new UchannelIndicator { StrPeriod = 5, CenterPeriod = 5, Multiplier = 2.0 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// Add varied data to generate band width
|
||||
double[] closes = { 100, 105, 95, 110, 90, 105, 100, 108, 92, 103 };
|
||||
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
double close = closes[i];
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), close, close + 5, close - 5, close, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// Get last values: Middle=0, Upper=1, Lower=2, STR=3, Width=4
|
||||
double middle = indicator.LinesSeries[0].GetValue(0);
|
||||
double upper = indicator.LinesSeries[1].GetValue(0);
|
||||
double lower = indicator.LinesSeries[2].GetValue(0);
|
||||
double width = indicator.LinesSeries[4].GetValue(0);
|
||||
|
||||
// Band relationships: Upper >= Middle >= Lower
|
||||
Assert.True(upper >= middle, $"Upper ({upper}) should be >= Middle ({middle})");
|
||||
Assert.True(middle >= lower, $"Middle ({middle}) should be >= Lower ({lower})");
|
||||
|
||||
// Width = Upper - Lower
|
||||
Assert.Equal(upper - lower, width, 6);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Multiplier Tests
|
||||
|
||||
[Fact]
|
||||
public void UchannelIndicator_Multiplier_AffectsBandWidth()
|
||||
{
|
||||
var indicator1 = new UchannelIndicator { StrPeriod = 5, CenterPeriod = 5, Multiplier = 1.0 };
|
||||
var indicator2 = new UchannelIndicator { StrPeriod = 5, CenterPeriod = 5, Multiplier = 2.0 };
|
||||
indicator1.Initialize();
|
||||
indicator2.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] closes = { 100, 105, 95, 110, 90, 105, 100, 108, 92, 103 };
|
||||
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
double close = closes[i];
|
||||
indicator1.HistoricalData.AddBar(now.AddMinutes(i), close, close + 5, close - 5, close, 1000);
|
||||
indicator1.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator2.HistoricalData.AddBar(now.AddMinutes(i), close, close + 5, close - 5, close, 1000);
|
||||
indicator2.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double width1 = indicator1.LinesSeries[4].GetValue(0);
|
||||
double width2 = indicator2.LinesSeries[4].GetValue(0);
|
||||
|
||||
// Width2 should be approximately 2x Width1
|
||||
Assert.True(Math.Abs(width2 - 2 * width1) < 0.0001,
|
||||
$"Width2 ({width2}) should be ~2x Width1 ({width1})");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Different Period Tests
|
||||
|
||||
[Fact]
|
||||
public void UchannelIndicator_DifferentPeriods_ProduceDifferentResults()
|
||||
{
|
||||
var indicator1 = new UchannelIndicator { StrPeriod = 5, CenterPeriod = 5 };
|
||||
var indicator2 = new UchannelIndicator { StrPeriod = 20, CenterPeriod = 20 };
|
||||
indicator1.Initialize();
|
||||
indicator2.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double close = 100 + (i % 5) * 2;
|
||||
indicator1.HistoricalData.AddBar(now.AddMinutes(i), close, close + 3, close - 3, close, 1000);
|
||||
indicator1.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator2.HistoricalData.AddBar(now.AddMinutes(i), close, close + 3, close - 3, close, 1000);
|
||||
indicator2.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double middle1 = indicator1.LinesSeries[0].GetValue(0);
|
||||
double middle2 = indicator2.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Different smoothing periods should produce different middle values
|
||||
Assert.NotEqual(middle1, middle2);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region STR Series Tests
|
||||
|
||||
[Fact]
|
||||
public void UchannelIndicator_STR_IsNonNegative()
|
||||
{
|
||||
var indicator = new UchannelIndicator { StrPeriod = 5, CenterPeriod = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] closes = { 100, 105, 95, 110, 90, 105, 100, 108, 92, 103 };
|
||||
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
double close = closes[i];
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), close, close + 5, close - 5, close, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// STR (smoothed true range) should be non-negative
|
||||
double str = indicator.LinesSeries[3].GetValue(0);
|
||||
Assert.True(str >= 0, $"STR ({str}) should be >= 0");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ShowColdValues Tests
|
||||
|
||||
[Fact]
|
||||
public void UchannelIndicator_ShowColdValues_True_ShowsValues()
|
||||
{
|
||||
var indicator = new UchannelIndicator
|
||||
{
|
||||
StrPeriod = 50,
|
||||
CenterPeriod = 50,
|
||||
ShowColdValues = true
|
||||
};
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Add fewer bars than warmup
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 105, 95, 102, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// With ShowColdValues = true, values should be shown even before warmup
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UchannelIndicator_ShowColdValues_False_SetsNaN()
|
||||
{
|
||||
var indicator = new UchannelIndicator
|
||||
{
|
||||
StrPeriod = 50,
|
||||
CenterPeriod = 50,
|
||||
ShowColdValues = false
|
||||
};
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Add fewer bars than warmup
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 105, 95, 102, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// With ShowColdValues = false, cold values should be NaN before warmup
|
||||
Assert.True(double.IsNaN(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ namespace QuanTAlib.Tests;
|
||||
|
||||
public class VwapsdIndicatorTests
|
||||
{
|
||||
// ── Constructor & Defaults ──────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
@@ -16,6 +18,37 @@ public class VwapsdIndicatorTests
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_Constructor_Description_IsNotEmpty()
|
||||
{
|
||||
var indicator = new VwapsdIndicator();
|
||||
|
||||
Assert.False(string.IsNullOrWhiteSpace(indicator.Description));
|
||||
Assert.Contains("volume", indicator.Description, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_Constructor_CreatesFourLineSeries()
|
||||
{
|
||||
var indicator = new VwapsdIndicator();
|
||||
|
||||
Assert.Equal(4, indicator.LinesSeries.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_Constructor_LineSeriesNames_BeforeInit()
|
||||
{
|
||||
var indicator = new VwapsdIndicator();
|
||||
|
||||
// Before OnInit, series have their constructor names
|
||||
Assert.Equal("VWAP", indicator.LinesSeries[0].Name);
|
||||
Assert.Equal("Upper", indicator.LinesSeries[1].Name);
|
||||
Assert.Equal("Lower", indicator.LinesSeries[2].Name);
|
||||
Assert.Equal("Width", indicator.LinesSeries[3].Name);
|
||||
}
|
||||
|
||||
// ── MinHistoryDepths ────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_MinHistoryDepths_EqualsTwo()
|
||||
{
|
||||
@@ -25,6 +58,16 @@ public class VwapsdIndicatorTests
|
||||
Assert.Equal(2, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
// ── ShortName ───────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_ShortName_DefaultFormat()
|
||||
{
|
||||
var indicator = new VwapsdIndicator();
|
||||
|
||||
Assert.Equal("VWAPSD (2.0)", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_ShortName_IncludesNumDevs()
|
||||
{
|
||||
@@ -34,37 +77,92 @@ public class VwapsdIndicatorTests
|
||||
Assert.Contains("2.5", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
// ── SourceCodeLink ──────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_Initialize_CreatesFourLineSeries()
|
||||
public void VwapsdIndicator_SourceCodeLink_PointsToGitHub()
|
||||
{
|
||||
var indicator = new VwapsdIndicator();
|
||||
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("Vwapsd.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
// ── OnInit σ Rename ─────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_Initialize_RenamesSeriesWithSigmaNotation()
|
||||
{
|
||||
var indicator = new VwapsdIndicator { NumDevs = 2.0 };
|
||||
indicator.Initialize();
|
||||
|
||||
// After OnInit, Upper/Lower should have σ notation
|
||||
Assert.Equal("Upper (+2.0σ)", indicator.LinesSeries[1].Name);
|
||||
Assert.Equal("Lower (-2.0σ)", indicator.LinesSeries[2].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_Initialize_SigmaNotation_ReflectsNumDevs()
|
||||
{
|
||||
var indicator = new VwapsdIndicator { NumDevs = 1.5 };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Equal("Upper (+1.5σ)", indicator.LinesSeries[1].Name);
|
||||
Assert.Equal("Lower (-1.5σ)", indicator.LinesSeries[2].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_Initialize_PreservesSeriesCount()
|
||||
{
|
||||
var indicator = new VwapsdIndicator { NumDevs = 2.0 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist (VWAP, Upper, Lower, Width)
|
||||
indicator.Initialize();
|
||||
Assert.Equal(4, indicator.LinesSeries.Count);
|
||||
}
|
||||
|
||||
// ── Parameters ──────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_Parameters_CanBeChanged()
|
||||
{
|
||||
var indicator = new VwapsdIndicator { NumDevs = 1.5 };
|
||||
Assert.Equal(1.5, indicator.NumDevs);
|
||||
|
||||
indicator.NumDevs = 2.5;
|
||||
Assert.Equal(2.5, indicator.NumDevs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_ShowColdValues_CanBeChanged()
|
||||
{
|
||||
var indicator = new VwapsdIndicator();
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
// ── ProcessUpdate: HistoricalBar ────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new VwapsdIndicator { NumDevs = 2.0 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data with volume
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
|
||||
|
||||
// Process update
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
// Line series should have values
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
// ── ProcessUpdate: NewBar ───────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
@@ -81,6 +179,8 @@ public class VwapsdIndicatorTests
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
// ── ProcessUpdate: NewTick ──────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
@@ -100,6 +200,8 @@ public class VwapsdIndicatorTests
|
||||
Assert.True(double.IsFinite(secondValue));
|
||||
}
|
||||
|
||||
// ── MultipleUpdates ─────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_MultipleUpdates_ProducesCorrectSequence()
|
||||
{
|
||||
@@ -129,15 +231,7 @@ public class VwapsdIndicatorTests
|
||||
Assert.True(lastVwap >= 95 && lastVwap <= 110);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_Parameters_CanBeChanged()
|
||||
{
|
||||
var indicator = new VwapsdIndicator { NumDevs = 1.5 };
|
||||
Assert.Equal(1.5, indicator.NumDevs);
|
||||
|
||||
indicator.NumDevs = 2.5;
|
||||
Assert.Equal(2.5, indicator.NumDevs);
|
||||
}
|
||||
// ── AllBandsUpdate ──────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_AllBandsUpdate_Correctly()
|
||||
@@ -161,6 +255,8 @@ public class VwapsdIndicatorTests
|
||||
}
|
||||
}
|
||||
|
||||
// ── BandRelationships ───────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_BandRelationships_AreCorrect()
|
||||
{
|
||||
@@ -168,7 +264,6 @@ public class VwapsdIndicatorTests
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// Add varied data to generate band width
|
||||
double[] closes = { 100, 105, 95, 110, 90, 105, 100, 108, 92, 103 };
|
||||
double[] volumes = { 1000, 1500, 2000, 1200, 1800, 1100, 1600, 1300, 1900, 1400 };
|
||||
|
||||
@@ -180,21 +275,19 @@ public class VwapsdIndicatorTests
|
||||
now = now.AddMinutes(1);
|
||||
}
|
||||
|
||||
// Get last values: VWAP=0, Upper=1, Lower=2, Width=3
|
||||
double vwap = indicator.LinesSeries[0].GetValue(0);
|
||||
double upper = indicator.LinesSeries[1].GetValue(0);
|
||||
double lower = indicator.LinesSeries[2].GetValue(0);
|
||||
double width = indicator.LinesSeries[3].GetValue(0);
|
||||
|
||||
// Band relationships: Upper > VWAP > Lower
|
||||
Assert.True(upper >= vwap, $"Upper ({upper}) should be >= VWAP ({vwap})");
|
||||
Assert.True(vwap >= lower, $"VWAP ({vwap}) should be >= Lower ({lower})");
|
||||
|
||||
// Width = Upper - Lower (2 × numDevs × StdDev)
|
||||
Assert.True(Math.Abs(width - (upper - lower)) < 0.0001,
|
||||
$"Width ({width}) should equal Upper - Lower ({upper - lower})");
|
||||
}
|
||||
|
||||
// ── VolumeWeighting ─────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_VolumeWeighting_AffectsVwap()
|
||||
{
|
||||
@@ -205,9 +298,6 @@ public class VwapsdIndicatorTests
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Same prices but different volume distributions
|
||||
// Process both bars for each indicator
|
||||
|
||||
// Indicator1: high volume on low price, low volume on high price
|
||||
indicator1.HistoricalData.AddBar(now, 100, 102, 98, 100, 10000);
|
||||
indicator1.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
@@ -223,11 +313,11 @@ public class VwapsdIndicatorTests
|
||||
double vwap1 = indicator1.LinesSeries[0].GetValue(0);
|
||||
double vwap2 = indicator2.LinesSeries[0].GetValue(0);
|
||||
|
||||
// VWAP1 should be lower (weighted toward 100 due to high volume at low price)
|
||||
// VWAP2 should be higher (weighted toward 110 due to high volume at high price)
|
||||
Assert.True(vwap1 < vwap2, $"VWAP1 ({vwap1}) should be less than VWAP2 ({vwap2}) due to volume weighting");
|
||||
}
|
||||
|
||||
// ── NumDevs Effect ──────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_NumDevs_AffectsBandWidth()
|
||||
{
|
||||
@@ -250,7 +340,6 @@ public class VwapsdIndicatorTests
|
||||
now = now.AddMinutes(1);
|
||||
}
|
||||
|
||||
// Width should be proportional to numDevs
|
||||
double width1 = indicator1.LinesSeries[3].GetValue(0);
|
||||
double width2 = indicator2.LinesSeries[3].GetValue(0);
|
||||
|
||||
@@ -258,4 +347,114 @@ public class VwapsdIndicatorTests
|
||||
Assert.True(Math.Abs(width2 - 2 * width1) < 0.0001,
|
||||
$"Width2 ({width2}) should be ~2x Width1 ({width1})");
|
||||
}
|
||||
|
||||
// ── Width Non-Negative ──────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_Width_IsNonNegative()
|
||||
{
|
||||
var indicator = new VwapsdIndicator { NumDevs = 2.0 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] closes = { 100, 102, 98, 105, 97, 103, 101, 99 };
|
||||
double[] volumes = { 1000, 1200, 800, 1500, 900, 1100, 1300, 700 };
|
||||
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
double close = closes[i];
|
||||
indicator.HistoricalData.AddBar(now, close, close + 2, close - 2, close, volumes[i]);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
now = now.AddMinutes(1);
|
||||
}
|
||||
|
||||
// Width should be non-negative at every bar
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
double w = indicator.LinesSeries[3].GetValue(closes.Length - 1 - i);
|
||||
Assert.True(w >= 0.0, $"Width at bar {i} ({w}) should be >= 0");
|
||||
}
|
||||
}
|
||||
|
||||
// ── SingleBar Zero Width ────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_SingleBar_ProducesZeroWidth()
|
||||
{
|
||||
var indicator = new VwapsdIndicator { NumDevs = 2.0 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// With only one bar, stddev is 0 → width should be 0
|
||||
double width = indicator.LinesSeries[3].GetValue(0);
|
||||
Assert.Equal(0.0, width, 4);
|
||||
}
|
||||
|
||||
// ── ShowColdValues False ────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_ShowColdValues_False_SuppressesColdValues()
|
||||
{
|
||||
var indicator = new VwapsdIndicator { NumDevs = 2.0, ShowColdValues = false };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// With ShowColdValues=false, cold bars produce NaN
|
||||
double vwap = indicator.LinesSeries[0].GetValue(0);
|
||||
// Value is either NaN (suppressed) or finite (hot)
|
||||
Assert.True(double.IsNaN(vwap) || double.IsFinite(vwap));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_ShowColdValues_True_ShowsAllValues()
|
||||
{
|
||||
var indicator = new VwapsdIndicator { NumDevs = 2.0, ShowColdValues = true };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// With ShowColdValues=true, all values should be finite
|
||||
double vwap = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(vwap));
|
||||
}
|
||||
|
||||
// ── ReInitialize Updates Series Names ───────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_ReInitialize_UpdatesSigmaNotation()
|
||||
{
|
||||
var indicator = new VwapsdIndicator { NumDevs = 2.0 };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Equal("Upper (+2.0σ)", indicator.LinesSeries[1].Name);
|
||||
Assert.Equal("Lower (-2.0σ)", indicator.LinesSeries[2].Name);
|
||||
|
||||
// Change NumDevs and re-init
|
||||
indicator.NumDevs = 3.0;
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Equal("Upper (+3.0σ)", indicator.LinesSeries[1].Name);
|
||||
Assert.Equal("Lower (-3.0σ)", indicator.LinesSeries[2].Name);
|
||||
}
|
||||
|
||||
// ── VWAP Series Name Unchanged After Init ───────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_Initialize_VwapAndWidthNames_Unchanged()
|
||||
{
|
||||
var indicator = new VwapsdIndicator { NumDevs = 2.0 };
|
||||
indicator.Initialize();
|
||||
|
||||
// VWAP and Width series names should remain as constructor set them
|
||||
Assert.Equal("VWAP", indicator.LinesSeries[0].Name);
|
||||
Assert.Equal("Width", indicator.LinesSeries[3].Name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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)")]
|
||||
|
||||
Reference in New Issue
Block a user