using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; public struct EmaState { public double Ema; public double E; public bool IsHot; public static EmaState New() => new() { Ema = 0, E = 1.0, IsHot = false }; } /// /// Exponential Moving Average (EMA) - IIR filter with exponential warmup compensator. /// Provides valid output from first bar with O(1) complexity. /// /// /// Algorithm uses exponential smoothing with compensator for immediate valid results. /// Reference: https://github.com/mihakralj/pinescript/blob/main/indicators/trends_IIR/ema.md /// public class Ema { private readonly double _alpha; private EmaState _state = EmaState.New(); private EmaState _p_state = EmaState.New(); /// /// Creates EMA with specified period. /// Alpha = 2 / (period + 1) /// /// Period for EMA calculation (must be > 0) public Ema(int period) { if (period <= 0) throw new ArgumentException("Period must be greater than 0", nameof(period)); _alpha = 2.0 / (period + 1); } /// /// Creates EMA with specified alpha smoothing factor. /// /// Smoothing factor (0 < alpha <= 1) public Ema(double alpha) { if (alpha <= 0 || alpha > 1) throw new ArgumentException("Alpha must be between 0 and 1", nameof(alpha)); _alpha = alpha; } /// /// Current EMA value. /// public TValue Value { get; private set; } /// /// True if the EMA has warmed up and is providing valid results. /// public bool IsHot => _state.IsHot; /// /// Core EMA calculation kernel. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double Compute(double input, double alpha, ref EmaState state) { state.Ema += alpha * (input - state.Ema); if (!state.IsHot) { state.E *= (1.0 - alpha); state.IsHot = state.E <= 1e-10; return state.Ema / (1.0 - state.E); } return state.Ema; } /// /// Updates EMA with the given value. /// /// Input value /// True for new bar, false for update to current bar (default: true) /// Compensated EMA value [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TValue input, bool isNew = true) { if (isNew) { _p_state = _state; } else { _state = _p_state; } double val = Compute(input.Value, _alpha, ref _state); Value = new TValue(input.Time, val); return Value; } /// /// Updates EMA with the entire series. /// /// Input series /// EMA series public TSeries Update(TSeries source) { int len = source.Count; var t = new List(len); var v = new List(len); CollectionsMarshal.SetCount(t, len); CollectionsMarshal.SetCount(v, len); var tSpan = CollectionsMarshal.AsSpan(t); var vSpan = CollectionsMarshal.AsSpan(v); var sourceValues = source.Values; var sourceTimes = source.Times; // Local state for batch processing EmaState state = _state; for (int i = 0; i < len; i++) { double val = Compute(sourceValues[i], _alpha, ref state); tSpan[i] = sourceTimes[i]; vSpan[i] = val; } // Update instance state to the final state _state = state; _p_state = state; // Assume last point is committed Value = new TValue(tSpan[len - 1], vSpan[len - 1]); return new TSeries(t, v); } /// /// Calculates EMA for the entire series using a new instance. /// /// Input series /// EMA period /// EMA series public static TSeries Calculate(TSeries source, int period) { var ema = new Ema(period); return ema.Update(source); } /// /// Resets the EMA state. /// public void Reset() { _state = EmaState.New(); _p_state = _state; Value = default; } }