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 : AbstractBase { // inherited _index // inherited _value /// /// The period for the EMA calculation. /// private readonly int _period; /// /// Circular buffer for SMA calculation. /// private CircularBuffer _sma; /// /// The last calculated EMA value. /// private double _lastEma, _p_lastEma; /// /// Compensator for early EMA values. /// private double _e, _p_e; /// /// The smoothing factor for EMA calculation. /// private readonly double _k; /// /// Flags to track initialization status. /// private bool _isInit, _p_isInit; /// /// Flag to determine whether to use SMA for initial values. /// private readonly bool _useSma; /// /// Initializes a new instance of the Ema class with a specified period. /// /// The period for EMA calculation. /// Whether to use SMA for initial values. Default is true. /// Thrown when period is less than 1. public Ema(int period, bool useSma = true) { if (period < 1) { throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); } _period = period; _k = 2.0 / (_period + 1); _useSma = useSma; _sma = new(period); Name = "Ema"; WarmupPeriod = (int)Math.Ceiling(Math.Log(0.05) / Math.Log(1 - _k)); //95th percentile Init(); } /// /// Initializes a new instance of the Ema class with a specified alpha value. /// /// The smoothing factor for EMA calculation. public Ema(double alpha) { _k = alpha; _useSma = false; _sma = new(1); Name = "Ema"; _period = 1; WarmupPeriod = (int)Math.Ceiling(Math.Log(0.05) / Math.Log(1 - _k)); //95th percentile Init(); } /// /// Initializes a new instance of the Ema class with a specified source and period. /// /// The source object for event subscription. /// The period for EMA calculation. /// Whether to use SMA for initial values. Default is true. public Ema(object source, int period, bool useSma = true) : this(period, useSma) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } /// /// Initializes the Ema instance. /// public override void Init() { base.Init(); _e = 1.0; _lastEma = 0; _isInit = false; _p_isInit = false; _sma = new(_period); } /// /// Manages the state of the Ema instance. /// /// Indicates whether the input is new. protected override void ManageState(bool isNew) { if (isNew) { _p_lastEma = _lastEma; _p_isInit = _isInit; _p_e = _e; _index++; } else { _lastEma = _p_lastEma; _isInit = _p_isInit; _e = _p_e; } } /// /// Performs the EMA calculation. /// /// The calculated EMA value. protected override double Calculation() { double result, _ema; ManageState(Input.IsNew); // when _UseSma == true, use SMA calculation until we have enough data points if (!_isInit && _useSma) { _sma.Add(Input.Value, Input.IsNew); _ema = _sma.Average(); result = _ema; if (_index >= _period) { _isInit = true; } } else { // compensator for early ema values _e = (_e > 1e-10) ? (1 - _k) * _e : 0; _ema = _k * (Input.Value - _lastEma) + _lastEma; // _useSma decides if we use compensator or not result = (_useSma || _e <= double.Epsilon) ? _ema : _ema / (1 - _e); } _lastEma = _ema; IsHot = _index >= WarmupPeriod; return result; } }