using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// EMA: Exponential Moving Average /// /// /// EMA applies exponential weighting to data points, giving more weight to recent values. /// Uses a single state variable for O(1) complexity per update. /// /// Calculation: /// alpha = 2 / (period + 1) /// EMA_new = EMA_old + alpha * (newest - EMA_old) /// /// Initialization: /// Uses a compensator factor to correct early-stage bias (when n < period). /// Output = EMA_state / (1 - (1-alpha)^n) /// /// O(1) update: /// No buffer required, only previous EMA value and compensator state. /// /// IsHot: /// Becomes true when n = ln(0.05) / ln(1 - alpha) /// [SkipLocalsInit] public sealed class Ema : ITValuePublisher { private record struct State(double Ema, double E, bool IsHot, bool IsCompensated) { public static State New() => new() { Ema = 0, E = 1.0, IsHot = false, IsCompensated = false }; } private readonly double _alpha; private readonly double _decay; private State _state = State.New(); private State _p_state = State.New(); private double _lastValidValue; private double _p_lastValidValue; /// /// Display name for the indicator. /// public string Name { get; } public event Action? Pub; /// /// 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); _decay = 1.0 - _alpha; Name = $"Ema({period})"; } /// /// Creates EMA with specified source and period. /// Subscribes to source.Pub event. /// /// Source to subscribe to /// Period for EMA calculation public Ema(ITValuePublisher source, int period) : this(period) { source.Pub += (item) => Update(item); } /// /// 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; _decay = 1.0 - alpha; Name = $"Ema(α={alpha:F4})"; } /// /// Current EMA value. /// public TValue Last { get; private set; } /// /// True if the EMA has warmed up and is providing valid results. /// public bool IsHot => _state.IsHot; [MethodImpl(MethodImplOptions.AggressiveInlining)] private double GetValidValue(double input) { if (double.IsFinite(input)) { _lastValidValue = input; return input; } return _lastValidValue; } private const double COVERAGE_THRESHOLD = 0.05; private const double COMPENSATOR_THRESHOLD = 1e-10; [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TValue input, bool isNew = true) { if (isNew) { _p_state = _state; _p_lastValidValue = _lastValidValue; } else { _state = _p_state; _lastValidValue = _p_lastValidValue; } double val = GetValidValue(input.Value); val = Compute(val, _alpha, _decay, ref _state); Last = new TValue(input.Time, val); Pub?.Invoke(Last); return Last; } public TSeries Update(TSeries source) { if (source.Count == 0) return []; 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; State state = _state; double lastValidValue = _lastValidValue; CalculateCore(sourceValues, vSpan, _alpha, ref state, ref lastValidValue); _state = state; _lastValidValue = lastValidValue; sourceTimes.CopyTo(tSpan); _p_state = _state; _p_lastValidValue = _lastValidValue; Last = new TValue(tSpan[len - 1], vSpan[len - 1]); return new TSeries(t, v); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static double Compute(double input, double alpha, double decay, ref State state) { state.Ema += alpha * (input - state.Ema); double result; if (!state.IsCompensated) { state.E *= decay; if (!state.IsHot && state.E <= COVERAGE_THRESHOLD) state.IsHot = true; if (state.E <= COMPENSATOR_THRESHOLD) { state.IsCompensated = true; result = state.Ema; } else { result = state.Ema / (1.0 - state.E); } } else { result = state.Ema; } return result; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void CalculateCore(ReadOnlySpan source, Span output, double alpha, ref State state, ref double lastValidValue) { int len = source.Length; double decay = 1.0 - alpha; int i = 0; if (!state.IsCompensated) { for (; i < len && state.E > COMPENSATOR_THRESHOLD; i++) { double val = source[i]; if (double.IsFinite(val)) lastValidValue = val; else val = lastValidValue; state.Ema += alpha * (val - state.Ema); state.E *= decay; if (!state.IsHot && state.E <= COVERAGE_THRESHOLD) state.IsHot = true; output[i] = state.Ema / (1.0 - state.E); } if (state.E <= COMPENSATOR_THRESHOLD) state.IsCompensated = true; } for (; i < len; i++) { double val = source[i]; if (double.IsFinite(val)) lastValidValue = val; else val = lastValidValue; state.Ema += alpha * (val - state.Ema); output[i] = state.Ema; } } /// /// 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); } /// /// Calculates EMA in-place using period, writing results to pre-allocated output span. /// Zero-allocation method for maximum performance. /// Alpha = 2 / (period + 1) /// /// Input values /// Output span (must be same length as source) /// EMA period (must be > 0) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Calculate(ReadOnlySpan source, Span output, int period) { if (period <= 0) throw new ArgumentException("Period must be greater than 0", nameof(period)); double alpha = 2.0 / (period + 1); Calculate(source, output, alpha); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Calculate(ReadOnlySpan source, Span output, double alpha) { if (source.Length != output.Length) throw new ArgumentException("Source and output must have the same length"); if (alpha <= 0 || alpha > 1) throw new ArgumentException("Alpha must be between 0 and 1", nameof(alpha)); if (source.Length == 0) return; var state = State.New(); double lastValid = 0; CalculateCore(source, output, alpha, ref state, ref lastValid); } /// /// Resets the EMA state. /// public void Reset() { _state = State.New(); _p_state = _state; _lastValidValue = 0; _p_lastValidValue = 0; Last = default; } }