feat: implement last-value substitution for NaN/Infinity in Ema and EmaVector, enhance documentation and tests

This commit is contained in:
Miha Kralj
2025-11-29 13:19:04 -08:00
parent acac3e610c
commit 6cdebb984d
11 changed files with 506 additions and 29 deletions
+142
View File
@@ -135,4 +135,146 @@ public class EmaVectorTests
Assert.Equal(200.0, res[0].Value, 1e-9);
}
[Fact]
public void Update_NaN_Input_UsesLastValidValue()
{
int[] periods = { 10, 20 };
var emaVector = new EmaVector(periods);
// Feed some valid values
emaVector.Update(new TValue(DateTime.Now, 100.0));
emaVector.Update(new TValue(DateTime.Now, 110.0));
// Feed NaN - should use last valid value (110)
var resultAfterNaN = emaVector.Update(new TValue(DateTime.Now, double.NaN));
// All results should be finite (not NaN)
foreach (var result in resultAfterNaN)
{
Assert.True(double.IsFinite(result.Value), $"Expected finite value but got {result.Value}");
}
}
[Fact]
public void Update_Infinity_Input_UsesLastValidValue()
{
int[] periods = { 10, 20 };
var emaVector = new EmaVector(periods);
// Feed some valid values
emaVector.Update(new TValue(DateTime.Now, 100.0));
emaVector.Update(new TValue(DateTime.Now, 110.0));
// Feed positive infinity - should use last valid value
var resultAfterPosInf = emaVector.Update(new TValue(DateTime.Now, double.PositiveInfinity));
foreach (var result in resultAfterPosInf)
{
Assert.True(double.IsFinite(result.Value));
}
// Feed negative infinity - should use last valid value
var resultAfterNegInf = emaVector.Update(new TValue(DateTime.Now, double.NegativeInfinity));
foreach (var result in resultAfterNegInf)
{
Assert.True(double.IsFinite(result.Value));
}
}
[Fact]
public void Update_MultipleNaN_ContinuesWithLastValid()
{
int[] periods = { 5, 10 };
var emaVector = new EmaVector(periods);
// Feed valid values
emaVector.Update(new TValue(DateTime.Now, 100.0));
emaVector.Update(new TValue(DateTime.Now, 110.0));
emaVector.Update(new TValue(DateTime.Now, 120.0));
// Feed multiple NaN values
var r1 = emaVector.Update(new TValue(DateTime.Now, double.NaN));
var r2 = emaVector.Update(new TValue(DateTime.Now, double.NaN));
var r3 = emaVector.Update(new TValue(DateTime.Now, double.NaN));
// All results should be finite
foreach (var result in r1) Assert.True(double.IsFinite(result.Value));
foreach (var result in r2) Assert.True(double.IsFinite(result.Value));
foreach (var result in r3) Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Calculate_Series_HandlesNaN()
{
int[] periods = { 5, 10 };
var emaVector = new EmaVector(periods);
// Create series with NaN values interspersed
var t = new System.Collections.Generic.List<long>();
var v = new System.Collections.Generic.List<double>();
var now = DateTime.Now;
t.Add(now.Ticks); v.Add(100.0);
t.Add(now.AddMinutes(1).Ticks); v.Add(110.0);
t.Add(now.AddMinutes(2).Ticks); v.Add(double.NaN);
t.Add(now.AddMinutes(3).Ticks); v.Add(120.0);
t.Add(now.AddMinutes(4).Ticks); v.Add(double.PositiveInfinity);
t.Add(now.AddMinutes(5).Ticks); v.Add(130.0);
var series = new TSeries(t, v);
var results = emaVector.Calculate(series);
// All results should be finite for all periods
foreach (var periodResults in results)
{
foreach (var val in periodResults.Values)
{
Assert.True(double.IsFinite(val), $"Expected finite value but got {val}");
}
}
}
[Fact]
public void Reset_ClearsLastValidValue()
{
int[] periods = { 10 };
var emaVector = new EmaVector(periods);
// Feed values including NaN
emaVector.Update(new TValue(DateTime.Now, 100.0));
emaVector.Update(new TValue(DateTime.Now, double.NaN));
// Reset
emaVector.Reset();
// After reset, first valid value should establish new baseline
var result = emaVector.Update(new TValue(DateTime.Now, 50.0));
Assert.Equal(50.0, result[0].Value, 1e-9);
}
[Fact]
public void NaN_Handling_MatchesSingleEma()
{
int[] periods = { 5, 10, 20 };
var emaVector = new EmaVector(periods);
var emaSingles = periods.Select(p => new Ema(p)).ToArray();
// Data with NaN values
var values = new double[] { 10, 20, double.NaN, 40, double.PositiveInfinity, 60, 70 };
var time = DateTime.Now;
foreach (var val in values)
{
var tVal = new TValue(time, val);
var multiRes = emaVector.Update(tVal);
for (int i = 0; i < periods.Length; i++)
{
var singleRes = emaSingles[i].Update(tVal);
Assert.Equal(singleRes.Value, multiRes[i].Value, 1e-9);
}
time = time.AddMinutes(1);
}
}
}