mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-07-30 10:37:44 +00:00
060649192f
- 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
545 lines
18 KiB
C#
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);
|
|
}
|
|
}
|