using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// EWMA: Exponentially Weighted Moving Average Volatility /// /// /// EWMA Volatility calculates volatility using an exponentially weighted moving average /// of squared log returns. This approach gives more weight to recent observations while /// still considering historical data, making it responsive to market changes. /// /// Formula: /// r_t = ln(Close_t / Close_{t-1}) /// RMA_t = (RMA_{t-1} × (period - 1) + r²_t) / period /// BiasCorrection = 1 - (1 - 1/period)^n /// CorrectedVariance = RMA_t / BiasCorrection /// EWMA = √(CorrectedVariance × AnnualPeriods) /// /// Key properties: /// - Uses RMA (Running Moving Average) for exponential smoothing /// - Includes bias correction for accurate early estimates /// - Can be annualized or returned as periodic volatility /// - More responsive than simple moving average approaches /// [SkipLocalsInit] public sealed class Ewma : AbstractBase { private readonly int _period; private readonly bool _annualize; private readonly int _annualPeriods; private readonly double _decay; private const double MinPrice = 1e-10; private const double Epsilon = 1e-10; [StructLayout(LayoutKind.Auto)] private record struct State( double RawRmaSqRet, double BiasE, double PrevClose, double LastValid, int Count); private State _s; private State _ps; /// /// Creates EWMA Volatility indicator with specified parameters. /// /// The period for EWMA calculation (must be > 0) /// Whether to annualize the volatility output (default: true) /// Number of periods in a year for annualization (default: 252 for daily data) /// Thrown when parameters are invalid public Ewma(int period = 20, bool annualize = true, int annualPeriods = 252) { if (period <= 0) { throw new ArgumentException("Period must be greater than 0", nameof(period)); } if (annualize && annualPeriods <= 0) { throw new ArgumentException("Annual periods must be greater than 0 when annualizing", nameof(annualPeriods)); } _period = period; _annualize = annualize; _annualPeriods = annualPeriods; _decay = 1.0 - (1.0 / period); Name = annualize ? $"Ewma({period},{annualPeriods})" : $"Ewma({period})"; WarmupPeriod = period; _s = new State(0.0, 1.0, double.NaN, 0.0, 0); _ps = _s; } /// /// Creates EWMA Volatility indicator with specified source and parameters. /// public Ewma(ITValuePublisher source, int period = 20, bool annualize = true, int annualPeriods = 252) : this(period, annualize, annualPeriods) { source.Pub += Handle; } private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew); /// /// True if the indicator has completed the warmup period. /// public override bool IsHot => _s.Count >= _period; /// /// Period for EWMA calculation. /// public int Period => _period; /// /// Whether volatility is annualized. /// public bool Annualize => _annualize; /// /// Number of periods per year for annualization. /// public int AnnualPeriods => _annualPeriods; /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public override TValue Update(TValue input, bool isNew = true) { double close = input.Value; if (isNew) { _ps = _s; } else { _s = _ps; } var s = _s; // Sanitize input - use state's LastValid for consistency double lastValid = double.IsFinite(s.LastValid) && s.LastValid > 0 ? s.LastValid : 1.0; if (!double.IsFinite(close) || close <= 0) { close = lastValid; } else if (isNew) { s.LastValid = close; } double safeClose = Math.Max(close, MinPrice); double safePrevClose = double.IsFinite(s.PrevClose) && s.PrevClose > 0 ? s.PrevClose : safeClose; // Calculate log return double logReturn = 0.0; if (safeClose > 0.0 && safePrevClose > 0.0) { logReturn = Math.Log(safeClose / safePrevClose); } double squaredReturn = logReturn * logReturn; // RMA calculation: raw_rma_sq_ret = (raw_rma_sq_ret * (period - 1) + squaredReturn) / period double rawRmaSqRet; double biasE; if (s.Count == 0) { // First value: initialize with squared return rawRmaSqRet = squaredReturn; biasE = _decay; } else { // RMA update: (prev * (period - 1) + current) / period rawRmaSqRet = Math.FusedMultiplyAdd(s.RawRmaSqRet, _period - 1, squaredReturn) / _period; // Update bias correction factor: e = (1 - alpha) * e_prev biasE = _decay * s.BiasE; } // Bias correction: corrected = raw / (1 - e) double biasCorrection = 1.0 - biasE; double correctedRmaSqRet = biasCorrection > Epsilon ? rawRmaSqRet / biasCorrection : rawRmaSqRet; // Ensure non-negative variance double currentEwmaSqReturns = Math.Max(correctedRmaSqRet, 0.0); // Calculate volatility double volatility = Math.Sqrt(currentEwmaSqReturns); // Annualize if requested double result = _annualize ? volatility * Math.Sqrt(_annualPeriods) : volatility; if (isNew) { s.RawRmaSqRet = rawRmaSqRet; s.BiasE = biasE; s.PrevClose = safeClose; s.Count++; _s = s; } if (!double.IsFinite(result)) { result = 0.0; } Last = new TValue(input.Time, result); PubEvent(Last, isNew); return Last; } /// public override 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); Batch(source.Values, vSpan, _period, _annualize, _annualPeriods); source.Times.CopyTo(tSpan); // Update internal state to match final position for (int i = 0; i < len; i++) { Update(new TValue(source.Times[i], source.Values[i]), isNew: true); } return new TSeries(t, v); } /// public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { for (int i = 0; i < source.Length; i++) { Update(new TValue(DateTime.UtcNow, source[i]), isNew: true); } } /// public override void Reset() { _s = new State(0.0, 1.0, double.NaN, 0.0, 0); _ps = _s; Last = default; } /// /// Calculates EWMA Volatility for entire series. /// public static TSeries Batch(TSeries source, int period = 20, bool annualize = true, int annualPeriods = 252) { if (period <= 0) { throw new ArgumentException("Period must be greater than 0", nameof(period)); } if (annualize && annualPeriods <= 0) { throw new ArgumentException("Annual periods must be greater than 0 when annualizing", nameof(annualPeriods)); } 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); Batch(source.Values, vSpan, period, annualize, annualPeriods); source.Times.CopyTo(tSpan); return new TSeries(t, v); } /// /// Batch EWMA Volatility calculation. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Batch(ReadOnlySpan source, Span output, int period = 20, bool annualize = true, int annualPeriods = 252) { if (source.Length != output.Length) { throw new ArgumentException("Source and output must have the same length", nameof(output)); } if (period <= 0) { throw new ArgumentException("Period must be greater than 0", nameof(period)); } if (annualize && annualPeriods <= 0) { throw new ArgumentException("Annual periods must be greater than 0 when annualizing", nameof(annualPeriods)); } int len = source.Length; if (len == 0) { return; } double alpha = 1.0 / period; double decay = 1.0 - alpha; double annualFactor = annualize ? Math.Sqrt(annualPeriods) : 1.0; double rawRmaSqRet = 0.0; double biasE = 1.0; double prevClose = double.NaN; double lastValidClose = 1.0; for (int i = 0; i < len; i++) { double close = source[i]; // Sanitize input if (!double.IsFinite(close) || close <= 0) { close = lastValidClose; } else { lastValidClose = close; } double safeClose = Math.Max(close, MinPrice); double safePrevClose = double.IsFinite(prevClose) && prevClose > 0 ? prevClose : safeClose; // Calculate log return double logReturn = 0.0; if (safeClose > 0.0 && safePrevClose > 0.0) { logReturn = Math.Log(safeClose / safePrevClose); } double squaredReturn = logReturn * logReturn; // RMA calculation if (i == 0) { rawRmaSqRet = squaredReturn; biasE = decay; } else { rawRmaSqRet = Math.FusedMultiplyAdd(rawRmaSqRet, period - 1, squaredReturn) / period; biasE = decay * biasE; } // Bias correction double biasCorrection = 1.0 - biasE; double correctedRmaSqRet = biasCorrection > Epsilon ? rawRmaSqRet / biasCorrection : rawRmaSqRet; // Calculate volatility double currentEwmaSqReturns = Math.Max(correctedRmaSqRet, 0.0); double volatility = Math.Sqrt(currentEwmaSqReturns); double result = volatility * annualFactor; prevClose = safeClose; output[i] = double.IsFinite(result) ? result : 0.0; } } public static (TSeries Results, Ewma Indicator) Calculate(TSeries source, int period = 20, bool annualize = true, int annualPeriods = 252) { var indicator = new Ewma(period, annualize, annualPeriods); TSeries results = indicator.Update(source); return (results, indicator); } }