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 static State New() => new() { Ema = 0, E = 1.0, IsHot = false }; } private readonly double _alpha; private State _state = State.New(); private State _p_state = State.New(); private double _lastValidValue; /// /// 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; /// /// Gets a valid input value, using last-value substitution for non-finite inputs. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private double GetValidValue(double input) { if (double.IsFinite(input)) { _lastValidValue = input; return input; } return _lastValidValue; } /// /// Core EMA calculation kernel. /// Assumes input has already been validated via GetValidValue(). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static double Compute(double input, double alpha, ref State state) { state.Ema += alpha * (input - state.Ema); double result; if (!state.IsHot) { state.E *= (1.0 - alpha); state.IsHot = state.E <= 1e-10; result = state.Ema / (1.0 - state.E); } else { result = state.Ema; } return result; } /// /// 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; } // Last-value substitution: replace non-finite inputs with last valid value double val = GetValidValue(input.Value); val = Compute(val, _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 State state = _state; for (int i = 0; i < len; i++) { // Last-value substitution: replace non-finite inputs with last valid value double val = GetValidValue(sourceValues[i]); val = Compute(val, _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 = State.New(); _p_state = _state; _lastValidValue = 0; Value = default; } }