Files
Miha Kralj 060649192f 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
2026-03-12 12:34:16 -07:00

545 lines
18 KiB
C#

// 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);
}
}