using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// MA Type enumeration for MAENV indicator. /// public enum MaenvType { /// Simple Moving Average (O(1) with ring buffer) SMA = 0, /// Exponential Moving Average (O(1) with warmup) EMA = 1, /// Weighted Moving Average (O(n)) WMA = 2 } /// /// MAENV: Moving Average Envelope /// A percentage-based envelope using a selectable moving average as the middle line. /// Middle = MA(source, period) - SMA, EMA, or WMA /// Upper = Middle + (Middle × percentage / 100) /// Lower = Middle - (Middle × percentage / 100) /// [SkipLocalsInit] public sealed class Maenv : ITValuePublisher, IDisposable { private readonly int _period; private readonly double _percentage; private readonly MaenvType _maType; private readonly double _emaAlpha; // Ring buffer for SMA private readonly double[]? _smaBuffer; [StructLayout(LayoutKind.Auto)] private record struct State( // EMA state double EmaSum, double EmaWeight, // SMA state double SmaSum, int SmaHead, int SmaCount, // WMA state int WmaCount, // General double LastValid, int Bars, bool IsHot); private State _state; private State _p_state; private double[]? _p_smaBuffer; // WMA lookback buffer private readonly double[]? _wmaBuffer; private double[]? _p_wmaBuffer; private readonly TValuePublishedHandler _valueHandler; // Subscription tracking for IDisposable private TSeries? _source; private bool _disposed; public string Name { get; } public int WarmupPeriod { get; } public TValue Last { get; private set; } public TValue Upper { get; private set; } public TValue Lower { get; private set; } public bool IsHot => _state.IsHot; public event TValuePublishedHandler? Pub; public Maenv(int period = 20, double percentage = 1.0, MaenvType maType = MaenvType.EMA) { if (period < 1) { throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 1."); } if (percentage <= 0.0) { throw new ArgumentOutOfRangeException(nameof(percentage), "Percentage must be > 0."); } _period = period; _percentage = percentage; _maType = maType; _emaAlpha = 2.0 / (period + 1); WarmupPeriod = period; Name = $"Maenv({period},{percentage},{maType})"; _valueHandler = HandleValue; // Allocate buffers based on MA type if (maType == MaenvType.SMA) { _smaBuffer = new double[period]; _p_smaBuffer = new double[period]; } else if (maType == MaenvType.WMA) { _wmaBuffer = new double[period]; _p_wmaBuffer = new double[period]; } Reset(); } public Maenv(TSeries source, int period = 20, double percentage = 1.0, MaenvType maType = MaenvType.EMA) : this(period, percentage, maType) { _source = source; Prime(source); source.Pub += _valueHandler; } /// /// Releases the event subscription to the source publisher. /// public void Dispose() { if (_disposed) { return; } if (_source != null) { _source.Pub -= _valueHandler; _source = null; } _disposed = true; } private void HandleValue(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew); [MethodImpl(MethodImplOptions.AggressiveInlining)] private void PubEvent(TValue value, bool isNew = true) => Pub?.Invoke(this, new TValueEventArgs { Value = value, IsNew = isNew }); [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Reset() { _state = new State(0, 0, 0, 0, 0, 0, double.NaN, 0, false); _p_state = _state; if (_smaBuffer != null) { Array.Fill(_smaBuffer, 0.0); _p_smaBuffer = (double[])_smaBuffer.Clone(); } if (_wmaBuffer != null) { Array.Fill(_wmaBuffer, 0.0); _p_wmaBuffer = (double[])_wmaBuffer.Clone(); } Last = default; Upper = default; Lower = default; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private double GetValid(double value, bool isNew) { if (double.IsFinite(value)) { if (isNew) { _state = _state with { LastValid = value }; } return value; } return _state.LastValid; } // ======================== // Update overloads (adjacent per S4136) // ======================== [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TValue input, bool isNew = true) { if (isNew) { _p_state = _state; if (_smaBuffer != null && _p_smaBuffer != null) { Array.Copy(_smaBuffer, _p_smaBuffer, _period); } if (_wmaBuffer != null && _p_wmaBuffer != null) { Array.Copy(_wmaBuffer, _p_wmaBuffer, _period); } } else { _state = _p_state; if (_smaBuffer != null && _p_smaBuffer != null) { Array.Copy(_p_smaBuffer, _smaBuffer, _period); } if (_wmaBuffer != null && _p_wmaBuffer != null) { Array.Copy(_p_wmaBuffer, _wmaBuffer, _period); } } double value = GetValid(input.Value, isNew); if (isNew) { _state = _state with { Bars = _state.Bars + 1 }; } double middle = _maType switch { MaenvType.SMA => CalculateSMA(value, isNew), MaenvType.EMA => CalculateEMA(value, isNew), MaenvType.WMA => CalculateWMA(value, isNew), _ => value }; double dist = middle * _percentage / 100.0; double upper = middle + dist; double lower = middle - dist; if (!_state.IsHot && _state.Bars >= WarmupPeriod) { _state = _state with { IsHot = true }; } Last = new TValue(input.Time, middle); Upper = new TValue(input.Time, upper); Lower = new TValue(input.Time, lower); PubEvent(Last, isNew); return Last; } public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TSeries source) { if (source.Count == 0) { return (new TSeries([], []), new TSeries([], []), new TSeries([], [])); } int len = source.Count; var tMiddle = new List(len); var vMiddle = new List(len); var tUpper = new List(len); var vUpper = new List(len); var tLower = new List(len); var vLower = new List(len); CollectionsMarshal.SetCount(tMiddle, len); CollectionsMarshal.SetCount(vMiddle, len); CollectionsMarshal.SetCount(tUpper, len); CollectionsMarshal.SetCount(vUpper, len); CollectionsMarshal.SetCount(tLower, len); CollectionsMarshal.SetCount(vLower, len); var tSpan = CollectionsMarshal.AsSpan(tMiddle); var vMiddleSpan = CollectionsMarshal.AsSpan(vMiddle); var vUpperSpan = CollectionsMarshal.AsSpan(vUpper); var vLowerSpan = CollectionsMarshal.AsSpan(vLower); // Process through streaming path to compute results and prime state in one pass Reset(); for (int i = 0; i < len; i++) { Update(source[i], isNew: true); vMiddleSpan[i] = Last.Value; vUpperSpan[i] = Upper.Value; vLowerSpan[i] = Lower.Value; } source.Times.CopyTo(tSpan); tSpan.CopyTo(CollectionsMarshal.AsSpan(tUpper)); tSpan.CopyTo(CollectionsMarshal.AsSpan(tLower)); var lastTime = new DateTime(source.Times[^1], DateTimeKind.Utc); Last = new TValue(lastTime, vMiddleSpan[^1]); Upper = new TValue(lastTime, vUpperSpan[^1]); Lower = new TValue(lastTime, vLowerSpan[^1]); return (new TSeries(tMiddle, vMiddle), new TSeries(tUpper, vUpper), new TSeries(tLower, vLower)); } // ======================== // Private MA calculation helpers // ======================== [MethodImpl(MethodImplOptions.AggressiveInlining)] private double CalculateSMA(double value, bool isNew) { if (_smaBuffer == null) { return value; } // Calculate new count (always increment if not full, for both isNew cases) int currentCount = _state.SmaCount; int calcCount = currentCount < _period ? currentCount + 1 : currentCount; // Remove oldest value from sum if buffer is full double oldest = _smaBuffer[_state.SmaHead]; double newSum = _state.SmaSum; if (currentCount >= _period) { newSum -= oldest; } // Add new value newSum += value; // Update buffer _smaBuffer[_state.SmaHead] = value; int newHead = (_state.SmaHead + 1) % _period; // Persist state only for isNew=true if (isNew) { _state = _state with { SmaSum = newSum, SmaHead = newHead, SmaCount = calcCount }; } return newSum / calcCount; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private double CalculateEMA(double value, bool isNew) { // Use EmaWeight==0 to detect first value for correct isNew=false behavior if (Math.Abs(_state.EmaWeight) < double.Epsilon) { // First value - persist only for isNew=true if (isNew) { _state = _state with { EmaSum = value, EmaWeight = 1.0 }; } return value; } // EMA with warmup compensation double newSum = Math.FusedMultiplyAdd(_state.EmaSum, 1.0 - _emaAlpha, value * _emaAlpha); double newWeight = Math.FusedMultiplyAdd(_state.EmaWeight, 1.0 - _emaAlpha, _emaAlpha); // Persist state only for isNew=true if (isNew) { _state = _state with { EmaSum = newSum, EmaWeight = newWeight }; } return newSum / newWeight; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private double CalculateWMA(double value, bool isNew) { if (_wmaBuffer == null) { return value; } // Calculate count for this bar (always increment if not full, for both isNew cases) int currentCount = _state.WmaCount; int calcCount = currentCount < _period ? currentCount + 1 : currentCount; // Shift buffer (always shift if count > 1, regardless of isNew) // This ensures restoration produces same buffer state as original if (calcCount > 1) { for (int i = _period - 1; i > 0; i--) { _wmaBuffer[i] = _wmaBuffer[i - 1]; } } _wmaBuffer[0] = value; // Persist state only for isNew=true if (isNew) { _state = _state with { WmaCount = calcCount }; } // Calculate WMA double norm = 0.0; double sum = 0.0; for (int i = 0; i < calcCount; i++) { double w = (_period - i) * _period; norm += w; sum += _wmaBuffer[i] * w; } return norm > 0 ? sum / norm : value; } public void Prime(TSeries source) { Reset(); if (source.Count == 0) { return; } for (int i = 0; i < source.Count; i++) { Update(source[i], isNew: true); } } // ======================== // Batch overloads (adjacent per S4136) // ======================== /// /// Batch calculation using spans (zero allocation for SMA and EMA). /// public static void Batch( ReadOnlySpan source, Span middle, Span upper, Span lower, int period, double percentage = 1.0, MaenvType maType = MaenvType.EMA) { if (period < 1) { throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 1."); } if (percentage <= 0.0) { throw new ArgumentOutOfRangeException(nameof(percentage), "Percentage must be > 0."); } if (middle.Length < source.Length || upper.Length < source.Length || lower.Length < source.Length) { throw new ArgumentException("Output spans must be at least as long as input", nameof(middle)); } int len = source.Length; if (len == 0) { return; } switch (maType) { case MaenvType.SMA: BatchSMA(source, middle, upper, lower, period, percentage); break; case MaenvType.EMA: BatchEMA(source, middle, upper, lower, period, percentage); break; case MaenvType.WMA: BatchWMA(source, middle, upper, lower, period, percentage); break; default: BatchEMA(source, middle, upper, lower, period, percentage); break; } } public static (TSeries Middle, TSeries Upper, TSeries Lower) Batch(TSeries source, int period = 20, double percentage = 1.0, MaenvType maType = MaenvType.EMA) { int len = source.Count; var tMiddle = new List(len); var vMiddle = new List(len); var tUpper = new List(len); var vUpper = new List(len); var tLower = new List(len); var vLower = new List(len); CollectionsMarshal.SetCount(tMiddle, len); CollectionsMarshal.SetCount(vMiddle, len); CollectionsMarshal.SetCount(tUpper, len); CollectionsMarshal.SetCount(vUpper, len); CollectionsMarshal.SetCount(tLower, len); CollectionsMarshal.SetCount(vLower, len); Batch(source.Values, CollectionsMarshal.AsSpan(vMiddle), CollectionsMarshal.AsSpan(vUpper), CollectionsMarshal.AsSpan(vLower), period, percentage, maType); source.Times.CopyTo(CollectionsMarshal.AsSpan(tMiddle)); CollectionsMarshal.AsSpan(tMiddle).CopyTo(CollectionsMarshal.AsSpan(tUpper)); CollectionsMarshal.AsSpan(tMiddle).CopyTo(CollectionsMarshal.AsSpan(tLower)); return (new TSeries(tMiddle, vMiddle), new TSeries(tUpper, vUpper), new TSeries(tLower, vLower)); } // ======================== // Private batch helpers // ======================== [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void BatchSMA( ReadOnlySpan source, Span middle, Span upper, Span lower, int period, double percentage) { int len = source.Length; Span buffer = period <= 256 ? stackalloc double[period] : new double[period]; buffer.Clear(); double sum = 0.0; int head = 0; int count = 0; for (int i = 0; i < len; i++) { double value = source[i]; // Remove oldest if full if (count >= period) { sum -= buffer[head]; } else { count++; } // Add new sum += value; buffer[head] = value; head = (head + 1) % period; double ma = sum / count; double dist = ma * percentage / 100.0; middle[i] = ma; upper[i] = ma + dist; lower[i] = ma - dist; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void BatchEMA( ReadOnlySpan source, Span middle, Span upper, Span lower, int period, double percentage) { int len = source.Length; double alpha = 2.0 / (period + 1); double emaSum = source[0]; double emaWeight = 1.0; double ma = emaSum; double dist = ma * percentage / 100.0; middle[0] = ma; upper[0] = ma + dist; lower[0] = ma - dist; for (int i = 1; i < len; i++) { double value = source[i]; emaSum = Math.FusedMultiplyAdd(emaSum, 1.0 - alpha, value * alpha); emaWeight = Math.FusedMultiplyAdd(emaWeight, 1.0 - alpha, alpha); ma = emaSum / emaWeight; dist = ma * percentage / 100.0; middle[i] = ma; upper[i] = ma + dist; lower[i] = ma - dist; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void BatchWMA( ReadOnlySpan source, Span middle, Span upper, Span lower, int period, double percentage) { int len = source.Length; for (int i = 0; i < len; i++) { double norm = 0.0; double sum = 0.0; int count = Math.Min(i + 1, period); for (int j = 0; j < count; j++) { double w = (period - j) * period; norm += w; sum += source[i - j] * w; } double ma = norm > 0 ? sum / norm : source[i]; double dist = ma * percentage / 100.0; middle[i] = ma; upper[i] = ma + dist; lower[i] = ma - dist; } } public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Maenv Indicator) Calculate(TSeries source, int period = 20, double percentage = 1.0, MaenvType maType = MaenvType.EMA) { // Use parameterless constructor to avoid double-priming: // The Maenv(source, ...) constructor already calls Prime(source), // so calling Update(source) afterwards would Prime again. var indicator = new Maenv(period, percentage, maType); var results = indicator.Update(source); return (results, indicator); } }