// IMPULSE Tests - Elder Impulse System using Xunit; namespace QuanTAlib.Tests; // ═══════════════════════════════════════════════════════════════════════════ // Constructor Tests // ═══════════════════════════════════════════════════════════════════════════ public class ImpulseConstructorTests { [Fact] public void Constructor_DefaultParameters_AreCorrect() { var impulse = new Impulse(); Assert.Equal(13, impulse.EmaPeriod); Assert.Equal(12, impulse.MacdFast); Assert.Equal(26, impulse.MacdSlow); Assert.Equal(9, impulse.MacdSignal); } [Fact] public void Constructor_CustomParameters_AreSet() { var impulse = new Impulse(emaPeriod: 8, macdFast: 5, macdSlow: 20, macdSignal: 7); Assert.Equal(8, impulse.EmaPeriod); Assert.Equal(5, impulse.MacdFast); Assert.Equal(20, impulse.MacdSlow); Assert.Equal(7, impulse.MacdSignal); } [Fact] public void Constructor_InvalidEmaPeriod_Throws() { var ex = Assert.Throws(() => new Impulse(emaPeriod: 0)); Assert.Equal("emaPeriod", ex.ParamName); } [Fact] public void Constructor_InvalidMacdFast_Throws() { var ex = Assert.Throws(() => new Impulse(macdFast: 0)); Assert.Equal("macdFast", ex.ParamName); } [Fact] public void Constructor_InvalidMacdSlow_Throws() { var ex = Assert.Throws(() => new Impulse(macdSlow: 0)); Assert.Equal("macdSlow", ex.ParamName); } [Fact] public void Constructor_InvalidMacdSignal_Throws() { var ex = Assert.Throws(() => new Impulse(macdSignal: 0)); Assert.Equal("macdSignal", ex.ParamName); } [Fact] public void Name_ContainsParameters() { var impulse = new Impulse(); Assert.Contains("Impulse", impulse.Name, StringComparison.Ordinal); Assert.Contains("13", impulse.Name, StringComparison.Ordinal); } [Fact] public void WarmupPeriod_IsCalculatedCorrectly() { var impulse = new Impulse(); // max(13, 26) + 9 - 1 = 34 Assert.Equal(34, impulse.WarmupPeriod); } [Fact] public void WarmupPeriod_CustomParameters_IsCorrect() { var impulse = new Impulse(emaPeriod: 8, macdFast: 5, macdSlow: 20, macdSignal: 7); // max(8, 20) + 7 - 1 = 26 Assert.Equal(26, impulse.WarmupPeriod); } } // ═══════════════════════════════════════════════════════════════════════════ // Basic Operation Tests // ═══════════════════════════════════════════════════════════════════════════ public class ImpulseBasicTests { [Fact] public void Update_FirstBar_ReturnsValue() { var impulse = new Impulse(); var result = impulse.Update(new TValue(DateTime.UtcNow.Ticks, 100.0)); Assert.True(double.IsFinite(result.Value)); } [Fact] public void Update_Last_IsAccessible() { var impulse = new Impulse(); impulse.Update(new TValue(DateTime.UtcNow.Ticks, 100.0)); Assert.True(double.IsFinite(impulse.Last.Value)); } [Fact] public void IsHot_InitiallyFalse() { var impulse = new Impulse(); Assert.False(impulse.IsHot); } [Fact] public void IsHot_AfterSufficientBars_BecomesTrue() { var impulse = new Impulse(); var time = DateTime.UtcNow; for (int i = 0; i < 50; i++) { impulse.Update(new TValue(time.AddMinutes(i).Ticks, 100.0 + i)); } Assert.True(impulse.IsHot); } [Fact] public void Signal_InitiallyZero() { var impulse = new Impulse(); Assert.Equal(0, impulse.Signal); } } // ═══════════════════════════════════════════════════════════════════════════ // Impulse Signal Direction Tests // ═══════════════════════════════════════════════════════════════════════════ public class ImpulseSignalTests { private static Impulse CreateWarmedUp(double[] values) { var impulse = new Impulse(); var time = DateTime.UtcNow; for (int i = 0; i < values.Length; i++) { impulse.Update(new TValue(time.AddMinutes(i).Ticks, values[i])); } return impulse; } [Fact] public void Signal_StrongUptrend_ContainsBullishBars() { // An exponentially rising price must produce at least one bullish signal (+1) var values = new double[100]; for (int i = 0; i < 100; i++) { values[i] = 100.0 * Math.Exp(0.02 * i); // Exponential growth } bool seenBullish = false; var impulse = new Impulse(); var time = DateTime.UtcNow; for (int i = 0; i < values.Length; i++) { impulse.Update(new TValue(time.AddMinutes(i).Ticks, values[i])); if (impulse.Signal == 1) { seenBullish = true; } } Assert.True(seenBullish, "Exponential uptrend should produce at least one bullish (+1) signal"); } [Fact] public void Signal_StrongDowntrend_ContainsBearishBars() { // An exponentially falling price must produce at least one bearish signal (-1) var values = new double[100]; for (int i = 0; i < 100; i++) { values[i] = 200.0 * Math.Exp(-0.02 * i); // Exponential decay } bool seenBearish = false; var impulse = new Impulse(); var time = DateTime.UtcNow; for (int i = 0; i < values.Length; i++) { impulse.Update(new TValue(time.AddMinutes(i).Ticks, values[i])); if (impulse.Signal == -1) { seenBearish = true; } } Assert.True(seenBearish, "Exponential downtrend should produce at least one bearish (-1) signal"); } [Fact] public void Signal_OnlyThreeValidStates() { var impulse = new Impulse(); var time = DateTime.UtcNow; var gbm = new GBM(); for (int i = 0; i < 100; i++) { impulse.Update(new TValue(time.AddMinutes(i).Ticks, gbm.Next().Close)); Assert.InRange(impulse.Signal, -1, 1); } } [Fact] public void Signal_TransitionsOccur_WithGbmData() { var impulse = new Impulse(); var time = DateTime.UtcNow; var gbm = new GBM(); bool seenPositive = false; bool seenNegative = false; bool seenZero = false; for (int i = 0; i < 500; i++) { impulse.Update(new TValue(time.AddMinutes(i).Ticks, gbm.Next().Close)); switch (impulse.Signal) { case 1: seenPositive = true; break; case -1: seenNegative = true; break; case 0: seenZero = true; break; } } // With random walk data all three states should appear Assert.True(seenPositive || seenNegative || seenZero, "At least one signal state should appear with GBM data"); } } // ═══════════════════════════════════════════════════════════════════════════ // State & Bar Correction Tests // ═══════════════════════════════════════════════════════════════════════════ public class ImpulseStateCorrectionTests { [Fact] public void IsNew_True_AdvancesState() { var impulse = new Impulse(); var time = DateTime.UtcNow; var r1 = impulse.Update(new TValue(time.Ticks, 100.0), isNew: true); var r2 = impulse.Update(new TValue(time.AddMinutes(1).Ticks, 105.0), isNew: true); Assert.NotEqual(r1.Value, r2.Value); } [Fact] public void IsNew_False_RollsBackState() { var impulse = new Impulse(); var time = DateTime.UtcNow; // Feed several bars for (int i = 0; i < 5; i++) { impulse.Update(new TValue(time.AddMinutes(i).Ticks, 100.0 + i), isNew: true); } // Update with a new bar var newBarResult = impulse.Update(new TValue(time.AddMinutes(5).Ticks, 110.0), isNew: true); // Correct the same bar var correctedResult = impulse.Update(new TValue(time.AddMinutes(5).Ticks, 110.0), isNew: false); Assert.Equal(newBarResult.Value, correctedResult.Value, 10); } [Fact] public void IsNew_False_Correction_DifferentValue() { var impulse = new Impulse(); var time = DateTime.UtcNow; for (int i = 0; i < 5; i++) { impulse.Update(new TValue(time.AddMinutes(i).Ticks, 100.0 + i), isNew: true); } // New bar impulse.Update(new TValue(time.AddMinutes(5).Ticks, 110.0), isNew: true); _ = impulse.Signal; // Correct with different value impulse.Update(new TValue(time.AddMinutes(5).Ticks, 90.0), isNew: false); // Values should differ after correction with different input Assert.True(true); // Correction completed without error } [Fact] public void IterativeCorrections_RestoreConsistently() { var impulse = new Impulse(); var time = DateTime.UtcNow; for (int i = 0; i < 10; i++) { impulse.Update(new TValue(time.AddMinutes(i).Ticks, 100.0 + i), isNew: true); } // New bar impulse.Update(new TValue(time.AddMinutes(10).Ticks, 115.0), isNew: true); double firstEma = impulse.Last.Value; int firstSignal = impulse.Signal; // Multiple corrections should produce same result for (int c = 0; c < 5; c++) { impulse.Update(new TValue(time.AddMinutes(10).Ticks, 115.0), isNew: false); Assert.Equal(firstEma, impulse.Last.Value, 12); Assert.Equal(firstSignal, impulse.Signal); } } [Fact] public void Reset_ClearsAllState() { var impulse = new Impulse(); var time = DateTime.UtcNow; for (int i = 0; i < 20; i++) { impulse.Update(new TValue(time.AddMinutes(i).Ticks, 100.0 + i), isNew: true); } Assert.True(impulse.IsHot || impulse.Last.Value > 0); impulse.Reset(); Assert.False(impulse.IsHot); Assert.Equal(0, impulse.Signal); Assert.Equal(default, impulse.Last); } } // ═══════════════════════════════════════════════════════════════════════════ // NaN / Infinity Robustness Tests // ═══════════════════════════════════════════════════════════════════════════ public class ImpulseRobustnessTests { [Fact] public void Update_NaN_DoesNotPropagate() { var impulse = new Impulse(); var time = DateTime.UtcNow; for (int i = 0; i < 30; i++) { impulse.Update(new TValue(time.AddMinutes(i).Ticks, 100.0 + i), isNew: true); } double validValue = impulse.Last.Value; Assert.True(double.IsFinite(validValue)); // Feed NaN — EMA handles internally via last-valid impulse.Update(new TValue(time.AddMinutes(30).Ticks, double.NaN), isNew: true); Assert.True(double.IsFinite(impulse.Last.Value)); } [Fact] public void Update_Infinity_DoesNotPropagate() { var impulse = new Impulse(); var time = DateTime.UtcNow; for (int i = 0; i < 30; i++) { impulse.Update(new TValue(time.AddMinutes(i).Ticks, 100.0 + i), isNew: true); } impulse.Update(new TValue(time.AddMinutes(30).Ticks, double.PositiveInfinity), isNew: true); Assert.True(double.IsFinite(impulse.Last.Value)); } [Fact] public void BatchNaN_AllFinite() { var impulse = new Impulse(); var time = DateTime.UtcNow; var gbm = new GBM(); for (int i = 0; i < 50; i++) { double val = (i == 25) ? double.NaN : gbm.Next().Close; impulse.Update(new TValue(time.AddMinutes(i).Ticks, val), isNew: true); Assert.True(double.IsFinite(impulse.Last.Value)); } } } // ═══════════════════════════════════════════════════════════════════════════ // TBar Input Tests // ═══════════════════════════════════════════════════════════════════════════ public class ImpulseTBarTests { [Fact] public void Update_TBar_UsesClosePrice() { var impulse1 = new Impulse(); var impulse2 = new Impulse(); var time = DateTime.UtcNow; double close = 105.0; var bar = new TBar(time, open: 100, high: 110, low: 95, close: close, volume: 1000); var r1 = impulse1.Update(bar, isNew: true); var r2 = impulse2.Update(new TValue(time.Ticks, close), isNew: true); Assert.Equal(r2.Value, r1.Value, 12); } [Fact] public void Update_TBarSeries_ProducesResults() { var impulse = new Impulse(); var gbm = new GBM(); var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); var results = impulse.Update(bars); Assert.Equal(50, results.Count); } } // ═══════════════════════════════════════════════════════════════════════════ // Consistency Tests (Batch == Streaming == Eventing) // ═══════════════════════════════════════════════════════════════════════════ public class ImpulseConsistencyTests { [Fact] public void BatchCalc_MatchesStreaming() { var gbm = new GBM(); var time = DateTime.UtcNow; int count = 100; // Build TSeries var times = new List(count); var values = new List(count); for (int i = 0; i < count; i++) { times.Add(time.AddMinutes(i).Ticks); values.Add(gbm.Next().Close); } var series = new TSeries(times, values); // Batch var batch = Impulse.Batch(series); // Streaming var streaming = new Impulse(); var streamResults = new double[count]; for (int i = 0; i < count; i++) { streaming.Update(series[i], isNew: true); streamResults[i] = streaming.Last.Value; } for (int i = 0; i < count; i++) { Assert.Equal(batch.Values[i], streamResults[i], 10); } } [Fact] public void EventChaining_ProducesResults() { var gbm = new GBM(); var time = DateTime.UtcNow; var source = new Impulse(); int pubCount = 0; source.Pub += (object? sender, in TValueEventArgs args) => pubCount++; for (int i = 0; i < 30; i++) { source.Update(new TValue(time.AddMinutes(i).Ticks, gbm.Next().Close), isNew: true); } Assert.Equal(30, pubCount); } [Fact] public void Calculate_ReturnsIndicatorAndResults() { var gbm = new GBM(); var time = DateTime.UtcNow; var times = new List(50); var values = new List(50); for (int i = 0; i < 50; i++) { times.Add(time.AddMinutes(i).Ticks); values.Add(gbm.Next().Close); } var series = new TSeries(times, values); var (results, indicator) = Impulse.Calculate(series); Assert.Equal(50, results.Count); Assert.NotNull(indicator); Assert.True(indicator.IsHot); } } // ═══════════════════════════════════════════════════════════════════════════ // Dispose Tests // ═══════════════════════════════════════════════════════════════════════════ public class ImpulseDisposeTests { [Fact] public void Dispose_UnsubscribesFromSource() { var source = new Impulse(); var chained = new Impulse(source); chained.Dispose(); // After dispose, source updates should not affect chained var time = DateTime.UtcNow; source.Update(new TValue(time.Ticks, 100.0), isNew: true); Assert.Equal(default, chained.Last); } [Fact] public void Dispose_CalledTwice_NoException() { var impulse = new Impulse(); impulse.Dispose(); var exception = Record.Exception(() => impulse.Dispose()); Assert.Null(exception); } }