docs: remove C# Implementation Considerations sections, clean up temp scripts, reorganize test files

- Remove 'C# Implementation Considerations' sections from 34 indicator .md files
- Delete 29 temp PowerShell scripts (_fix_mojibake.ps1, _hex_scan.ps1, etc.)
- Move test files into tests/ subdirectories for consistent project structure
- Add trader-focused bullet points to indicator documentation
This commit is contained in:
Miha Kralj
2026-03-12 12:34:16 -07:00
parent 8937b0c0fa
commit 060649192f
1149 changed files with 1780 additions and 3316 deletions
@@ -0,0 +1,149 @@
using TradingPlatform.BusinessLayer;
using Xunit;
namespace QuanTAlib.Tests;
public class ImpulseIndicatorTests
{
[Fact]
public void Constructor_CreatesValidIndicator()
{
var indicator = new ImpulseIndicator();
Assert.NotNull(indicator);
Assert.Equal("Elder Impulse System", indicator.Name);
}
[Fact]
public void Constructor_SetsDescription()
{
var indicator = new ImpulseIndicator();
Assert.Contains("Elder", indicator.Description, StringComparison.Ordinal);
Assert.Contains("Impulse", indicator.Description, StringComparison.Ordinal);
}
[Fact]
public void DefaultParameters_AreCorrect()
{
var indicator = new ImpulseIndicator();
Assert.Equal(13, indicator.EmaPeriod);
Assert.Equal(12, indicator.MacdFast);
Assert.Equal(26, indicator.MacdSlow);
Assert.Equal(9, indicator.MacdSignal);
}
[Fact]
public void DefaultShowColdValues_IsTrue()
{
var indicator = new ImpulseIndicator();
Assert.True(indicator.ShowColdValues);
}
[Fact]
public void ShortName_IncludesParameters()
{
var indicator = new ImpulseIndicator { EmaPeriod = 8, MacdFast = 5, MacdSlow = 20, MacdSignal = 7 };
Assert.Equal("IMPULSE(8,5,20,7)", indicator.ShortName);
}
[Fact]
public void MinHistoryDepths_EqualsZero()
{
var indicator = new ImpulseIndicator();
Assert.Equal(0, ImpulseIndicator.MinHistoryDepths);
IWatchlistIndicator watchlistIndicator = indicator;
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
}
[Fact]
public void SeparateWindow_IsFalse()
{
var indicator = new ImpulseIndicator();
Assert.False(indicator.SeparateWindow);
}
[Fact]
public void OnBackGround_IsTrue()
{
var indicator = new ImpulseIndicator();
Assert.True(indicator.OnBackGround);
}
[Fact]
public void Constructor_AddsOneLineSeries()
{
var indicator = new ImpulseIndicator();
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void Parameters_CanBeChanged()
{
var indicator = new ImpulseIndicator();
indicator.EmaPeriod = 20;
indicator.MacdFast = 8;
indicator.MacdSlow = 30;
indicator.MacdSignal = 5;
Assert.Equal(20, indicator.EmaPeriod);
Assert.Equal(8, indicator.MacdFast);
Assert.Equal(30, indicator.MacdSlow);
Assert.Equal(5, indicator.MacdSignal);
}
[Fact]
public void ShowColdValues_CanBeChanged()
{
var indicator = new ImpulseIndicator();
indicator.ShowColdValues = false;
Assert.False(indicator.ShowColdValues);
}
[Fact]
public void Initialize_CreatesInternalIndicator()
{
var indicator = new ImpulseIndicator { EmaPeriod = 13 };
indicator.Initialize();
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new ImpulseIndicator { EmaPeriod = 13 };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 40; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
}
double value = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(value));
}
[Fact]
public void ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new ImpulseIndicator { EmaPeriod = 13 };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 20; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
}
indicator.HistoricalData.AddBar(now.AddMinutes(20), 120, 130, 110, 125);
var newArgs = new UpdateArgs(UpdateReason.NewBar);
indicator.ProcessUpdate(newArgs);
double value = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(value));
}
}
+544
View File
@@ -0,0 +1,544 @@
// 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<ArgumentException>(() => new Impulse(emaPeriod: 0));
Assert.Equal("emaPeriod", ex.ParamName);
}
[Fact]
public void Constructor_InvalidMacdFast_Throws()
{
var ex = Assert.Throws<ArgumentException>(() => new Impulse(macdFast: 0));
Assert.Equal("macdFast", ex.ParamName);
}
[Fact]
public void Constructor_InvalidMacdSlow_Throws()
{
var ex = Assert.Throws<ArgumentException>(() => new Impulse(macdSlow: 0));
Assert.Equal("macdSlow", ex.ParamName);
}
[Fact]
public void Constructor_InvalidMacdSignal_Throws()
{
var ex = Assert.Throws<ArgumentException>(() => 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<long>(count);
var values = new List<double>(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<long>(50);
var values = new List<double>(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);
}
}
@@ -0,0 +1,195 @@
// IMPULSE Validation Tests - Elder Impulse System
// Self-consistency validation (no external library implements Elder Impulse directly)
using Xunit;
namespace QuanTAlib.Tests;
public class ImpulseValidationTests
{
private const int DataCount = 200;
private static (TSeries Series, GBM Gbm) CreateTestData()
{
var gbm = new GBM();
var time = DateTime.UtcNow;
var times = new List<long>(DataCount);
var values = new List<double>(DataCount);
for (int i = 0; i < DataCount; i++)
{
times.Add(time.AddMinutes(i).Ticks);
values.Add(gbm.Next().Close);
}
return (new TSeries(times, values), gbm);
}
// ═══════════════════════════════════════════════════════════════════
// Streaming vs Batch consistency
// ═══════════════════════════════════════════════════════════════════
[Fact]
public void StreamingMatchesBatch()
{
var (series, _) = CreateTestData();
// Batch
var batchResults = Impulse.Batch(series);
// Streaming
var streaming = new Impulse();
var streamValues = new double[DataCount];
for (int i = 0; i < DataCount; i++)
{
streaming.Update(series[i], isNew: true);
streamValues[i] = streaming.Last.Value;
}
for (int i = 0; i < DataCount; i++)
{
Assert.Equal(batchResults.Values[i], streamValues[i], 10);
}
}
// ═══════════════════════════════════════════════════════════════════
// Component identity: EMA output matches standalone EMA
// ═══════════════════════════════════════════════════════════════════
[Fact]
public void EmaOutput_MatchesStandaloneEma()
{
var (series, _) = CreateTestData();
var impulse = new Impulse();
var ema = new Ema(13);
for (int i = 0; i < DataCount; i++)
{
var impulseResult = impulse.Update(series[i], isNew: true);
var emaResult = ema.Update(series[i], isNew: true);
Assert.Equal(emaResult.Value, impulseResult.Value, 10);
}
}
// ═══════════════════════════════════════════════════════════════════
// Signal correctness: manual EMA + MACD verification
// ═══════════════════════════════════════════════════════════════════
[Fact]
public void Signal_MatchesManualEmaAndMacdComparison()
{
var (series, _) = CreateTestData();
var impulse = new Impulse();
var ema = new Ema(13);
var macd = new Macd(12, 26, 9);
double prevEma = 0;
double prevHist = 0;
bool hasPrev = false;
for (int i = 0; i < DataCount; i++)
{
impulse.Update(series[i], isNew: true);
ema.Update(series[i], isNew: true);
macd.Update(series[i], isNew: true);
double curEma = ema.Last.Value;
double curHist = macd.Histogram.Value;
if (hasPrev && impulse.IsHot)
{
bool emaRising = curEma > prevEma;
bool emaFalling = curEma < prevEma;
bool histRising = curHist > prevHist;
bool histFalling = curHist < prevHist;
int expectedSignal;
if (emaRising && histRising)
{
expectedSignal = 1;
}
else if (emaFalling && histFalling)
{
expectedSignal = -1;
}
else
{
expectedSignal = 0;
}
Assert.Equal(expectedSignal, impulse.Signal);
}
prevEma = curEma;
prevHist = curHist;
hasPrev = true;
}
}
// ═══════════════════════════════════════════════════════════════════
// Determinism: same input produces same output
// ═══════════════════════════════════════════════════════════════════
[Fact]
public void Determinism_SameInputSameOutput()
{
var time = DateTime.UtcNow;
var values = new double[100];
var rng = new GBM(seed: 42);
for (int i = 0; i < 100; i++)
{
values[i] = rng.Next().Close;
}
var impulse1 = new Impulse();
var impulse2 = new Impulse();
for (int i = 0; i < 100; i++)
{
var tv = new TValue(time.AddMinutes(i).Ticks, values[i]);
impulse1.Update(tv, isNew: true);
impulse2.Update(tv, isNew: true);
Assert.Equal(impulse1.Last.Value, impulse2.Last.Value, 12);
Assert.Equal(impulse1.Signal, impulse2.Signal);
}
}
// ═══════════════════════════════════════════════════════════════════
// Directional correctness
// ═══════════════════════════════════════════════════════════════════
[Fact]
public void SteadyUptrend_ProducesBullishSignals()
{
var impulse = new Impulse();
var time = DateTime.UtcNow;
// Exponential uptrend must produce at least one bullish signal
bool seenBullish = false;
for (int i = 0; i < 100; i++)
{
impulse.Update(new TValue(time.AddMinutes(i).Ticks, 100.0 * Math.Exp(0.02 * i)), isNew: true);
if (impulse.Signal == 1) { seenBullish = true; }
}
Assert.True(seenBullish, "Exponential uptrend should produce at least one bullish signal");
}
[Fact]
public void SteadyDowntrend_ProducesBearishSignals()
{
var impulse = new Impulse();
var time = DateTime.UtcNow;
// Exponential downtrend must produce at least one bearish signal
bool seenBearish = false;
for (int i = 0; i < 100; i++)
{
impulse.Update(new TValue(time.AddMinutes(i).Ticks, 200.0 * Math.Exp(-0.02 * i)), isNew: true);
if (impulse.Signal == -1) { seenBearish = true; }
}
Assert.True(seenBearish, "Exponential downtrend should produce at least one bearish signal");
}
}