mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-17 18:18:04 +00:00
feat: implement last-value substitution for NaN/Infinity in Ema and EmaVector, enhance documentation and tests
This commit is contained in:
@@ -399,3 +399,9 @@ FodyWeavers.xsd
|
|||||||
|
|
||||||
# Cline Memory Bank - exclude from git
|
# Cline Memory Bank - exclude from git
|
||||||
memory-bank/
|
memory-bank/
|
||||||
|
|
||||||
|
# macOS resource forks and metadata
|
||||||
|
._*
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
|||||||
Binary file not shown.
@@ -44,7 +44,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<NoWarn>S1944,S2053,S2222,S2259,S2583,S2589,S3329,S3655,S3900,S3949,S3966,S4158,S4347,S5773,S6781</NoWarn>
|
<NoWarn>S1944,S2053,S2222,S2259,S2583,S2589,S3329,S3655,S3776,S3900,S3949,S3966,S4158,S4347,S5773,S6781</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
Binary file not shown.
@@ -216,3 +216,79 @@ for (int i = 0; i < periods.Length; i++)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Console.WriteLine($"\nAll Vectorized Stream/Batch values match: {allMatch}");
|
Console.WriteLine($"\nAll Vectorized Stream/Batch values match: {allMatch}");
|
||||||
|
|
||||||
|
#!markdown
|
||||||
|
|
||||||
|
## 5. Handling Invalid Values (NaN/Infinity)
|
||||||
|
|
||||||
|
Both `Ema` and `EmaVector` use **last-value substitution** for invalid inputs. When a non-finite value (NaN, PositiveInfinity, NegativeInfinity) is encountered, it is replaced with the last valid value. This provides output continuity instead of propagating invalid values through the calculation.
|
||||||
|
|
||||||
|
#!csharp
|
||||||
|
|
||||||
|
Console.WriteLine("\n--- Handling Invalid Values ---");
|
||||||
|
|
||||||
|
// Single EMA
|
||||||
|
var emaNaN = new Ema(10);
|
||||||
|
|
||||||
|
// Feed valid values first
|
||||||
|
emaNaN.Update(new TValue(DateTime.Now, 100.0));
|
||||||
|
emaNaN.Update(new TValue(DateTime.Now.AddMinutes(1), 110.0));
|
||||||
|
Console.WriteLine($"After valid values: {emaNaN.Value.Value:F2}");
|
||||||
|
|
||||||
|
// Feed NaN - should use last valid value (110)
|
||||||
|
var resultAfterNaN = emaNaN.Update(new TValue(DateTime.Now.AddMinutes(2), double.NaN));
|
||||||
|
Console.WriteLine($"After NaN input: {resultAfterNaN.Value:F2} (IsFinite: {double.IsFinite(resultAfterNaN.Value)})");
|
||||||
|
|
||||||
|
// Feed Infinity - should use last valid value (110)
|
||||||
|
var resultAfterInf = emaNaN.Update(new TValue(DateTime.Now.AddMinutes(3), double.PositiveInfinity));
|
||||||
|
Console.WriteLine($"After Infinity input: {resultAfterInf.Value:F2} (IsFinite: {double.IsFinite(resultAfterInf.Value)})");
|
||||||
|
|
||||||
|
// Continue with valid value
|
||||||
|
var resultAfterValid = emaNaN.Update(new TValue(DateTime.Now.AddMinutes(4), 120.0));
|
||||||
|
Console.WriteLine($"After valid value (120): {resultAfterValid.Value:F2}");
|
||||||
|
|
||||||
|
#!csharp
|
||||||
|
|
||||||
|
Console.WriteLine("\n--- Batch Processing with Invalid Values ---");
|
||||||
|
|
||||||
|
// Create series with NaN values interspersed
|
||||||
|
var seriesWithNaN = new TSeries();
|
||||||
|
seriesWithNaN.Add(DateTime.Now.Ticks, 100.0);
|
||||||
|
seriesWithNaN.Add(DateTime.Now.Ticks + 1, 110.0);
|
||||||
|
seriesWithNaN.Add(DateTime.Now.Ticks + 2, double.NaN);
|
||||||
|
seriesWithNaN.Add(DateTime.Now.Ticks + 3, 120.0);
|
||||||
|
seriesWithNaN.Add(DateTime.Now.Ticks + 4, double.PositiveInfinity);
|
||||||
|
seriesWithNaN.Add(DateTime.Now.Ticks + 5, 130.0);
|
||||||
|
|
||||||
|
var emaBatchNaN = new Ema(3);
|
||||||
|
var resultsWithNaN = emaBatchNaN.Update(seriesWithNaN);
|
||||||
|
|
||||||
|
Console.WriteLine("Input → Output:");
|
||||||
|
for (int i = 0; i < seriesWithNaN.Count; i++)
|
||||||
|
{
|
||||||
|
var input = seriesWithNaN[i].Value;
|
||||||
|
var output = resultsWithNaN[i].Value;
|
||||||
|
var inputStr = double.IsFinite(input) ? input.ToString("F2") : input.ToString();
|
||||||
|
Console.WriteLine($" {inputStr,-10} → {output:F2} (IsFinite: {double.IsFinite(output)})");
|
||||||
|
}
|
||||||
|
|
||||||
|
#!csharp
|
||||||
|
|
||||||
|
Console.WriteLine("\n--- Vectorized EMA with Invalid Values ---");
|
||||||
|
|
||||||
|
int[] periodsNaN = { 5, 10 };
|
||||||
|
var emaVectorNaN = new EmaVector(periodsNaN);
|
||||||
|
|
||||||
|
// Feed values including invalid ones
|
||||||
|
var inputsNaN = new double[] { 100, 110, double.NaN, 120, double.PositiveInfinity, 130 };
|
||||||
|
var time = DateTime.Now;
|
||||||
|
|
||||||
|
foreach (var val in inputsNaN)
|
||||||
|
{
|
||||||
|
var results = emaVectorNaN.Update(new TValue(time, val));
|
||||||
|
var inputStr = double.IsFinite(val) ? val.ToString("F2") : val.ToString();
|
||||||
|
Console.WriteLine($"Input: {inputStr,-10} → EMA(5): {results[0].Value:F2}, EMA(10): {results[1].Value:F2}");
|
||||||
|
time = time.AddMinutes(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("\nAll outputs are finite - invalid inputs were substituted with last valid values.");
|
||||||
|
|||||||
@@ -225,4 +225,106 @@ public class EmaTests
|
|||||||
|
|
||||||
Assert.Equal(100.0, result, 1e-10);
|
Assert.Equal(100.0, result, 1e-10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Ema_NaN_Input_UsesLastValidValue()
|
||||||
|
{
|
||||||
|
var ema = new Ema(10);
|
||||||
|
|
||||||
|
// Feed some valid values
|
||||||
|
ema.Update(new TValue(DateTime.Now, 100));
|
||||||
|
ema.Update(new TValue(DateTime.Now, 110));
|
||||||
|
double valueBeforeNaN = ema.Value;
|
||||||
|
|
||||||
|
// Feed NaN - should use last valid value (110)
|
||||||
|
var resultAfterNaN = ema.Update(new TValue(DateTime.Now, double.NaN));
|
||||||
|
|
||||||
|
// Result should be finite (not NaN)
|
||||||
|
Assert.True(double.IsFinite(resultAfterNaN.Value));
|
||||||
|
// EMA should continue to evolve (may differ slightly due to substitution)
|
||||||
|
Assert.NotEqual(0, resultAfterNaN.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Ema_Infinity_Input_UsesLastValidValue()
|
||||||
|
{
|
||||||
|
var ema = new Ema(10);
|
||||||
|
|
||||||
|
// Feed some valid values
|
||||||
|
ema.Update(new TValue(DateTime.Now, 100));
|
||||||
|
ema.Update(new TValue(DateTime.Now, 110));
|
||||||
|
|
||||||
|
// Feed positive infinity - should use last valid value
|
||||||
|
var resultAfterPosInf = ema.Update(new TValue(DateTime.Now, double.PositiveInfinity));
|
||||||
|
Assert.True(double.IsFinite(resultAfterPosInf.Value));
|
||||||
|
|
||||||
|
// Feed negative infinity - should use last valid value
|
||||||
|
var resultAfterNegInf = ema.Update(new TValue(DateTime.Now, double.NegativeInfinity));
|
||||||
|
Assert.True(double.IsFinite(resultAfterNegInf.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Ema_MultipleNaN_ContinuesWithLastValid()
|
||||||
|
{
|
||||||
|
var ema = new Ema(10);
|
||||||
|
|
||||||
|
// Feed valid values
|
||||||
|
ema.Update(new TValue(DateTime.Now, 100));
|
||||||
|
ema.Update(new TValue(DateTime.Now, 110));
|
||||||
|
ema.Update(new TValue(DateTime.Now, 120));
|
||||||
|
|
||||||
|
// Feed multiple NaN values
|
||||||
|
var r1 = ema.Update(new TValue(DateTime.Now, double.NaN));
|
||||||
|
var r2 = ema.Update(new TValue(DateTime.Now, double.NaN));
|
||||||
|
var r3 = ema.Update(new TValue(DateTime.Now, double.NaN));
|
||||||
|
|
||||||
|
// All results should be finite
|
||||||
|
Assert.True(double.IsFinite(r1.Value));
|
||||||
|
Assert.True(double.IsFinite(r2.Value));
|
||||||
|
Assert.True(double.IsFinite(r3.Value));
|
||||||
|
|
||||||
|
// EMA should converge toward last valid value (120) with repeated substitution
|
||||||
|
// Values should be getting closer to 120
|
||||||
|
Assert.True(r3.Value > r1.Value || Math.Abs(r3.Value - 120) < Math.Abs(r1.Value - 120));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Ema_BatchCalc_HandlesNaN()
|
||||||
|
{
|
||||||
|
var ema = new Ema(10);
|
||||||
|
|
||||||
|
// Create series with NaN values interspersed
|
||||||
|
var series = new TSeries();
|
||||||
|
series.Add(DateTime.Now.Ticks, 100);
|
||||||
|
series.Add(DateTime.Now.Ticks + 1, 110);
|
||||||
|
series.Add(DateTime.Now.Ticks + 2, double.NaN);
|
||||||
|
series.Add(DateTime.Now.Ticks + 3, 120);
|
||||||
|
series.Add(DateTime.Now.Ticks + 4, double.PositiveInfinity);
|
||||||
|
series.Add(DateTime.Now.Ticks + 5, 130);
|
||||||
|
|
||||||
|
var results = ema.Update(series);
|
||||||
|
|
||||||
|
// All results should be finite
|
||||||
|
foreach (var result in results)
|
||||||
|
{
|
||||||
|
Assert.True(double.IsFinite(result.Value), $"Expected finite value but got {result.Value}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Ema_Reset_ClearsLastValidValue()
|
||||||
|
{
|
||||||
|
var ema = new Ema(10);
|
||||||
|
|
||||||
|
// Feed values including NaN
|
||||||
|
ema.Update(new TValue(DateTime.Now, 100));
|
||||||
|
ema.Update(new TValue(DateTime.Now, double.NaN));
|
||||||
|
|
||||||
|
// Reset
|
||||||
|
ema.Reset();
|
||||||
|
|
||||||
|
// After reset, first valid value should establish new baseline
|
||||||
|
var result = ema.Update(new TValue(DateTime.Now, 50));
|
||||||
|
Assert.Equal(50.0, result.Value, 1e-10);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-7
@@ -6,9 +6,9 @@ namespace QuanTAlib;
|
|||||||
|
|
||||||
public struct EmaState
|
public struct EmaState
|
||||||
{
|
{
|
||||||
public double Ema;
|
public double Ema { get; set; }
|
||||||
public double E;
|
public double E { get; set; }
|
||||||
public bool IsHot;
|
public bool IsHot { get; set; }
|
||||||
|
|
||||||
public static EmaState New() => new() { Ema = 0, E = 1.0, IsHot = false };
|
public static EmaState New() => new() { Ema = 0, E = 1.0, IsHot = false };
|
||||||
}
|
}
|
||||||
@@ -26,6 +26,7 @@ public class Ema
|
|||||||
private readonly double _alpha;
|
private readonly double _alpha;
|
||||||
private EmaState _state = EmaState.New();
|
private EmaState _state = EmaState.New();
|
||||||
private EmaState _p_state = EmaState.New();
|
private EmaState _p_state = EmaState.New();
|
||||||
|
private double _lastValidValue;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates EMA with specified period.
|
/// Creates EMA with specified period.
|
||||||
@@ -62,22 +63,42 @@ public class Ema
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsHot => _state.IsHot;
|
public bool IsHot => _state.IsHot;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a valid input value, using last-value substitution for non-finite inputs.
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private double GetValidValue(double input)
|
||||||
|
{
|
||||||
|
if (double.IsFinite(input))
|
||||||
|
{
|
||||||
|
_lastValidValue = input;
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
return _lastValidValue;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Core EMA calculation kernel.
|
/// Core EMA calculation kernel.
|
||||||
|
/// Assumes input has already been validated via GetValidValue().
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static double Compute(double input, double alpha, ref EmaState state)
|
public static double Compute(double input, double alpha, ref EmaState state)
|
||||||
{
|
{
|
||||||
state.Ema += alpha * (input - state.Ema);
|
state.Ema += alpha * (input - state.Ema);
|
||||||
|
|
||||||
|
double result;
|
||||||
if (!state.IsHot)
|
if (!state.IsHot)
|
||||||
{
|
{
|
||||||
state.E *= (1.0 - alpha);
|
state.E *= (1.0 - alpha);
|
||||||
state.IsHot = state.E <= 1e-10;
|
state.IsHot = state.E <= 1e-10;
|
||||||
return state.Ema / (1.0 - state.E);
|
result = state.Ema / (1.0 - state.E);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = state.Ema;
|
||||||
}
|
}
|
||||||
|
|
||||||
return state.Ema;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -98,7 +119,9 @@ public class Ema
|
|||||||
_state = _p_state;
|
_state = _p_state;
|
||||||
}
|
}
|
||||||
|
|
||||||
double val = Compute(input.Value, _alpha, ref _state);
|
// Last-value substitution: replace non-finite inputs with last valid value
|
||||||
|
double val = GetValidValue(input.Value);
|
||||||
|
val = Compute(val, _alpha, ref _state);
|
||||||
Value = new TValue(input.Time, val);
|
Value = new TValue(input.Time, val);
|
||||||
return Value;
|
return Value;
|
||||||
}
|
}
|
||||||
@@ -126,7 +149,9 @@ public class Ema
|
|||||||
|
|
||||||
for (int i = 0; i < len; i++)
|
for (int i = 0; i < len; i++)
|
||||||
{
|
{
|
||||||
double val = Compute(sourceValues[i], _alpha, ref state);
|
// Last-value substitution: replace non-finite inputs with last valid value
|
||||||
|
double val = GetValidValue(sourceValues[i]);
|
||||||
|
val = Compute(val, _alpha, ref state);
|
||||||
tSpan[i] = sourceTimes[i];
|
tSpan[i] = sourceTimes[i];
|
||||||
vSpan[i] = val;
|
vSpan[i] = val;
|
||||||
}
|
}
|
||||||
@@ -158,6 +183,7 @@ public class Ema
|
|||||||
{
|
{
|
||||||
_state = EmaState.New();
|
_state = EmaState.New();
|
||||||
_p_state = _state;
|
_p_state = _state;
|
||||||
|
_lastValidValue = 0;
|
||||||
Value = default;
|
Value = default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,6 +103,35 @@ TSeries source = ...;
|
|||||||
TSeries[] seriesResults = emaVector.Calculate(source);
|
TSeries[] seriesResults = emaVector.Calculate(source);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Handling Invalid Values (NaN/Infinity)
|
||||||
|
|
||||||
|
Both `Ema` and `EmaVector` use **last-value substitution** for handling invalid inputs:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var ema = new Ema(10);
|
||||||
|
|
||||||
|
// Valid values establish baseline
|
||||||
|
ema.Update(new TValue(time, 100));
|
||||||
|
ema.Update(new TValue(time, 110));
|
||||||
|
|
||||||
|
// NaN or Infinity inputs are replaced with last valid value (110)
|
||||||
|
var result = ema.Update(new TValue(time, double.NaN));
|
||||||
|
Console.WriteLine(double.IsFinite(result.Value)); // true
|
||||||
|
|
||||||
|
// Works identically for batch operations
|
||||||
|
var series = new TSeries();
|
||||||
|
series.Add(time, 100);
|
||||||
|
series.Add(time + 1, double.NaN); // Will use 100
|
||||||
|
series.Add(time + 2, 120);
|
||||||
|
var results = ema.Update(series); // All values are finite
|
||||||
|
```
|
||||||
|
|
||||||
|
**Behavior:**
|
||||||
|
- When `NaN`, `PositiveInfinity`, or `NegativeInfinity` is encountered, the last valid value is substituted
|
||||||
|
- This provides output continuity instead of propagating invalid values
|
||||||
|
- Both scalar (`Ema`) and SIMD (`EmaVector`) implementations use identical logic
|
||||||
|
- `Reset()` clears the last valid value, so the next valid input establishes a new baseline
|
||||||
|
|
||||||
### Performance Characteristics
|
### Performance Characteristics
|
||||||
|
|
||||||
* **O(1) Complexity:** The calculation time is constant regardless of the period length.
|
* **O(1) Complexity:** The calculation time is constant regardless of the period length.
|
||||||
|
|||||||
@@ -135,4 +135,146 @@ public class EmaVectorTests
|
|||||||
|
|
||||||
Assert.Equal(200.0, res[0].Value, 1e-9);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ namespace QuanTAlib;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Multi-Alpha Exponential Moving Average (EMA) - SIMD optimized.
|
/// Multi-Alpha Exponential Moving Average (EMA) - SIMD optimized.
|
||||||
/// Calculates multiple EMAs with different periods/alphas for the same input series in parallel.
|
/// Calculates multiple EMAs with different periods/alphas for the same input series in parallel.
|
||||||
|
/// Uses last-value substitution for invalid inputs (NaN/Infinity).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class EmaVector
|
public class EmaVector
|
||||||
{
|
{
|
||||||
@@ -16,6 +17,7 @@ public class EmaVector
|
|||||||
private readonly double[] _emas;
|
private readonly double[] _emas;
|
||||||
private readonly double[] _Es;
|
private readonly double[] _Es;
|
||||||
private readonly int _count;
|
private readonly int _count;
|
||||||
|
private double _lastValidValue;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Current EMA values for all periods.
|
/// Current EMA values for all periods.
|
||||||
@@ -68,6 +70,20 @@ public class EmaVector
|
|||||||
_Es[index] = 1.0;
|
_Es[index] = 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a valid input value, using last-value substitution for non-finite inputs.
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private double GetValidValue(double input)
|
||||||
|
{
|
||||||
|
if (double.IsFinite(input))
|
||||||
|
{
|
||||||
|
_lastValidValue = input;
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
return _lastValidValue;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resets all EMA states.
|
/// Resets all EMA states.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -77,18 +93,22 @@ public class EmaVector
|
|||||||
{
|
{
|
||||||
ResetAt(i);
|
ResetAt(i);
|
||||||
}
|
}
|
||||||
|
_lastValidValue = 0;
|
||||||
Array.Clear(Values);
|
Array.Clear(Values);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates EMAs with the given value.
|
/// Updates EMAs with the given value.
|
||||||
|
/// Uses last-value substitution: invalid inputs (NaN/Infinity) are replaced with
|
||||||
|
/// the last known good value, providing continuity in the output series.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="input">Input value</param>
|
/// <param name="input">Input value</param>
|
||||||
/// <returns>Array of compensated EMA values</returns>
|
/// <returns>Array of compensated EMA values</returns>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public TValue[] Update(TValue input)
|
public TValue[] Update(TValue input)
|
||||||
{
|
{
|
||||||
double val = input.Value;
|
// Last-value substitution: replace non-finite inputs with last valid value
|
||||||
|
double val = GetValidValue(input.Value);
|
||||||
|
|
||||||
// SIMD Loop
|
// SIMD Loop
|
||||||
int vecCount = Vector<double>.Count;
|
int vecCount = Vector<double>.Count;
|
||||||
@@ -199,6 +219,10 @@ public class EmaVector
|
|||||||
{
|
{
|
||||||
double val = sourceValues[t];
|
double val = sourceValues[t];
|
||||||
long time = sourceTimes[t];
|
long time = sourceTimes[t];
|
||||||
|
|
||||||
|
// Last-value substitution: replace non-finite inputs with last valid value
|
||||||
|
val = GetValidValue(val);
|
||||||
|
|
||||||
var vecInput = new Vector<double>(val);
|
var vecInput = new Vector<double>(val);
|
||||||
|
|
||||||
int i = 0;
|
int i = 0;
|
||||||
|
|||||||
@@ -9,15 +9,64 @@ namespace QuanTAlib;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class SimdExtensions
|
public static class SimdExtensions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if span contains any non-finite values (NaN or Infinity).
|
||||||
|
/// Returns true if any non-finite value is found.
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static bool ContainsNonFinite(this ReadOnlySpan<double> span)
|
||||||
|
{
|
||||||
|
if (span.IsEmpty) return false;
|
||||||
|
|
||||||
|
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
|
||||||
|
{
|
||||||
|
int vectorSize = Vector<double>.Count;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
for (; i <= span.Length - vectorSize; i += vectorSize)
|
||||||
|
{
|
||||||
|
var vector = new Vector<double>(span.Slice(i, vectorSize));
|
||||||
|
// Check for NaN: NaN != NaN is true
|
||||||
|
// Check for Infinity: IsInfinity
|
||||||
|
for (int j = 0; j < vectorSize; j++)
|
||||||
|
{
|
||||||
|
if (!double.IsFinite(vector[j]))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check remaining elements
|
||||||
|
for (; i < span.Length; i++)
|
||||||
|
{
|
||||||
|
if (!double.IsFinite(span[i]))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scalar fallback
|
||||||
|
for (int i = 0; i < span.Length; i++)
|
||||||
|
{
|
||||||
|
if (!double.IsFinite(span[i]))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calculates sum using SIMD vectorization when available.
|
/// Calculates sum using SIMD vectorization when available.
|
||||||
/// 4-8x faster than scalar loop on AVX2/AVX-512 hardware.
|
/// 4-8x faster than scalar loop on AVX2/AVX-512 hardware.
|
||||||
|
/// Returns NaN if any input value is non-finite.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static double SumSIMD(this ReadOnlySpan<double> span)
|
public static double SumSIMD(this ReadOnlySpan<double> span)
|
||||||
{
|
{
|
||||||
if (span.IsEmpty) return 0.0;
|
if (span.IsEmpty) return 0.0;
|
||||||
|
|
||||||
|
// Guard against non-finite inputs
|
||||||
|
if (span.ContainsNonFinite()) return double.NaN;
|
||||||
|
|
||||||
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
|
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
|
||||||
{
|
{
|
||||||
Vector<double> sum = Vector<double>.Zero;
|
Vector<double> sum = Vector<double>.Zero;
|
||||||
@@ -53,6 +102,7 @@ public static class SimdExtensions
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calculates minimum value using SIMD vectorization when available.
|
/// Calculates minimum value using SIMD vectorization when available.
|
||||||
/// 4-6x faster than scalar loop on AVX2/AVX-512 hardware.
|
/// 4-6x faster than scalar loop on AVX2/AVX-512 hardware.
|
||||||
|
/// Returns NaN if any input value is non-finite.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static double MinSIMD(this ReadOnlySpan<double> span)
|
public static double MinSIMD(this ReadOnlySpan<double> span)
|
||||||
@@ -60,6 +110,9 @@ public static class SimdExtensions
|
|||||||
if (span.IsEmpty) return double.NaN;
|
if (span.IsEmpty) return double.NaN;
|
||||||
if (span.Length == 1) return span[0];
|
if (span.Length == 1) return span[0];
|
||||||
|
|
||||||
|
// Guard against non-finite inputs
|
||||||
|
if (span.ContainsNonFinite()) return double.NaN;
|
||||||
|
|
||||||
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
|
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
|
||||||
{
|
{
|
||||||
int vectorSize = Vector<double>.Count;
|
int vectorSize = Vector<double>.Count;
|
||||||
@@ -104,6 +157,7 @@ public static class SimdExtensions
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calculates maximum value using SIMD vectorization when available.
|
/// Calculates maximum value using SIMD vectorization when available.
|
||||||
/// 4-6x faster than scalar loop on AVX2/AVX-512 hardware.
|
/// 4-6x faster than scalar loop on AVX2/AVX-512 hardware.
|
||||||
|
/// Returns NaN if any input value is non-finite.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static double MaxSIMD(this ReadOnlySpan<double> span)
|
public static double MaxSIMD(this ReadOnlySpan<double> span)
|
||||||
@@ -111,6 +165,9 @@ public static class SimdExtensions
|
|||||||
if (span.IsEmpty) return double.NaN;
|
if (span.IsEmpty) return double.NaN;
|
||||||
if (span.Length == 1) return span[0];
|
if (span.Length == 1) return span[0];
|
||||||
|
|
||||||
|
// Guard against non-finite inputs
|
||||||
|
if (span.ContainsNonFinite()) return double.NaN;
|
||||||
|
|
||||||
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
|
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
|
||||||
{
|
{
|
||||||
int vectorSize = Vector<double>.Count;
|
int vectorSize = Vector<double>.Count;
|
||||||
@@ -155,25 +212,34 @@ public static class SimdExtensions
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calculates average using SIMD vectorization when available.
|
/// Calculates average using SIMD vectorization when available.
|
||||||
/// 4-8x faster than scalar loop on AVX2/AVX-512 hardware.
|
/// 4-8x faster than scalar loop on AVX2/AVX-512 hardware.
|
||||||
|
/// Returns NaN if any input value is non-finite.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static double AverageSIMD(this ReadOnlySpan<double> span)
|
public static double AverageSIMD(this ReadOnlySpan<double> span)
|
||||||
{
|
{
|
||||||
if (span.IsEmpty) return double.NaN;
|
if (span.IsEmpty) return double.NaN;
|
||||||
|
// SumSIMD already guards against non-finite, which will propagate NaN
|
||||||
return span.SumSIMD() / span.Length;
|
return span.SumSIMD() / span.Length;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calculates variance using SIMD vectorization (Welford's online algorithm adapted).
|
/// Calculates variance using SIMD vectorization (Welford's online algorithm adapted).
|
||||||
/// More numerically stable than naive two-pass algorithm.
|
/// More numerically stable than naive two-pass algorithm.
|
||||||
|
/// Returns NaN if any input value is non-finite or if mean is non-finite.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static double VarianceSIMD(this ReadOnlySpan<double> span, double? mean = null)
|
public static double VarianceSIMD(this ReadOnlySpan<double> span, double? mean = null)
|
||||||
{
|
{
|
||||||
if (span.Length < 2) return double.NaN;
|
if (span.Length < 2) return double.NaN;
|
||||||
|
|
||||||
|
// Guard against non-finite inputs
|
||||||
|
if (span.ContainsNonFinite()) return double.NaN;
|
||||||
|
|
||||||
double m = mean ?? span.AverageSIMD();
|
double m = mean ?? span.AverageSIMD();
|
||||||
|
|
||||||
|
// Guard against non-finite mean (could be passed in or computed from non-finite values)
|
||||||
|
if (!double.IsFinite(m)) return double.NaN;
|
||||||
|
|
||||||
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
|
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
|
||||||
{
|
{
|
||||||
var meanVec = new Vector<double>(m);
|
var meanVec = new Vector<double>(m);
|
||||||
@@ -216,16 +282,19 @@ public static class SimdExtensions
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calculates standard deviation using SIMD vectorization.
|
/// Calculates standard deviation using SIMD vectorization.
|
||||||
|
/// Returns NaN if any input value is non-finite or if mean is non-finite.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static double StdDevSIMD(this ReadOnlySpan<double> span, double? mean = null)
|
public static double StdDevSIMD(this ReadOnlySpan<double> span, double? mean = null)
|
||||||
{
|
{
|
||||||
|
// VarianceSIMD already guards against non-finite, which will propagate NaN through Sqrt
|
||||||
return Math.Sqrt(span.VarianceSIMD(mean));
|
return Math.Sqrt(span.VarianceSIMD(mean));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Finds both min and max in a single pass using SIMD vectorization.
|
/// Finds both min and max in a single pass using SIMD vectorization.
|
||||||
/// More efficient than calling MinSIMD and MaxSIMD separately.
|
/// More efficient than calling MinSIMD and MaxSIMD separately.
|
||||||
|
/// Returns (NaN, NaN) if any input value is non-finite.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static (double Min, double Max) MinMaxSIMD(this ReadOnlySpan<double> span)
|
public static (double Min, double Max) MinMaxSIMD(this ReadOnlySpan<double> span)
|
||||||
@@ -233,6 +302,9 @@ public static class SimdExtensions
|
|||||||
if (span.IsEmpty) return (double.NaN, double.NaN);
|
if (span.IsEmpty) return (double.NaN, double.NaN);
|
||||||
if (span.Length == 1) return (span[0], span[0]);
|
if (span.Length == 1) return (span[0], span[0]);
|
||||||
|
|
||||||
|
// Guard against non-finite inputs
|
||||||
|
if (span.ContainsNonFinite()) return (double.NaN, double.NaN);
|
||||||
|
|
||||||
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
|
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
|
||||||
{
|
{
|
||||||
int vectorSize = Vector<double>.Count;
|
int vectorSize = Vector<double>.Count;
|
||||||
|
|||||||
Reference in New Issue
Block a user