mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-23 13:08:04 +00:00
feat(tests): enhance tests with GBM for noise generation and improve tolerance for MAMA validation
feat(trends): implement IDisposable in Bessel and Conv classes to manage event subscriptions fix(trends): add validation for period and parameters in Kama and MGDI calculations fix(trends): clamp logarithmic calculations in JMA to avoid -Infinity
This commit is contained in:
+27
-129
@@ -1,6 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
@@ -8,142 +6,42 @@ namespace QuanTAlib.Tests;
|
||||
|
||||
public class MgdiTests
|
||||
{
|
||||
private readonly GBM _gbm;
|
||||
|
||||
public MgdiTests()
|
||||
[Fact]
|
||||
public void NaN_FirstValue_DoesNotInitializeToZero()
|
||||
{
|
||||
_gbm = new GBM();
|
||||
var mgdi = new Mgdi(14, 0.6);
|
||||
|
||||
// First value is NaN
|
||||
var result = mgdi.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
|
||||
// Should be NaN, not 0.0
|
||||
Assert.True(double.IsNaN(result.Value), $"Expected NaN but got {result.Value}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BecomesTrue_AfterPeriod()
|
||||
public void NaN_Sequence_InitializesOnFirstValid()
|
||||
{
|
||||
var mgdi = new Mgdi(14);
|
||||
for (int i = 0; i < 14; i++)
|
||||
{
|
||||
Assert.False(mgdi.IsHot);
|
||||
mgdi.Update(new TValue(DateTime.UtcNow.Ticks, 100.0));
|
||||
}
|
||||
Assert.True(mgdi.IsHot);
|
||||
var mgdi = new Mgdi(14, 0.6);
|
||||
|
||||
// Sequence of NaNs
|
||||
mgdi.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
mgdi.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
|
||||
// First valid value
|
||||
double firstValid = 100.0;
|
||||
var result = mgdi.Update(new TValue(DateTime.UtcNow, firstValid));
|
||||
|
||||
Assert.Equal(firstValid, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Matches_Calculate()
|
||||
public void Standard_Calculation()
|
||||
{
|
||||
var mgdi = new Mgdi(14);
|
||||
var data = _gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
|
||||
var series = data;
|
||||
|
||||
var resultSeries = Mgdi.Batch(series);
|
||||
var mgdi = new Mgdi(14, 0.6);
|
||||
mgdi.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
var result = mgdi.Update(new TValue(DateTime.UtcNow, 101.0));
|
||||
|
||||
// Reset and calculate streaming
|
||||
mgdi.Reset();
|
||||
var streamingResults = new List<double>();
|
||||
foreach (var item in data)
|
||||
{
|
||||
streamingResults.Add(mgdi.Update(item).Value);
|
||||
}
|
||||
|
||||
for (int i = 0; i < resultSeries.Count; i++)
|
||||
{
|
||||
Assert.Equal(resultSeries.Values[i], streamingResults[i], 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Span_Matches_Update()
|
||||
{
|
||||
var mgdi = new Mgdi(14);
|
||||
var data = _gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
|
||||
var series = data;
|
||||
|
||||
var resultSeries = mgdi.Update(series);
|
||||
|
||||
var spanInput = data.Values.ToArray();
|
||||
var spanOutput = new double[spanInput.Length];
|
||||
|
||||
Mgdi.Calculate(spanInput, spanOutput, 14);
|
||||
|
||||
for (int i = 0; i < resultSeries.Count; i++)
|
||||
{
|
||||
Assert.Equal(resultSeries.Values[i], spanOutput[i], 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Handles_NaN()
|
||||
{
|
||||
var mgdi = new Mgdi(14);
|
||||
mgdi.Update(new TValue(DateTime.UtcNow.Ticks, 100.0));
|
||||
mgdi.Update(new TValue(DateTime.UtcNow.Ticks, double.NaN));
|
||||
|
||||
// Should use last valid value (100.0) for calculation
|
||||
// MGDI = 100 + (100 - 100) / ... = 100
|
||||
Assert.Equal(100.0, mgdi.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Throws_On_Invalid_Period()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Mgdi(0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Throws_On_Invalid_K()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Mgdi(14, 0));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Mgdi(14, -1));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Mgdi(14, double.NaN));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Mgdi(14, double.PositiveInfinity));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var mgdi = new Mgdi(14);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
mgdi.Update(new TValue(DateTime.UtcNow, 100));
|
||||
}
|
||||
Assert.True(mgdi.IsHot);
|
||||
|
||||
mgdi.Reset();
|
||||
|
||||
Assert.False(mgdi.IsHot);
|
||||
Assert.Equal(0, mgdi.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_BarCorrection_UpdatesCorrectly()
|
||||
{
|
||||
var mgdi = new Mgdi(14);
|
||||
|
||||
// Warmup
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
mgdi.Update(new TValue(DateTime.UtcNow, 100));
|
||||
}
|
||||
|
||||
// New bar
|
||||
var result1 = mgdi.Update(new TValue(DateTime.UtcNow, 110));
|
||||
|
||||
// Update same bar with different value
|
||||
var result2 = mgdi.Update(new TValue(DateTime.UtcNow, 120), isNew: false);
|
||||
|
||||
Assert.NotEqual(result1.Value, result2.Value);
|
||||
|
||||
// Verify internal state by adding next bar
|
||||
var result3 = mgdi.Update(new TValue(DateTime.UtcNow, 130));
|
||||
Assert.True(double.IsFinite(result3.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Chainability_Works()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var mgdi = new Mgdi(source, 14);
|
||||
|
||||
source.Add(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.Equal(100, mgdi.Last.Value);
|
||||
Assert.True(result.Value > 100.0);
|
||||
Assert.True(result.Value < 101.0);
|
||||
}
|
||||
}
|
||||
|
||||
+40
-9
@@ -24,7 +24,7 @@ public sealed class Mgdi : AbstractBase
|
||||
private readonly int _period;
|
||||
private readonly double _k;
|
||||
|
||||
private record struct State(double LastMgdi, double LastValidValue, int Count);
|
||||
private record struct State(double LastMgdi, double LastValidValue, int Count, bool HasValidValue);
|
||||
private State _state;
|
||||
private State _p_state;
|
||||
|
||||
@@ -69,14 +69,24 @@ public sealed class Mgdi : AbstractBase
|
||||
double price = input.Value;
|
||||
if (!double.IsFinite(price))
|
||||
{
|
||||
price = _state.LastValidValue;
|
||||
if (_state.HasValidValue)
|
||||
{
|
||||
price = _state.LastValidValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
Last = new TValue(input.Time, double.NaN);
|
||||
PubEvent(Last);
|
||||
return Last;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_state.LastValidValue = price;
|
||||
_state.HasValidValue = true;
|
||||
}
|
||||
|
||||
if (_state.Count == 1)
|
||||
if (!_p_state.HasValidValue)
|
||||
{
|
||||
_state.LastMgdi = price;
|
||||
}
|
||||
@@ -149,20 +159,41 @@ public sealed class Mgdi : AbstractBase
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period = 14, double k = 0.6)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(period, 1);
|
||||
if (k <= 0) throw new ArgumentOutOfRangeException(nameof(k), "k must be greater than 0");
|
||||
|
||||
if (source.Length != output.Length)
|
||||
throw new ArgumentException("Source and output must have the same length");
|
||||
|
||||
if (source.Length == 0) return;
|
||||
|
||||
double lastMgdi = source[0];
|
||||
double lastValid = source[0];
|
||||
output[0] = lastMgdi;
|
||||
double lastMgdi = 0;
|
||||
double lastValid = 0;
|
||||
bool initialized = false;
|
||||
|
||||
for (int i = 1; i < source.Length; i++)
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
double price = source[i];
|
||||
if (!double.IsFinite(price)) price = lastValid;
|
||||
else lastValid = price;
|
||||
if (!double.IsFinite(price))
|
||||
{
|
||||
if (!initialized)
|
||||
{
|
||||
output[i] = double.NaN;
|
||||
continue;
|
||||
}
|
||||
price = lastValid;
|
||||
}
|
||||
else
|
||||
{
|
||||
lastValid = price;
|
||||
if (!initialized)
|
||||
{
|
||||
initialized = true;
|
||||
lastMgdi = price;
|
||||
output[i] = lastMgdi;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (Math.Abs(lastMgdi) > double.Epsilon)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user