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
+6
View File
@@ -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.
+1 -1
View File
@@ -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.
+76
View File
@@ -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.");
+102
View File
@@ -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);
}
} }
+37 -11
View File
@@ -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.
@@ -36,7 +37,7 @@ public class Ema
{ {
if (period <= 0) if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period)); throw new ArgumentException("Period must be greater than 0", nameof(period));
_alpha = 2.0 / (period + 1); _alpha = 2.0 / (period + 1);
} }
@@ -48,7 +49,7 @@ public class Ema
{ {
if (alpha <= 0 || alpha > 1) if (alpha <= 0 || alpha > 1)
throw new ArgumentException("Alpha must be between 0 and 1", nameof(alpha)); throw new ArgumentException("Alpha must be between 0 and 1", nameof(alpha));
_alpha = alpha; _alpha = alpha;
} }
@@ -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
return state.Ema; {
result = 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;
} }
@@ -123,10 +146,12 @@ public class Ema
// Local state for batch processing // Local state for batch processing
EmaState state = _state; EmaState state = _state;
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;
} }
} }
+29
View File
@@ -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.
+142
View File
@@ -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);
}
}
} }
+41 -17
View File
@@ -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,7 +17,8 @@ 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.
/// </summary> /// </summary>
@@ -33,7 +35,7 @@ public class EmaVector
_emas = new double[_count]; _emas = new double[_count];
_Es = new double[_count]; _Es = new double[_count];
Values = new TValue[_count]; Values = new TValue[_count];
for (int i = 0; i < _count; i++) for (int i = 0; i < _count; i++)
{ {
if (periods[i] <= 0) throw new ArgumentException("Period must be greater than 0", nameof(periods)); 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]; _emas = new double[_count];
_Es = new double[_count]; _Es = new double[_count];
Values = new TValue[_count]; Values = new TValue[_count];
for (int i = 0; i < _count; i++) 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)); 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; _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,23 +93,27 @@ 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;
int i = 0; int i = 0;
if (Vector.IsHardwareAccelerated && _count >= vecCount) if (Vector.IsHardwareAccelerated && _count >= vecCount)
{ {
var vecInput = new Vector<double>(val); var vecInput = new Vector<double>(val);
@@ -110,7 +130,7 @@ public class EmaVector
// Update EMA // Update EMA
// ema += alpha * (input - ema) // ema += alpha * (input - ema)
vecEma += vecAlpha * (vecInput - vecEma); vecEma += vecAlpha * (vecInput - vecEma);
// Update E (warmup factor) // Update E (warmup factor)
// E *= (1 - alpha) // E *= (1 - alpha)
vecE *= (vecOne - vecAlpha); vecE *= (vecOne - vecAlpha);
@@ -123,7 +143,7 @@ public class EmaVector
// Vector.LessThanOrEqual returns Vector<long> with all-1s for true, all-0s for false // Vector.LessThanOrEqual returns Vector<long> with all-1s for true, all-0s for false
// We reinterpret as Vector<double> for use with ConditionalSelect // We reinterpret as Vector<double> for use with ConditionalSelect
var isHotMask = Vector.LessThanOrEqual(vecE, vecEpsilon); var isHotMask = Vector.LessThanOrEqual(vecE, vecEpsilon);
// Select result: if hot (E <= epsilon), use raw EMA; otherwise use compensated // Select result: if hot (E <= epsilon), use raw EMA; otherwise use compensated
// ConditionalSelect: mask=true -> first arg, mask=false -> second arg // ConditionalSelect: mask=true -> first arg, mask=false -> second arg
var vecResult = Vector.ConditionalSelect( var vecResult = Vector.ConditionalSelect(
@@ -149,7 +169,7 @@ public class EmaVector
{ {
double alpha = _alphas[i]; double alpha = _alphas[i];
_emas[i] += alpha * (val - _emas[i]); _emas[i] += alpha * (val - _emas[i]);
double result = _emas[i]; double result = _emas[i];
if (_Es[i] > 1e-10) if (_Es[i] > 1e-10)
{ {
@@ -159,7 +179,7 @@ public class EmaVector
result = _emas[i] / (1.0 - _Es[i]); result = _emas[i] / (1.0 - _Es[i]);
} }
} }
Values[i] = new TValue(input.Time, result); Values[i] = new TValue(input.Time, result);
} }
@@ -175,7 +195,7 @@ public class EmaVector
{ {
int len = source.Count; int len = source.Count;
var resultSeries = new TSeries[_count]; var resultSeries = new TSeries[_count];
// Pre-allocate lists // Pre-allocate lists
var tLists = new List<long>[_count]; var tLists = new List<long>[_count];
var vLists = new List<double>[_count]; var vLists = new List<double>[_count];
@@ -190,7 +210,7 @@ public class EmaVector
var sourceValues = source.Values; var sourceValues = source.Values;
var sourceTimes = source.Times; var sourceTimes = source.Times;
int vecCount = Vector<double>.Count; int vecCount = Vector<double>.Count;
var vecOne = Vector<double>.One; var vecOne = Vector<double>.One;
var vecEpsilon = new Vector<double>(1e-10); var vecEpsilon = new Vector<double>(1e-10);
@@ -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;
@@ -212,12 +236,12 @@ public class EmaVector
vecEma += vecAlpha * (vecInput - vecEma); vecEma += vecAlpha * (vecInput - vecEma);
vecE *= (vecOne - vecAlpha); vecE *= (vecOne - vecAlpha);
var vecCompensated = vecEma / (vecOne - vecE); var vecCompensated = vecEma / (vecOne - vecE);
// Check warmup condition: E <= 1e-10 means "hot" (use raw EMA) // Check warmup condition: E <= 1e-10 means "hot" (use raw EMA)
var isHotMask = Vector.LessThanOrEqual(vecE, vecEpsilon); var isHotMask = Vector.LessThanOrEqual(vecE, vecEpsilon);
// Select result: if hot, use raw EMA; otherwise use compensated // Select result: if hot, use raw EMA; otherwise use compensated
var vecResult = Vector.ConditionalSelect( var vecResult = Vector.ConditionalSelect(
Vector.AsVectorDouble(isHotMask), Vector.AsVectorDouble(isHotMask),
@@ -241,7 +265,7 @@ public class EmaVector
{ {
double alpha = _alphas[i]; double alpha = _alphas[i];
_emas[i] += alpha * (val - _emas[i]); _emas[i] += alpha * (val - _emas[i]);
double result = _emas[i]; double result = _emas[i];
if (_Es[i] > 1e-10) if (_Es[i] > 1e-10)
{ {
@@ -251,7 +275,7 @@ public class EmaVector
result = _emas[i] / (1.0 - _Es[i]); result = _emas[i] / (1.0 - _Es[i]);
} }
} }
CollectionsMarshal.AsSpan(tLists[i])[t] = time; CollectionsMarshal.AsSpan(tLists[i])[t] = time;
CollectionsMarshal.AsSpan(vLists[i])[t] = result; CollectionsMarshal.AsSpan(vLists[i])[t] = result;
} }
+72
View File
@@ -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;