using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// EMA: Exponential Moving Average /// /// /// EMA needs very short history buffer and calculates the EMA value using just the /// previous EMA value. The weight of the new datapoint (alpha) is alpha = 2 / (period + 1) /// /// Key characteristics: /// - Uses no buffer, relying only on the previous EMA value. /// - The weight of new data points is calculated as alpha = 2 / (period + 1). /// - Provides a balance between responsiveness and smoothing. No overshooting. Significant lag /// /// Calculation method: /// This implementation can use SMA for the first Period bars as a seeding value for EMA when useSma is true. /// /// Sources: /// - https://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:moving_averages /// - https://www.investopedia.com/ask/answers/122314/what-exponential-moving-average-ema-formula-and-how-ema-calculated.asp /// - https://blog.fugue88.ws/archives/2017-01/The-correct-way-to-start-an-Exponential-Moving-Average-EMA /// public class Ema { private struct State { public double Ema; public double E; public bool IsHot; public 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; /// /// Display name for the indicator. /// public string Name { get; } /// /// 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 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 Value { 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)] 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; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TValue input, bool isNew = true) { if (isNew) { _p_state = _state; } else { _state = _p_state; } double val = GetValidValue(input.Value); val = Compute(val, _alpha, _decay, ref _state); Value = new TValue(input.Time, val); return Value; } public TSeries Update(TSeries source) { if (source.Count == 0) return new TSeries(new List(), new List()); 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; 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); } /// /// 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; State 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; Value = default; } }