mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-07-29 02:07:42 +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
|
||||
memory-bank/
|
||||
|
||||
# macOS resource forks and metadata
|
||||
._*
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
Binary file not shown.
@@ -44,7 +44,7 @@
|
||||
</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>
|
||||
|
||||
<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}");
|
||||
|
||||
#!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);
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
}
|
||||
|
||||
+37
-11
@@ -6,9 +6,9 @@ namespace QuanTAlib;
|
||||
|
||||
public struct EmaState
|
||||
{
|
||||
public double Ema;
|
||||
public double E;
|
||||
public bool IsHot;
|
||||
public double Ema { get; set; }
|
||||
public double E { get; set; }
|
||||
public bool IsHot { get; set; }
|
||||
|
||||
public static EmaState New() => new() { Ema = 0, E = 1.0, IsHot = false };
|
||||
}
|
||||
@@ -26,6 +26,7 @@ public class Ema
|
||||
private readonly double _alpha;
|
||||
private EmaState _state = EmaState.New();
|
||||
private EmaState _p_state = EmaState.New();
|
||||
private double _lastValidValue;
|
||||
|
||||
/// <summary>
|
||||
/// Creates EMA with specified period.
|
||||
@@ -36,7 +37,7 @@ public class Ema
|
||||
{
|
||||
if (period <= 0)
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
|
||||
|
||||
_alpha = 2.0 / (period + 1);
|
||||
}
|
||||
|
||||
@@ -48,7 +49,7 @@ public class Ema
|
||||
{
|
||||
if (alpha <= 0 || alpha > 1)
|
||||
throw new ArgumentException("Alpha must be between 0 and 1", nameof(alpha));
|
||||
|
||||
|
||||
_alpha = alpha;
|
||||
}
|
||||
|
||||
@@ -62,22 +63,42 @@ public class Ema
|
||||
/// </summary>
|
||||
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>
|
||||
/// Core EMA calculation kernel.
|
||||
/// Assumes input has already been validated via GetValidValue().
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double Compute(double input, double alpha, ref EmaState state)
|
||||
{
|
||||
state.Ema += alpha * (input - state.Ema);
|
||||
|
||||
double result;
|
||||
if (!state.IsHot)
|
||||
{
|
||||
state.E *= (1.0 - alpha);
|
||||
state.IsHot = state.E <= 1e-10;
|
||||
return state.Ema / (1.0 - state.E);
|
||||
result = state.Ema / (1.0 - state.E);
|
||||
}
|
||||
|
||||
return state.Ema;
|
||||
else
|
||||
{
|
||||
result = state.Ema;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -98,7 +119,9 @@ public class Ema
|
||||
_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);
|
||||
return Value;
|
||||
}
|
||||
@@ -123,10 +146,12 @@ public class Ema
|
||||
|
||||
// Local state for batch processing
|
||||
EmaState state = _state;
|
||||
|
||||
|
||||
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];
|
||||
vSpan[i] = val;
|
||||
}
|
||||
@@ -158,6 +183,7 @@ public class Ema
|
||||
{
|
||||
_state = EmaState.New();
|
||||
_p_state = _state;
|
||||
_lastValidValue = 0;
|
||||
Value = default;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +103,35 @@ TSeries 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
|
||||
|
||||
* **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);
|
||||
}
|
||||
|
||||
[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>
|
||||
/// Multi-Alpha Exponential Moving Average (EMA) - SIMD optimized.
|
||||
/// Calculates multiple EMAs with different periods/alphas for the same input series in parallel.
|
||||
/// Uses last-value substitution for invalid inputs (NaN/Infinity).
|
||||
/// </summary>
|
||||
public class EmaVector
|
||||
{
|
||||
@@ -16,7 +17,8 @@ public class EmaVector
|
||||
private readonly double[] _emas;
|
||||
private readonly double[] _Es;
|
||||
private readonly int _count;
|
||||
|
||||
private double _lastValidValue;
|
||||
|
||||
/// <summary>
|
||||
/// Current EMA values for all periods.
|
||||
/// </summary>
|
||||
@@ -33,7 +35,7 @@ public class EmaVector
|
||||
_emas = new double[_count];
|
||||
_Es = new double[_count];
|
||||
Values = new TValue[_count];
|
||||
|
||||
|
||||
for (int i = 0; i < _count; i++)
|
||||
{
|
||||
if (periods[i] <= 0) throw new ArgumentException("Period must be greater than 0", nameof(periods));
|
||||
@@ -53,7 +55,7 @@ public class EmaVector
|
||||
_emas = new double[_count];
|
||||
_Es = new double[_count];
|
||||
Values = new TValue[_count];
|
||||
|
||||
|
||||
for (int i = 0; i < _count; i++)
|
||||
{
|
||||
if (alphas[i] <= 0 || alphas[i] > 1) throw new ArgumentException("Alpha must be between 0 and 1", nameof(alphas));
|
||||
@@ -68,6 +70,20 @@ public class EmaVector
|
||||
_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>
|
||||
/// Resets all EMA states.
|
||||
/// </summary>
|
||||
@@ -77,23 +93,27 @@ public class EmaVector
|
||||
{
|
||||
ResetAt(i);
|
||||
}
|
||||
_lastValidValue = 0;
|
||||
Array.Clear(Values);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
/// <param name="input">Input value</param>
|
||||
/// <returns>Array of compensated EMA values</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
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
|
||||
int vecCount = Vector<double>.Count;
|
||||
int i = 0;
|
||||
|
||||
|
||||
if (Vector.IsHardwareAccelerated && _count >= vecCount)
|
||||
{
|
||||
var vecInput = new Vector<double>(val);
|
||||
@@ -110,7 +130,7 @@ public class EmaVector
|
||||
// Update EMA
|
||||
// ema += alpha * (input - ema)
|
||||
vecEma += vecAlpha * (vecInput - vecEma);
|
||||
|
||||
|
||||
// Update E (warmup factor)
|
||||
// E *= (1 - alpha)
|
||||
vecE *= (vecOne - vecAlpha);
|
||||
@@ -123,7 +143,7 @@ public class EmaVector
|
||||
// Vector.LessThanOrEqual returns Vector<long> with all-1s for true, all-0s for false
|
||||
// We reinterpret as Vector<double> for use with ConditionalSelect
|
||||
var isHotMask = Vector.LessThanOrEqual(vecE, vecEpsilon);
|
||||
|
||||
|
||||
// Select result: if hot (E <= epsilon), use raw EMA; otherwise use compensated
|
||||
// ConditionalSelect: mask=true -> first arg, mask=false -> second arg
|
||||
var vecResult = Vector.ConditionalSelect(
|
||||
@@ -149,7 +169,7 @@ public class EmaVector
|
||||
{
|
||||
double alpha = _alphas[i];
|
||||
_emas[i] += alpha * (val - _emas[i]);
|
||||
|
||||
|
||||
double result = _emas[i];
|
||||
if (_Es[i] > 1e-10)
|
||||
{
|
||||
@@ -159,7 +179,7 @@ public class EmaVector
|
||||
result = _emas[i] / (1.0 - _Es[i]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Values[i] = new TValue(input.Time, result);
|
||||
}
|
||||
|
||||
@@ -175,7 +195,7 @@ public class EmaVector
|
||||
{
|
||||
int len = source.Count;
|
||||
var resultSeries = new TSeries[_count];
|
||||
|
||||
|
||||
// Pre-allocate lists
|
||||
var tLists = new List<long>[_count];
|
||||
var vLists = new List<double>[_count];
|
||||
@@ -190,7 +210,7 @@ public class EmaVector
|
||||
|
||||
var sourceValues = source.Values;
|
||||
var sourceTimes = source.Times;
|
||||
|
||||
|
||||
int vecCount = Vector<double>.Count;
|
||||
var vecOne = Vector<double>.One;
|
||||
var vecEpsilon = new Vector<double>(1e-10);
|
||||
@@ -199,6 +219,10 @@ public class EmaVector
|
||||
{
|
||||
double val = sourceValues[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);
|
||||
|
||||
int i = 0;
|
||||
@@ -212,12 +236,12 @@ public class EmaVector
|
||||
|
||||
vecEma += vecAlpha * (vecInput - vecEma);
|
||||
vecE *= (vecOne - vecAlpha);
|
||||
|
||||
|
||||
var vecCompensated = vecEma / (vecOne - vecE);
|
||||
|
||||
|
||||
// Check warmup condition: E <= 1e-10 means "hot" (use raw EMA)
|
||||
var isHotMask = Vector.LessThanOrEqual(vecE, vecEpsilon);
|
||||
|
||||
|
||||
// Select result: if hot, use raw EMA; otherwise use compensated
|
||||
var vecResult = Vector.ConditionalSelect(
|
||||
Vector.AsVectorDouble(isHotMask),
|
||||
@@ -241,7 +265,7 @@ public class EmaVector
|
||||
{
|
||||
double alpha = _alphas[i];
|
||||
_emas[i] += alpha * (val - _emas[i]);
|
||||
|
||||
|
||||
double result = _emas[i];
|
||||
if (_Es[i] > 1e-10)
|
||||
{
|
||||
@@ -251,7 +275,7 @@ public class EmaVector
|
||||
result = _emas[i] / (1.0 - _Es[i]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
CollectionsMarshal.AsSpan(tLists[i])[t] = time;
|
||||
CollectionsMarshal.AsSpan(vLists[i])[t] = result;
|
||||
}
|
||||
|
||||
@@ -9,15 +9,64 @@ namespace QuanTAlib;
|
||||
/// </summary>
|
||||
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>
|
||||
/// Calculates sum using SIMD vectorization when available.
|
||||
/// 4-8x faster than scalar loop on AVX2/AVX-512 hardware.
|
||||
/// Returns NaN if any input value is non-finite.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double SumSIMD(this ReadOnlySpan<double> span)
|
||||
{
|
||||
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)
|
||||
{
|
||||
Vector<double> sum = Vector<double>.Zero;
|
||||
@@ -53,6 +102,7 @@ public static class SimdExtensions
|
||||
/// <summary>
|
||||
/// Calculates minimum value using SIMD vectorization when available.
|
||||
/// 4-6x faster than scalar loop on AVX2/AVX-512 hardware.
|
||||
/// Returns NaN if any input value is non-finite.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double MinSIMD(this ReadOnlySpan<double> span)
|
||||
@@ -60,6 +110,9 @@ public static class SimdExtensions
|
||||
if (span.IsEmpty) return double.NaN;
|
||||
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)
|
||||
{
|
||||
int vectorSize = Vector<double>.Count;
|
||||
@@ -104,6 +157,7 @@ public static class SimdExtensions
|
||||
/// <summary>
|
||||
/// Calculates maximum value using SIMD vectorization when available.
|
||||
/// 4-6x faster than scalar loop on AVX2/AVX-512 hardware.
|
||||
/// Returns NaN if any input value is non-finite.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double MaxSIMD(this ReadOnlySpan<double> span)
|
||||
@@ -111,6 +165,9 @@ public static class SimdExtensions
|
||||
if (span.IsEmpty) return double.NaN;
|
||||
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)
|
||||
{
|
||||
int vectorSize = Vector<double>.Count;
|
||||
@@ -155,25 +212,34 @@ public static class SimdExtensions
|
||||
/// <summary>
|
||||
/// Calculates average using SIMD vectorization when available.
|
||||
/// 4-8x faster than scalar loop on AVX2/AVX-512 hardware.
|
||||
/// Returns NaN if any input value is non-finite.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double AverageSIMD(this ReadOnlySpan<double> span)
|
||||
{
|
||||
if (span.IsEmpty) return double.NaN;
|
||||
// SumSIMD already guards against non-finite, which will propagate NaN
|
||||
return span.SumSIMD() / span.Length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates variance using SIMD vectorization (Welford's online algorithm adapted).
|
||||
/// More numerically stable than naive two-pass algorithm.
|
||||
/// Returns NaN if any input value is non-finite or if mean is non-finite.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double VarianceSIMD(this ReadOnlySpan<double> span, double? mean = null)
|
||||
{
|
||||
if (span.Length < 2) return double.NaN;
|
||||
|
||||
// Guard against non-finite inputs
|
||||
if (span.ContainsNonFinite()) return double.NaN;
|
||||
|
||||
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)
|
||||
{
|
||||
var meanVec = new Vector<double>(m);
|
||||
@@ -216,16 +282,19 @@ public static class SimdExtensions
|
||||
|
||||
/// <summary>
|
||||
/// Calculates standard deviation using SIMD vectorization.
|
||||
/// Returns NaN if any input value is non-finite or if mean is non-finite.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds both min and max in a single pass using SIMD vectorization.
|
||||
/// More efficient than calling MinSIMD and MaxSIMD separately.
|
||||
/// Returns (NaN, NaN) if any input value is non-finite.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
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.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)
|
||||
{
|
||||
int vectorSize = Vector<double>.Count;
|
||||
|
||||
Reference in New Issue
Block a user