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:
Miha Kralj
2025-12-25 20:18:14 -08:00
parent df598c810d
commit ac8b2dbb3f
20 changed files with 281 additions and 187 deletions
+45
View File
@@ -0,0 +1,45 @@
using System;
using Xunit;
using QuanTAlib;
namespace QuanTAlib.Tests;
public class JmaZeroDivTests
{
[Fact]
public void Period1_DoesNotProduceInfinityOrNaN()
{
// Arrange
var jma = new Jma(period: 1);
double[] values = { 100, 101, 102, 101, 100 };
// Act & Assert
foreach (var v in values)
{
var result = jma.Update(new TValue(DateTime.UtcNow, v));
Assert.False(double.IsNaN(result.Value), $"JMA(1) produced NaN for input {v}");
Assert.False(double.IsInfinity(result.Value), $"JMA(1) produced Infinity for input {v}");
// For period 1, JMA should ideally track price very closely
Assert.Equal(v, result.Value, precision: 1);
}
}
[Fact]
public void Period1_LogValuesAreFinite()
{
// This test inspects private fields via reflection or just checks behavior
// Since we can't easily access private fields, we'll rely on the calculation logic check
// If the fix is applied, we shouldn't see -Infinity in internal calculations if we could see them.
// But we can check if the output is exactly the input, which implies adapt=0 (if logic holds).
var jma = new Jma(period: 1);
var result = jma.Update(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100, result.Value);
result = jma.Update(new TValue(DateTime.UtcNow, 200));
// If adapt is 0 (due to -Infinity log), bands snap to price.
// If JMA(1) is identity, result should be 200.
// With clamping, adapt is slightly non-zero (approx 1e-12), so result is very close to 200.
Assert.Equal(200, result.Value, precision: 8);
}
}
+12 -7
View File
@@ -87,8 +87,9 @@ public sealed class Jma : AbstractBase
double sqrtDivider = sqrtParam / (sqrtParam + 1.0);
// Precompute logs for Math.Exp optimization
_logLengthDivider = Math.Log(_lengthDivider);
_logSqrtDivider = Math.Log(sqrtDivider);
// Clamp to avoid -Infinity when period=1 (dividers can be zero)
_logLengthDivider = Math.Log(Math.Max(_lengthDivider, 1e-12));
_logSqrtDivider = Math.Log(Math.Max(sqrtDivider, 1e-12));
// same warmup heuristic used in the AFL port (SetBarsRequired)
WarmupPeriod = (int)Math.Ceiling(20.0 + 80.0 * Math.Pow(period, 0.36));
@@ -130,10 +131,14 @@ public sealed class Jma : AbstractBase
if (isNew)
{
_p_state = _state;
_devBuffer.Snapshot();
_volBuffer.Snapshot();
}
else
{
_state = _p_state;
_devBuffer.Restore();
_volBuffer.Restore();
}
// --- Handle NaN/inf: reuse last finite price ---
@@ -169,11 +174,11 @@ public sealed class Jma : AbstractBase
double deviation = absValue + 1e-10;
// 2. 10-bar SMA of local deviation -> "volatility"
_devBuffer.Add(deviation, isNew);
_devBuffer.Add(deviation);
double volatility = _devBuffer.Average;
// 3. 128-bar volatility history + middle-65 trimmed mean
_volBuffer.Add(volatility, isNew);
_volBuffer.Add(volatility);
double refVolatility = CalculateTrimmedMean(volatility);
if (refVolatility <= 0.0)
@@ -263,8 +268,6 @@ public sealed class Jma : AbstractBase
vSpan[i] = j;
}
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
// Restore state by replaying history
// JMA needs a lot of history (128 bars for volatility).
Reset();
@@ -272,9 +275,11 @@ public sealed class Jma : AbstractBase
int startIndex = Math.Max(0, len - lookback);
for (int i = startIndex; i < len; i++)
{
Update(new TValue(source.Times[i], source.Values[i]));
Step(source.Values[i], true);
}
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
return new TSeries(t, v);
}