namespace QuanTAlib.Tests; public class ApchannelTests { private const double Tolerance = 1e-10; #region Constructor & Validation [Fact] public void Constructor_ValidatesInput() { // Alpha must be > 0 Assert.Throws(() => new Apchannel(0.0)); Assert.Throws(() => new Apchannel(-0.1)); // Alpha must be <= 1 Assert.Throws(() => new Apchannel(1.1)); Assert.Throws(() => new Apchannel(2.0)); // Valid construction var apc = new Apchannel(0.2); Assert.NotNull(apc); } [Fact] public void Constructor_ValidBoundaryValues() { var apc1 = new Apchannel(0.001); // Very small alpha Assert.NotNull(apc1); var apc2 = new Apchannel(1.0); // Maximum alpha Assert.NotNull(apc2); var apc3 = new Apchannel(0.5); // Mid-range alpha Assert.NotNull(apc3); } #endregion #region Basic Functionality [Fact] public void Calc_ReturnsValue() { var apc = new Apchannel(0.2); var time = DateTime.UtcNow; Assert.Equal(0, apc.Last.Value); Assert.Equal(0, apc.UpperBand); Assert.Equal(0, apc.LowerBand); var bar = new TBar(time, 100, 105, 95, 100, 1000); var result = apc.Add(bar); Assert.True(result.Value > 0); Assert.Equal(result.Value, apc.Last.Value); Assert.Equal(105, apc.UpperBand, Tolerance); Assert.Equal(95, apc.LowerBand, Tolerance); } [Fact] public void FirstValue_ReturnsExpected() { var apc = new Apchannel(0.2); var time = DateTime.UtcNow; var bar = new TBar(time, 100, 110, 90, 100, 1000); var result = apc.Add(bar); // First bar: UpperBand = High, LowerBand = Low, Last = midpoint Assert.Equal(110, apc.UpperBand, Tolerance); Assert.Equal(90, apc.LowerBand, Tolerance); Assert.Equal(100, result.Value, Tolerance); // (110 + 90) / 2 } [Fact] public void Properties_Accessible() { var apc = new Apchannel(0.3); var time = DateTime.UtcNow; Assert.Equal(0, apc.Last.Value); Assert.False(apc.IsHot); Assert.Contains("Apchannel", apc.Name, StringComparison.Ordinal); Assert.Contains("0.3", apc.Name, StringComparison.Ordinal); apc.Add(new TBar(time, 100, 105, 95, 100, 1000)); Assert.NotEqual(0, apc.Last.Value); Assert.NotEqual(0, apc.UpperBand); Assert.NotEqual(0, apc.LowerBand); } [Fact] public void CalculatesCorrectEma() { var apc = new Apchannel(0.5); // Alpha = 0.5 for easier manual calculation var time = DateTime.UtcNow; // Bar 1: High = 110, Low = 90 apc.Add(new TBar(time, 100, 110, 90, 100, 1000)); Assert.Equal(110, apc.UpperBand, Tolerance); Assert.Equal(90, apc.LowerBand, Tolerance); // Bar 2: High = 120, Low = 80 // UpperEMA = 0.5 * 110 + 0.5 * 120 = 115 // LowerEMA = 0.5 * 90 + 0.5 * 80 = 85 apc.Add(new TBar(time.AddMinutes(1), 100, 120, 80, 100, 1000)); Assert.Equal(115, apc.UpperBand, Tolerance); Assert.Equal(85, apc.LowerBand, Tolerance); // Bar 3: High = 130, Low = 70 // UpperEMA = 0.5 * 115 + 0.5 * 130 = 122.5 // LowerEMA = 0.5 * 85 + 0.5 * 70 = 77.5 apc.Add(new TBar(time.AddMinutes(2), 100, 130, 70, 100, 1000)); Assert.Equal(122.5, apc.UpperBand, Tolerance); Assert.Equal(77.5, apc.LowerBand, Tolerance); } #endregion #region State Management & Bar Correction [Fact] public void Calc_IsNew_AcceptsParameter() { var apc = new Apchannel(0.2); var time = DateTime.UtcNow; apc.Add(new TBar(time, 100, 105, 95, 100, 1000), isNew: true); double value1 = apc.Last.Value; apc.Add(new TBar(time.AddMinutes(1), 102, 108, 96, 102, 1000), isNew: true); double value2 = apc.Last.Value; Assert.NotEqual(value1, value2); } [Fact] public void Calc_IsNew_False_UpdatesValue() { var apc = new Apchannel(0.2); var time = DateTime.UtcNow; apc.Add(new TBar(time, 100, 105, 95, 100, 1000)); apc.Add(new TBar(time.AddMinutes(1), 102, 108, 96, 102, 1000), isNew: true); double beforeUpdate = apc.Last.Value; apc.Add(new TBar(time.AddMinutes(1), 104, 110, 98, 104, 1000), isNew: false); double afterUpdate = apc.Last.Value; Assert.NotEqual(beforeUpdate, afterUpdate); } [Fact] public void IterativeCorrections_RestoreToOriginalState() { var apc = new Apchannel(0.2); var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42); // Feed 10 new bars TBar tenthBar = default; for (int i = 0; i < 10; i++) { tenthBar = gbm.Next(isNew: true); apc.Add(tenthBar, isNew: true); } // Remember state after 10 bars double stateAfterTen = apc.Last.Value; double upperAfterTen = apc.UpperBand; double lowerAfterTen = apc.LowerBand; // Generate 9 corrections with isNew=false for (int i = 0; i < 9; i++) { var bar = gbm.Next(isNew: false); apc.Add(bar, isNew: false); } // Feed the remembered 10th bar again with isNew=false var finalResult = apc.Add(tenthBar, isNew: false); // State should match the original state after 10 bars Assert.Equal(stateAfterTen, finalResult.Value, Tolerance); Assert.Equal(upperAfterTen, apc.UpperBand, Tolerance); Assert.Equal(lowerAfterTen, apc.LowerBand, Tolerance); } [Fact] public void Reset_ClearsState() { var apc = new Apchannel(0.2); var time = DateTime.UtcNow; apc.Add(new TBar(time, 100, 105, 95, 100, 1000)); apc.Add(new TBar(time.AddMinutes(1), 102, 108, 96, 102, 1000)); double valueBefore = apc.Last.Value; apc.Reset(); Assert.Equal(0, apc.Last.Value); Assert.Equal(0, apc.UpperBand); Assert.Equal(0, apc.LowerBand); Assert.False(apc.IsHot); // After reset, should accept new values apc.Add(new TBar(time, 50, 55, 45, 50, 1000)); Assert.NotEqual(0, apc.Last.Value); Assert.NotEqual(valueBefore, apc.Last.Value); } #endregion #region Warmup & Convergence [Fact] public void IsHot_BecomesTrueWhenConverged() { var apc = new Apchannel(0.2); var time = DateTime.UtcNow; int warmup = apc.WarmupPeriod; Assert.False(apc.IsHot); for (int i = 1; i < warmup; i++) { apc.Add(new TBar(time.AddMinutes(i), 100, 105, 95, 100, 1000)); Assert.False(apc.IsHot); } apc.Add(new TBar(time.AddMinutes(warmup), 100, 105, 95, 100, 1000)); Assert.True(apc.IsHot); } [Fact] public void IsHot_IsAlphaDependent() { double[] alphas = [0.1, 0.2, 0.5, 0.9]; int[] expectedSteps = new int[alphas.Length]; var time = DateTime.UtcNow; for (int i = 0; i < alphas.Length; i++) { double alpha = alphas[i]; var apc = new Apchannel(alpha); expectedSteps[i] = apc.WarmupPeriod; int steps = 0; while (!apc.IsHot && steps < 1000) { apc.Add(new TBar(time.AddMinutes(steps), 100, 105, 95, 100, 1000)); steps++; } } // Warmup times should decrease as alpha increases (faster convergence) Assert.True(expectedSteps[0] > expectedSteps[1]); Assert.True(expectedSteps[1] > expectedSteps[2]); Assert.True(expectedSteps[2] > expectedSteps[3]); } [Fact] public void WarmupPeriod_IsSetCorrectly() { var apc1 = new Apchannel(0.1); Assert.Equal(30, apc1.WarmupPeriod); // ceil(3.0 / 0.1) = 30 var apc2 = new Apchannel(0.2); Assert.Equal(15, apc2.WarmupPeriod); // ceil(3.0 / 0.2) = 15 var apc3 = new Apchannel(0.5); Assert.Equal(6, apc3.WarmupPeriod); // ceil(3.0 / 0.5) = 6 } #endregion #region Robustness (NaN/Infinity) [Fact] public void NaN_Input_UsesLastValidValue() { var apc = new Apchannel(0.2); var time = DateTime.UtcNow; apc.Add(new TBar(time, 100, 105, 95, 100, 1000)); apc.Add(new TBar(time.AddMinutes(1), 102, 108, 96, 102, 1000)); var resultAfterNaN = apc.Add(new TBar(time.AddMinutes(2), double.NaN, double.NaN, double.NaN, double.NaN, 1000)); Assert.True(double.IsFinite(resultAfterNaN.Value)); Assert.True(double.IsFinite(apc.UpperBand)); Assert.True(double.IsFinite(apc.LowerBand)); Assert.NotEqual(0, resultAfterNaN.Value); } [Fact] public void Infinity_Input_UsesLastValidValue() { var apc = new Apchannel(0.2); var time = DateTime.UtcNow; apc.Add(new TBar(time, 100, 105, 95, 100, 1000)); apc.Add(new TBar(time.AddMinutes(1), 102, 108, 96, 102, 1000)); var resultPosInf = apc.Add(new TBar(time.AddMinutes(2), double.PositiveInfinity, double.PositiveInfinity, double.PositiveInfinity, double.PositiveInfinity, 1000)); Assert.True(double.IsFinite(resultPosInf.Value)); var resultNegInf = apc.Add(new TBar(time.AddMinutes(3), double.NegativeInfinity, double.NegativeInfinity, double.NegativeInfinity, double.NegativeInfinity, 1000)); Assert.True(double.IsFinite(resultNegInf.Value)); } [Fact] public void MultipleNaN_ContinuesWithLastValid() { var apc = new Apchannel(0.2); var time = DateTime.UtcNow; apc.Add(new TBar(time, 100, 105, 95, 100, 1000)); apc.Add(new TBar(time.AddMinutes(1), 102, 108, 96, 102, 1000)); apc.Add(new TBar(time.AddMinutes(2), 104, 110, 98, 104, 1000)); var r1 = apc.Add(new TBar(time.AddMinutes(3), double.NaN, double.NaN, double.NaN, double.NaN, 1000)); var r2 = apc.Add(new TBar(time.AddMinutes(4), double.NaN, double.NaN, double.NaN, double.NaN, 1000)); var r3 = apc.Add(new TBar(time.AddMinutes(5), double.NaN, double.NaN, double.NaN, double.NaN, 1000)); Assert.True(double.IsFinite(r1.Value)); Assert.True(double.IsFinite(r2.Value)); Assert.True(double.IsFinite(r3.Value)); } #endregion #region Span API Tests [Fact] public void SpanCalculate_ValidatesInput() { double[] high = [105, 108, 110, 107, 109]; double[] low = [95, 96, 98, 97, 99]; double[] upperBand = new double[5]; double[] lowerBand = new double[5]; // Alpha must be > 0 and <= 1 Assert.Throws(() => Apchannel.Batch(high, low, upperBand, lowerBand, 0.0)); Assert.Throws(() => Apchannel.Batch(high, low, upperBand, lowerBand, 1.5)); // Arrays must be same length double[] wrongSizeLow = new double[3]; Assert.Throws(() => Apchannel.Batch(high, wrongSizeLow, upperBand, lowerBand, 0.2)); double[] wrongSizeUpper = new double[3]; Assert.Throws(() => Apchannel.Batch(high, low, wrongSizeUpper, lowerBand, 0.2)); double[] wrongSizeLower = new double[3]; Assert.Throws(() => Apchannel.Batch(high, low, upperBand, wrongSizeLower, 0.2)); } [Fact] public void SpanCalculate_MatchesIterativeCalc() { var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42); var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); double[] high = bars.Select(b => b.High).ToArray(); double[] low = bars.Select(b => b.Low).ToArray(); double[] upperBandSpan = new double[100]; double[] lowerBandSpan = new double[100]; // Calculate using span Apchannel.Batch(high, low, upperBandSpan, lowerBandSpan, 0.2); // Calculate iteratively var apc = new Apchannel(0.2); double[] upperBandIter = new double[100]; double[] lowerBandIter = new double[100]; for (int i = 0; i < 100; i++) { apc.Add(bars[i]); upperBandIter[i] = apc.UpperBand; lowerBandIter[i] = apc.LowerBand; } // Compare for (int i = 0; i < 100; i++) { Assert.Equal(upperBandIter[i], upperBandSpan[i], Tolerance); Assert.Equal(lowerBandIter[i], lowerBandSpan[i], Tolerance); } } [Fact] public void SpanCalculate_HandlesNaN() { double[] high = [105, double.NaN, 110, 107, double.PositiveInfinity]; double[] low = [95, 96, double.NaN, 97, double.NegativeInfinity]; double[] upperBand = new double[5]; double[] lowerBand = new double[5]; Apchannel.Batch(high, low, upperBand, lowerBand, 0.2); foreach (var val in upperBand) { Assert.True(double.IsFinite(val), $"Expected finite value but got {val}"); } foreach (var val in lowerBand) { Assert.True(double.IsFinite(val), $"Expected finite value but got {val}"); } } [Fact] public void SpanCalculate_ZeroAllocation() { double[] high = new double[10000]; double[] low = new double[10000]; double[] upperBand = new double[10000]; double[] lowerBand = new double[10000]; var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42); for (int i = 0; i < high.Length; i++) { var bar = gbm.Next(); high[i] = bar.High; low[i] = bar.Low; } // Warm up Apchannel.Batch(high, low, upperBand, lowerBand, 0.2); // Verify method completes without OOM or stack overflow Assert.True(double.IsFinite(upperBand[^1])); Assert.True(double.IsFinite(lowerBand[^1])); } #endregion #region Calculate Method Tests [Fact] public void Calculate_ReturnsCorrectResultsAndHotIndicator() { 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)); var (results, indicator) = Apchannel.Calculate(bars, 0.2); // Check results Assert.Equal(50, results.Count); Assert.True(double.IsFinite(results.Last.High)); Assert.True(double.IsFinite(results.Last.Low)); // Check indicator state Assert.True(indicator.IsHot); Assert.Equal(results.Last.High, indicator.UpperBand, Tolerance); Assert.Equal(results.Last.Low, indicator.LowerBand, Tolerance); Assert.Equal(15, indicator.WarmupPeriod); // ceil(3.0 / 0.2) // Verify indicator continues correctly var nextBar = gbm.Next(); indicator.Add(nextBar); Assert.True(double.IsFinite(indicator.Last.Value)); } #endregion #region Chainability Tests [Fact] public void Chainability_Works() { var source = new TBarSeries(); var apc = new Apchannel(source, 0.2); var time = DateTime.UtcNow; var bar = new TBar(time, 100, 105, 95, 100, 1000); source.Add(bar); Assert.Equal(105, apc.UpperBand); Assert.Equal(95, apc.LowerBand); Assert.Equal(100, apc.Last.Value); // (105 + 95) / 2 } [Fact] public void Pub_EventFires() { var apc = new Apchannel(0.2); bool eventFired = false; apc.Pub += (object? sender, in TValueEventArgs args) => eventFired = true; var time = DateTime.UtcNow; apc.Add(new TBar(time, 100, 105, 95, 100, 1000)); Assert.True(eventFired); } #endregion #region Indicator-Specific Tests [Fact] public void FlatLine_ReturnsSameValue() { var apc = new Apchannel(0.2); var time = DateTime.UtcNow; for (int i = 0; i < 20; i++) { apc.Add(new TBar(time.AddMinutes(i), 100, 105, 95, 100, 1000)); } // With flat high/low, bands should converge to input values Assert.Equal(105, apc.UpperBand, 1e-6); Assert.Equal(95, apc.LowerBand, 1e-6); Assert.Equal(100, apc.Last.Value, 1e-6); } [Fact] public void ChannelWidth_NarrowsWithHighAlpha() { var apc1 = new Apchannel(0.1); // Slower response var apc2 = new Apchannel(0.9); // Faster response var time = DateTime.UtcNow; // Feed same data to both for (int i = 0; i < 50; i++) { double price = 100 + (i % 2 == 0 ? 10 : -10); // Oscillating var bar = new TBar(time.AddMinutes(i), price, price + 5, price - 5, price, 1000); apc1.Add(bar); apc2.Add(bar); } double width1 = apc1.UpperBand - apc1.LowerBand; double width2 = apc2.UpperBand - apc2.LowerBand; // Higher alpha should track price more closely Assert.True(width2 < width1 * 1.5); // Some tolerance for oscillation } #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(); var values = new List(); 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(); var values = new List(); 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.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 }