mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-23 04:58:08 +00:00
Refactor and optimize various components of QuanTAlib
- Removed WmaVector class to streamline weighted moving average calculations. - Simplified RingBuffer implementation by removing unnecessary comments and improving clarity. - Enhanced SIMD extensions for better performance and readability. - Updated TBar and TBarSeries classes to improve property calculations and reduce overhead. - Cleaned up TValue struct by removing redundant comments. - Added comprehensive unit tests for IndicatorExtensions and TrimaIndicator to ensure functionality and correctness.
This commit is contained in:
@@ -16,7 +16,7 @@ This notebook demonstrates:
|
||||
1. **Manual Data Processing**: Understanding Batch vs. Streaming modes.
|
||||
2. **Streaming with `isNew`**: Handling intra-bar updates.
|
||||
3. **Large Dataset Processing**: Using Geometric Brownian Motion (GBM) generated data.
|
||||
4. **Vectorized Operations**: Calculating multiple EMAs simultaneously.
|
||||
4. **Handling Invalid Values**: Last-value substitution for NaN/Infinity.
|
||||
|
||||
#!csharp
|
||||
|
||||
@@ -165,63 +165,9 @@ Console.WriteLine($"Streaming Last Value: {lastStreamVal.Value:F2}");
|
||||
|
||||
#!markdown
|
||||
|
||||
## 4. Vectorized EMA (Multiple Periods)
|
||||
## 4. Handling Invalid Values (NaN/Infinity)
|
||||
|
||||
`EmaVector` allows calculating multiple EMAs (e.g., 9, 12, 26) simultaneously. This is optimized for performance using SIMD where available.
|
||||
|
||||
### Vectorized Batch
|
||||
|
||||
#!csharp
|
||||
|
||||
int[] periods = { 9, 12, 26 };
|
||||
Console.WriteLine($"\n--- Vectorized Batch EMA (Periods: {string.Join(", ", periods)}) ---");
|
||||
|
||||
var emaVectorBatch = new EmaVector(periods);
|
||||
var vectorBatchResults = emaVectorBatch.Calculate(closeSeries);
|
||||
|
||||
for (int i = 0; i < periods.Length; i++)
|
||||
{
|
||||
Console.WriteLine($"EMA({periods[i]}) Last Value: {vectorBatchResults[i].Last().Value:F2}");
|
||||
}
|
||||
|
||||
#!markdown
|
||||
|
||||
### Vectorized Streaming
|
||||
|
||||
#!csharp
|
||||
|
||||
Console.WriteLine($"\n--- Vectorized Streaming EMA (Periods: {string.Join(", ", periods)}) ---");
|
||||
|
||||
var emaVectorStream = new EmaVector(periods);
|
||||
TValue[] lastVectorVal = null;
|
||||
|
||||
foreach(var item in closeSeries)
|
||||
{
|
||||
lastVectorVal = emaVectorStream.Update(item);
|
||||
}
|
||||
|
||||
for (int i = 0; i < periods.Length; i++)
|
||||
{
|
||||
Console.WriteLine($"EMA({periods[i]}) Last Value: {lastVectorVal[i].Value:F2}");
|
||||
}
|
||||
|
||||
// Verification
|
||||
bool allMatch = true;
|
||||
for (int i = 0; i < periods.Length; i++)
|
||||
{
|
||||
if (Math.Abs(vectorBatchResults[i].Last().Value - lastVectorVal[i].Value) > 1e-10)
|
||||
{
|
||||
allMatch = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
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.
|
||||
`Ema` uses **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
|
||||
|
||||
@@ -271,24 +217,3 @@ for (int i = 0; i < seriesWithNaN.Count; i++)
|
||||
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.");
|
||||
|
||||
+5
-63
@@ -25,30 +25,18 @@ namespace QuanTAlib;
|
||||
/// </remarks>
|
||||
public class Ema
|
||||
{
|
||||
private struct State : IEquatable<State>
|
||||
private struct State
|
||||
{
|
||||
public double Ema;
|
||||
public double E; // Compensator: decays from 1.0 to 1e-10 for bias correction
|
||||
public bool IsHot; // True when 95% coverage reached (E <= 0.05)
|
||||
public bool IsCompensated; // True when compensator fully decayed (E <= 1e-10)
|
||||
public double E;
|
||||
public bool IsHot;
|
||||
public bool IsCompensated;
|
||||
|
||||
public static State New() => new() { Ema = 0, E = 1.0, IsHot = false, IsCompensated = false };
|
||||
|
||||
public readonly bool Equals(State other) =>
|
||||
Ema == other.Ema && E == other.E && IsHot == other.IsHot && IsCompensated == other.IsCompensated;
|
||||
|
||||
public override readonly bool Equals(object? obj) =>
|
||||
obj is State other && Equals(other);
|
||||
|
||||
public override readonly int GetHashCode() =>
|
||||
HashCode.Combine(Ema, E, IsHot, IsCompensated);
|
||||
|
||||
public static bool operator ==(State left, State right) => left.Equals(right);
|
||||
public static bool operator !=(State left, State right) => !left.Equals(right);
|
||||
}
|
||||
|
||||
private readonly double _alpha;
|
||||
private readonly double _decay; // Pre-calculated (1.0 - alpha) to avoid subtraction per tick
|
||||
private readonly double _decay;
|
||||
private State _state = State.New();
|
||||
private State _p_state = State.New();
|
||||
private double _lastValidValue;
|
||||
@@ -97,9 +85,6 @@ 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)
|
||||
{
|
||||
@@ -111,17 +96,9 @@ public class Ema
|
||||
return _lastValidValue;
|
||||
}
|
||||
|
||||
// 95% coverage threshold: E = 1 - 0.95 = 0.05
|
||||
private const double COVERAGE_THRESHOLD = 0.05;
|
||||
// Compensator decay threshold for bias correction
|
||||
private const double COMPENSATOR_THRESHOLD = 1e-10;
|
||||
|
||||
/// <summary>
|
||||
/// Core EMA calculation kernel.
|
||||
/// Assumes input has already been validated via GetValidValue().
|
||||
/// IsHot becomes true at 95% coverage (E <= 0.05).
|
||||
/// Bias correction continues until compensator decays to 1e-10.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double Compute(double input, double alpha, double decay, ref State state)
|
||||
{
|
||||
@@ -132,11 +109,9 @@ public class Ema
|
||||
{
|
||||
state.E *= decay;
|
||||
|
||||
// IsHot triggers at 95% coverage
|
||||
if (!state.IsHot && state.E <= COVERAGE_THRESHOLD)
|
||||
state.IsHot = true;
|
||||
|
||||
// Continue bias correction until compensator fully decays
|
||||
if (state.E <= COMPENSATOR_THRESHOLD)
|
||||
{
|
||||
state.IsCompensated = true;
|
||||
@@ -155,9 +130,6 @@ public class Ema
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Core calculation kernel that handles both batch and streaming-continuation.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void CalculateCore(ReadOnlySpan<double> source, Span<double> output, double alpha, ref State state, ref double lastValidValue)
|
||||
{
|
||||
@@ -165,8 +137,6 @@ public class Ema
|
||||
double decay = 1.0 - alpha;
|
||||
int i = 0;
|
||||
|
||||
// Phase 1: Warmup with bias correction
|
||||
// If state is already compensated, this loop is skipped
|
||||
if (!state.IsCompensated)
|
||||
{
|
||||
for (; i < len && state.E > COMPENSATOR_THRESHOLD; i++)
|
||||
@@ -189,7 +159,6 @@ public class Ema
|
||||
state.IsCompensated = true;
|
||||
}
|
||||
|
||||
// Phase 2: Hot loop
|
||||
for (; i < len; i++)
|
||||
{
|
||||
double val = source[i];
|
||||
@@ -203,12 +172,6 @@ public class Ema
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates EMA with the given value.
|
||||
/// </summary>
|
||||
/// <param name="input">Input value</param>
|
||||
/// <param name="isNew">True for new bar, false for update to current bar (default: true)</param>
|
||||
/// <returns>Compensated EMA value</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
@@ -221,19 +184,12 @@ public class Ema
|
||||
_state = _p_state;
|
||||
}
|
||||
|
||||
// Last-value substitution: replace non-finite inputs with last valid value
|
||||
double val = GetValidValue(input.Value);
|
||||
val = Compute(val, _alpha, _decay, ref _state);
|
||||
Value = new TValue(input.Time, val);
|
||||
return Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates EMA with the entire series.
|
||||
/// Uses split-loop optimization: warmup phase with bias correction, then branchless hot loop.
|
||||
/// </summary>
|
||||
/// <param name="source">Input series</param>
|
||||
/// <returns>EMA series</returns>
|
||||
public TSeries Update(TSeries source)
|
||||
{
|
||||
if (source.Count == 0) return new TSeries(new List<long>(), new List<double>());
|
||||
@@ -249,9 +205,6 @@ public class Ema
|
||||
var sourceValues = source.Values;
|
||||
var sourceTimes = source.Times;
|
||||
|
||||
// 1. Fast Batch Calculation
|
||||
// Uses the unified CalculateCore to handle both new and continuing states
|
||||
// Optimization: Copy state to locals to allow JIT register allocation
|
||||
State state = _state;
|
||||
double lastValidValue = _lastValidValue;
|
||||
|
||||
@@ -260,7 +213,6 @@ public class Ema
|
||||
_state = state;
|
||||
_lastValidValue = lastValidValue;
|
||||
|
||||
// Copy Times
|
||||
sourceTimes.CopyTo(tSpan);
|
||||
|
||||
_p_state = _state;
|
||||
@@ -299,15 +251,6 @@ public class Ema
|
||||
Calculate(source, output, alpha);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates EMA in-place using alpha, writing results to pre-allocated output span.
|
||||
/// Zero-allocation method for maximum performance.
|
||||
/// Bias correction continues until compensator decays to 1e-10.
|
||||
/// Uses split-loop optimization: warmup phase with bias correction, then branchless hot loop.
|
||||
/// </summary>
|
||||
/// <param name="source">Input values</param>
|
||||
/// <param name="output">Output span (must be same length as source)</param>
|
||||
/// <param name="alpha">Smoothing factor (0 < alpha <= 1)</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, double alpha)
|
||||
{
|
||||
@@ -318,7 +261,6 @@ public class Ema
|
||||
|
||||
if (source.Length == 0) return;
|
||||
|
||||
// Initialize default state for static calculation
|
||||
State state = State.New();
|
||||
double lastValid = 0;
|
||||
|
||||
|
||||
+1
-27
@@ -115,33 +115,9 @@ Console.WriteLine($"Last EMA: {emaOutput[^1]}");
|
||||
* **Hunter's bias correction**: Same accuracy as TSeries API
|
||||
* **Compatible** with `ArrayPool<T>` for buffer management
|
||||
|
||||
### Multi-Alpha EMA (`EmaVector`)
|
||||
|
||||
The `EmaVector` class is a SIMD-optimized implementation for calculating multiple EMAs with different periods on the same input series simultaneously. It leverages hardware intrinsics (AVX/SSE) for high performance.
|
||||
|
||||
```csharp
|
||||
using QuanTAlib;
|
||||
|
||||
// Initialize with multiple periods
|
||||
int[] periods = { 9, 12, 26 };
|
||||
var emaVector = new EmaVector(periods);
|
||||
|
||||
// Streaming update
|
||||
TValue[] results = emaVector.Update(new TValue(time, price));
|
||||
|
||||
// Access values
|
||||
Console.WriteLine($"EMA(9): {results[0].Value}");
|
||||
Console.WriteLine($"EMA(12): {results[1].Value}");
|
||||
Console.WriteLine($"EMA(26): {results[2].Value}");
|
||||
|
||||
// Batch calculation
|
||||
TSeries source = ...;
|
||||
TSeries[] seriesResults = emaVector.Calculate(source);
|
||||
```
|
||||
|
||||
### Handling Invalid Values (NaN/Infinity)
|
||||
|
||||
Both `Ema` and `EmaVector` use **last-value substitution** for handling invalid inputs:
|
||||
`Ema` uses **last-value substitution** for handling invalid inputs:
|
||||
|
||||
```csharp
|
||||
var ema = new Ema(10);
|
||||
@@ -166,13 +142,11 @@ var results = ema.Update(series); // All values are finite
|
||||
|
||||
* 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.
|
||||
* **SIMD Optimization:** `EmaVector` processes multiple periods in parallel using vector instructions, significantly reducing CPU cycles for multi-timeframe analysis.
|
||||
* **Zero Allocation:** The streaming `Update` method is designed to be allocation-free (excluding the return struct).
|
||||
|
||||
## Interpretation Details
|
||||
|
||||
@@ -1,378 +0,0 @@
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class EmaVectorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Initialization_WithPeriods_SetsCorrectAlphas()
|
||||
{
|
||||
int[] periods = { 10, 20 };
|
||||
var emaVector = new EmaVector(periods);
|
||||
|
||||
var res = emaVector.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
|
||||
Assert.Equal(100.0, res[0].Value, 1e-9);
|
||||
Assert.Equal(100.0, res[1].Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialization_WithAlphas_Works()
|
||||
{
|
||||
double[] alphas = { 0.1, 0.2, 0.5 };
|
||||
var emaVector = new EmaVector(alphas);
|
||||
|
||||
var res = emaVector.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
|
||||
Assert.Equal(3, res.Length);
|
||||
Assert.Equal(100.0, res[0].Value, 1e-9);
|
||||
Assert.Equal(100.0, res[1].Value, 1e-9);
|
||||
Assert.Equal(100.0, res[2].Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialization_WithZeroPeriod_ThrowsArgumentException()
|
||||
{
|
||||
int[] periods = { 10, 0, 20 };
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new EmaVector(periods));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialization_WithNegativePeriod_ThrowsArgumentException()
|
||||
{
|
||||
int[] periods = { 10, -5, 20 };
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new EmaVector(periods));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialization_WithZeroAlpha_ThrowsArgumentException()
|
||||
{
|
||||
double[] alphas = { 0.1, 0.0, 0.5 };
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new EmaVector(alphas));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialization_WithNegativeAlpha_ThrowsArgumentException()
|
||||
{
|
||||
double[] alphas = { 0.1, -0.1, 0.5 };
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new EmaVector(alphas));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialization_WithAlphaGreaterThanOne_ThrowsArgumentException()
|
||||
{
|
||||
double[] alphas = { 0.1, 1.5, 0.5 };
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new EmaVector(alphas));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialization_WithAlphaEqualToOne_Works()
|
||||
{
|
||||
double[] alphas = { 0.1, 1.0, 0.5 };
|
||||
var emaVector = new EmaVector(alphas);
|
||||
|
||||
var res = emaVector.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
|
||||
Assert.Equal(3, res.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_Streaming_MatchesSingleEma()
|
||||
{
|
||||
int[] periods = { 5, 10, 20 };
|
||||
var emaVector = new EmaVector(periods);
|
||||
var emaSingles = periods.Select(p => new Ema(p)).ToArray();
|
||||
|
||||
var values = new double[] { 10, 20, 30, 40, 50, 40, 30, 20, 10 };
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
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);
|
||||
Assert.Equal(singleRes.Time, multiRes[i].Time);
|
||||
}
|
||||
|
||||
time = time.AddMinutes(1);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_Series_MatchesSingleEma()
|
||||
{
|
||||
int[] periods = { 5, 10, 20 };
|
||||
var emaVector = new EmaVector(periods);
|
||||
var emaSingles = periods.Select(p => new Ema(p)).ToArray();
|
||||
|
||||
int len = 100;
|
||||
var t = new System.Collections.Generic.List<long>(len);
|
||||
var v = new System.Collections.Generic.List<double>(len);
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
t.Add(now.AddMinutes(i).Ticks);
|
||||
v.Add(Math.Sin(i * 0.1) * 100);
|
||||
}
|
||||
|
||||
var series = new TSeries(t, v);
|
||||
|
||||
var multiRes = emaVector.Calculate(series);
|
||||
|
||||
for (int i = 0; i < periods.Length; i++)
|
||||
{
|
||||
var singleRes = emaSingles[i].Update(series);
|
||||
|
||||
Assert.Equal(singleRes.Count, multiRes[i].Count);
|
||||
for (int j = 0; j < len; j++)
|
||||
{
|
||||
Assert.Equal(singleRes.Values[j], multiRes[i].Values[j], 1e-8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_Series_MatchesStreaming()
|
||||
{
|
||||
int[] periods = { 5, 10, 20 };
|
||||
var emaVectorBatch = new EmaVector(periods);
|
||||
var emaVectorStream = new EmaVector(periods);
|
||||
|
||||
int len = 100;
|
||||
var t = new System.Collections.Generic.List<long>(len);
|
||||
var v = new System.Collections.Generic.List<double>(len);
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
t.Add(now.AddMinutes(i).Ticks);
|
||||
v.Add(Math.Sin(i * 0.1) * 100);
|
||||
}
|
||||
|
||||
var series = new TSeries(t, v);
|
||||
|
||||
var batchRes = emaVectorBatch.Calculate(series);
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
var tVal = new TValue(new DateTime(t[i], DateTimeKind.Utc), v[i]);
|
||||
var streamRes = emaVectorStream.Update(tVal);
|
||||
|
||||
for (int j = 0; j < periods.Length; j++)
|
||||
{
|
||||
Assert.Equal(batchRes[j].Values[i], streamRes[j].Value, 1e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Static_MatchesInstanceMethod()
|
||||
{
|
||||
int[] periods = { 5, 10, 20 };
|
||||
|
||||
int len = 50;
|
||||
var t = new System.Collections.Generic.List<long>(len);
|
||||
var v = new System.Collections.Generic.List<double>(len);
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
t.Add(now.AddMinutes(i).Ticks);
|
||||
v.Add(Math.Sin(i * 0.1) * 100);
|
||||
}
|
||||
|
||||
var series = new TSeries(t, v);
|
||||
|
||||
var instanceEma = new EmaVector(periods);
|
||||
var instanceRes = instanceEma.Calculate(series);
|
||||
|
||||
var staticRes = EmaVector.Calculate(series, periods);
|
||||
|
||||
for (int i = 0; i < periods.Length; i++)
|
||||
{
|
||||
Assert.Equal(instanceRes[i].Count, staticRes[i].Count);
|
||||
for (int j = 0; j < len; j++)
|
||||
{
|
||||
Assert.Equal(instanceRes[i].Values[j], staticRes[i].Values[j], 1e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
int[] periods = { 10 };
|
||||
var emaVector = new EmaVector(periods);
|
||||
|
||||
emaVector.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
emaVector.Reset();
|
||||
|
||||
var res = emaVector.Update(new TValue(DateTime.UtcNow, 200.0));
|
||||
|
||||
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);
|
||||
|
||||
emaVector.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
emaVector.Update(new TValue(DateTime.UtcNow, 110.0));
|
||||
|
||||
var resultAfterNaN = emaVector.Update(new TValue(DateTime.UtcNow, double.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);
|
||||
|
||||
emaVector.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
emaVector.Update(new TValue(DateTime.UtcNow, 110.0));
|
||||
|
||||
var resultAfterPosInf = emaVector.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
|
||||
foreach (var result in resultAfterPosInf)
|
||||
{
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
var resultAfterNegInf = emaVector.Update(new TValue(DateTime.UtcNow, 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);
|
||||
|
||||
emaVector.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
emaVector.Update(new TValue(DateTime.UtcNow, 110.0));
|
||||
emaVector.Update(new TValue(DateTime.UtcNow, 120.0));
|
||||
|
||||
var r1 = emaVector.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
var r2 = emaVector.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
var r3 = emaVector.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
|
||||
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);
|
||||
|
||||
var t = new System.Collections.Generic.List<long>();
|
||||
var v = new System.Collections.Generic.List<double>();
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
emaVector.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
emaVector.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
|
||||
emaVector.Reset();
|
||||
|
||||
var result = emaVector.Update(new TValue(DateTime.UtcNow, 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();
|
||||
|
||||
var values = new double[] { 10, 20, double.NaN, 40, double.PositiveInfinity, 60, 70 };
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Values_Property_UpdatesAfterUpdate()
|
||||
{
|
||||
int[] periods = { 5, 10 };
|
||||
var emaVector = new EmaVector(periods);
|
||||
|
||||
var result = emaVector.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
|
||||
Assert.Equal(result[0].Value, emaVector.Values[0].Value);
|
||||
Assert.Equal(result[1].Value, emaVector.Values[1].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Values_Property_UpdatesAfterCalculate()
|
||||
{
|
||||
int[] periods = { 5, 10 };
|
||||
var emaVector = new EmaVector(periods);
|
||||
|
||||
var t = new System.Collections.Generic.List<long> { 100, 200, 300 };
|
||||
var v = new System.Collections.Generic.List<double> { 10.0, 20.0, 30.0 };
|
||||
var series = new TSeries(t, v);
|
||||
|
||||
var results = emaVector.Calculate(series);
|
||||
|
||||
Assert.Equal(results[0].Last.Value, emaVector.Values[0].Value, 1e-9);
|
||||
Assert.Equal(results[1].Last.Value, emaVector.Values[1].Value, 1e-9);
|
||||
}
|
||||
}
|
||||
@@ -1,327 +0,0 @@
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
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>
|
||||
[SkipLocalsInit]
|
||||
public class EmaVector
|
||||
{
|
||||
private readonly double[] _alphas;
|
||||
private readonly double[] _emas;
|
||||
private readonly double[] _Es;
|
||||
private readonly double[] _p_emas;
|
||||
private readonly double[] _p_Es;
|
||||
private readonly int _count;
|
||||
private double _lastValidValue;
|
||||
|
||||
/// <summary>
|
||||
/// Current EMA values for all periods.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<TValue> Values => _values;
|
||||
|
||||
private readonly TValue[] _values;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes EmaVector with specified periods.
|
||||
/// </summary>
|
||||
/// <param name="periods">Array of periods</param>
|
||||
public EmaVector(int[] periods)
|
||||
{
|
||||
_count = periods.Length;
|
||||
_alphas = new double[_count];
|
||||
_emas = new double[_count];
|
||||
_Es = new double[_count];
|
||||
_p_emas = new double[_count];
|
||||
_p_Es = new double[_count];
|
||||
_values = new TValue[_count];
|
||||
|
||||
for (int i = 0; i < _count; i++)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(periods[i], 0);
|
||||
_alphas[i] = 2.0 / (periods[i] + 1);
|
||||
ResetAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes EmaVector with specified alphas.
|
||||
/// </summary>
|
||||
/// <param name="alphas">Array of alphas</param>
|
||||
public EmaVector(double[] alphas)
|
||||
{
|
||||
_count = alphas.Length;
|
||||
_alphas = new double[_count];
|
||||
_emas = new double[_count];
|
||||
_Es = new double[_count];
|
||||
_p_emas = new double[_count];
|
||||
_p_Es = new double[_count];
|
||||
_values = new TValue[_count];
|
||||
|
||||
for (int i = 0; i < _count; i++)
|
||||
{
|
||||
if (alphas[i] <= 0 || alphas[i] > 1)
|
||||
throw new ArgumentOutOfRangeException(nameof(alphas), alphas[i], "Alpha must be between 0 (exclusive) and 1 (inclusive)");
|
||||
_alphas[i] = alphas[i];
|
||||
ResetAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetAt(int index)
|
||||
{
|
||||
_emas[index] = 0.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>
|
||||
/// Resets all EMA states.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
for (int i = 0; i < _count; i++)
|
||||
{
|
||||
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>
|
||||
/// <param name="isNew">True for new bar, false for update to current bar (default: true)</param>
|
||||
/// <returns>Array of compensated EMA values</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue[] Update(TValue input, bool isNew = true)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
Array.Copy(_emas, _p_emas, _count);
|
||||
Array.Copy(_Es, _p_Es, _count);
|
||||
}
|
||||
else
|
||||
{
|
||||
Array.Copy(_p_emas, _emas, _count);
|
||||
Array.Copy(_p_Es, _Es, _count);
|
||||
}
|
||||
|
||||
// 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);
|
||||
var vecOne = Vector<double>.One;
|
||||
var vecEpsilon = new Vector<double>(1e-10);
|
||||
|
||||
for (; i <= _count - vecCount; i += vecCount)
|
||||
{
|
||||
// Load state
|
||||
var vecAlpha = new Vector<double>(_alphas, i);
|
||||
var vecEma = new Vector<double>(_emas, i);
|
||||
var vecE = new Vector<double>(_Es, i);
|
||||
|
||||
// Update EMA
|
||||
// ema += alpha * (input - ema)
|
||||
vecEma += vecAlpha * (vecInput - vecEma);
|
||||
|
||||
// Update E (warmup factor)
|
||||
// E *= (1 - alpha)
|
||||
vecE *= (vecOne - vecAlpha);
|
||||
|
||||
// Calculate compensated result
|
||||
// res = ema / (1 - E)
|
||||
var vecCompensated = vecEma / (vecOne - vecE);
|
||||
|
||||
// Check warmup condition: E <= 1e-10 means "hot" (use raw EMA)
|
||||
// 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(
|
||||
Vector.AsVectorDouble(isHotMask),
|
||||
vecEma, // Hot: use raw EMA
|
||||
vecCompensated // Cold: use compensated
|
||||
);
|
||||
|
||||
// Store state
|
||||
vecEma.CopyTo(_emas, i);
|
||||
vecE.CopyTo(_Es, i);
|
||||
|
||||
// Store result
|
||||
for (int j = 0; j < vecCount; j++)
|
||||
{
|
||||
_values[i + j] = new TValue(input.Time, vecResult[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scalar fallback for remaining items
|
||||
for (; i < _count; i++)
|
||||
{
|
||||
double alpha = _alphas[i];
|
||||
_emas[i] += alpha * (val - _emas[i]);
|
||||
|
||||
double result = _emas[i];
|
||||
if (_Es[i] > 1e-10)
|
||||
{
|
||||
_Es[i] *= (1.0 - alpha);
|
||||
if (_Es[i] > 1e-10)
|
||||
{
|
||||
result = _emas[i] / (1.0 - _Es[i]);
|
||||
}
|
||||
}
|
||||
|
||||
_values[i] = new TValue(input.Time, result);
|
||||
}
|
||||
|
||||
return _values;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates EMAs for the entire series.
|
||||
/// </summary>
|
||||
/// <param name="source">Input series</param>
|
||||
/// <returns>Array of EMA series</returns>
|
||||
public TSeries[] Calculate(TSeries source)
|
||||
{
|
||||
int len = source.Count;
|
||||
var resultSeries = new TSeries[_count];
|
||||
|
||||
// Pre-allocate lists
|
||||
var tLists = new List<long>[_count];
|
||||
var vLists = new List<double>[_count];
|
||||
|
||||
for (int i = 0; i < _count; i++)
|
||||
{
|
||||
tLists[i] = new List<long>(len);
|
||||
vLists[i] = new List<double>(len);
|
||||
CollectionsMarshal.SetCount(tLists[i], len);
|
||||
CollectionsMarshal.SetCount(vLists[i], len);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
for (int t = 0; t < len; t++)
|
||||
{
|
||||
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;
|
||||
if (Vector.IsHardwareAccelerated && _count >= vecCount)
|
||||
{
|
||||
for (; i <= _count - vecCount; i += vecCount)
|
||||
{
|
||||
var vecAlpha = new Vector<double>(_alphas, i);
|
||||
var vecEma = new Vector<double>(_emas, i);
|
||||
var vecE = new Vector<double>(_Es, i);
|
||||
|
||||
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),
|
||||
vecEma, // Hot: use raw EMA
|
||||
vecCompensated // Cold: use compensated
|
||||
);
|
||||
|
||||
vecEma.CopyTo(_emas, i);
|
||||
vecE.CopyTo(_Es, i);
|
||||
|
||||
// Scatter results to lists
|
||||
for (int j = 0; j < vecCount; j++)
|
||||
{
|
||||
CollectionsMarshal.AsSpan(tLists[i + j])[t] = time;
|
||||
CollectionsMarshal.AsSpan(vLists[i + j])[t] = vecResult[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (; i < _count; i++)
|
||||
{
|
||||
double alpha = _alphas[i];
|
||||
_emas[i] += alpha * (val - _emas[i]);
|
||||
|
||||
double result = _emas[i];
|
||||
if (_Es[i] > 1e-10)
|
||||
{
|
||||
_Es[i] *= (1.0 - alpha);
|
||||
if (_Es[i] > 1e-10)
|
||||
{
|
||||
result = _emas[i] / (1.0 - _Es[i]);
|
||||
}
|
||||
}
|
||||
|
||||
CollectionsMarshal.AsSpan(tLists[i])[t] = time;
|
||||
CollectionsMarshal.AsSpan(vLists[i])[t] = result;
|
||||
}
|
||||
}
|
||||
|
||||
// Create TSeries and update Values
|
||||
for (int i = 0; i < _count; i++)
|
||||
{
|
||||
resultSeries[i] = new TSeries(tLists[i], vLists[i]);
|
||||
var lastT = CollectionsMarshal.AsSpan(tLists[i])[len - 1];
|
||||
var lastV = CollectionsMarshal.AsSpan(vLists[i])[len - 1];
|
||||
_values[i] = new TValue(lastT, lastV);
|
||||
}
|
||||
|
||||
return resultSeries;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates EMAs for the entire series using specified periods.
|
||||
/// </summary>
|
||||
/// <param name="source">Input series</param>
|
||||
/// <param name="periods">Array of periods</param>
|
||||
/// <returns>Array of EMA series</returns>
|
||||
public static TSeries[] Calculate(TSeries source, int[] periods)
|
||||
{
|
||||
var emaVector = new EmaVector(periods);
|
||||
return emaVector.Calculate(source);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user