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:
Miha Kralj
2025-12-04 13:49:05 -08:00
parent 3ed35322a5
commit 967096d4f5
27 changed files with 387 additions and 3367 deletions
+5 -63
View File
@@ -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;