using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// CMA: Cumulative Moving Average (Running Average / Cumulative Mean) /// /// /// CMA calculates the arithmetic mean of ALL data points seen so far, not just a fixed window. /// Uses Welford's algorithm with FMA (Fused Multiply-Add) for maximum numerical precision. /// /// Calculation: /// M_n = M_(n-1) + α * (x_n - M_(n-1)) where α = 1/n /// /// Implemented using FMA for single-rounding precision: /// mean = FusedMultiplyAdd(alpha, delta, mean) /// /// This is equivalent to: /// M_n = ((n-1) * M_(n-1) + x_n) / n /// /// Key Features: /// - Zero window: includes ALL historical data with equal weight /// - O(1) time complexity per update /// - Maximum precision: FMA avoids intermediate rounding of alpha*delta /// - Numerically stable: avoids overflow from summing large sequences /// - No buffer required: only stores count and mean /// /// IsHot: /// Always true after the first value (no warmup period needed). /// [SkipLocalsInit] public sealed class Cma : AbstractBase { [StructLayout(LayoutKind.Auto)] private record struct State(double Mean, long Count, double LastValidValue); private State _state; private State _p_state; private readonly TValuePublishedHandler _handler; /// /// Creates a new CMA indicator instance. /// No period parameter required since CMA averages all values. /// public Cma() { Name = "Cma"; WarmupPeriod = 1; _handler = Handle; } /// /// Creates CMA with a source to subscribe to. /// /// Source to subscribe to public Cma(ITValuePublisher source) : this() { source.Pub += _handler; } /// /// Creates CMA with a TSeries source to prime from and subscribe to. /// /// TSeries source public Cma(TSeries source) : this() { Prime(source.Values); if (source.Count > 0) { Last = new TValue(source.LastTime, Last.Value); } source.Pub += _handler; } private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew); ///////////////////////////////////////////////////////////////////////////////////////////////// // Mode B: Streaming (Stateful) ///////////////////////////////////////////////////////////////////////////////////////////////// /// /// True if the CMA has enough data to produce valid results. /// CMA is "hot" after the first value since no warmup is needed. /// public override bool IsHot => _state.Count > 0; ///////////////////////////////////////////////////////////////////////////////////////////////// // Mode C: Priming (The Bridge) ///////////////////////////////////////////////////////////////////////////////////////////////// /// /// Initializes the indicator state using the provided history. /// /// Historical data /// Time interval between values (not used for CMA) public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { if (source.Length == 0) { return; } // Reset state _state = default; _p_state = default; // Find first valid value to seed lastValid for (int i = 0; i < source.Length; i++) { if (double.IsFinite(source[i])) { _state.LastValidValue = source[i]; break; } } // Process all values using Welford's algorithm with FMA for (int i = 0; i < source.Length; i++) { double val = GetValidValue(source[i]); _state.Count++; double alpha = 1.0 / _state.Count; double delta = val - _state.Mean; _state.Mean = Math.FusedMultiplyAdd(alpha, delta, _state.Mean); } Last = new TValue(DateTime.MinValue, _state.Mean); _p_state = _state; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private double GetValidValue(double input) { if (double.IsFinite(input)) { _state.LastValidValue = input; return input; } return _state.LastValidValue; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public override TValue Update(TValue input, bool isNew = true) { if (isNew) { _p_state = _state; } else { _state = _p_state; } double val = GetValidValue(input.Value); _state.Count++; double alpha = 1.0 / _state.Count; double delta = val - _state.Mean; _state.Mean = Math.FusedMultiplyAdd(alpha, delta, _state.Mean); Last = new TValue(input.Time, _state.Mean); PubEvent(Last, isNew); return Last; } public override TSeries Update(TSeries source) { if (source.Count == 0) { return []; } 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); source.Times.CopyTo(tSpan); Prime(source.Values); Last = new TValue(tSpan[len - 1], vSpan[len - 1]); return new TSeries(t, v); } ///////////////////////////////////////////////////////////////////////////////////////////////// // Mode A: Batch (Stateless) ///////////////////////////////////////////////////////////////////////////////////////////////// /// /// Calculates CMA for the entire series using a new instance. /// /// Input series /// CMA series public static TSeries Batch(TSeries source) { var cma = new Cma(); return cma.Update(source); } /// /// Calculates CMA in-place, writing results to pre-allocated output span. /// Zero-allocation method for maximum performance. /// Uses Welford's algorithm for numerical stability. /// /// Input values /// Output span (must be same length as source) public static void Batch(ReadOnlySpan source, Span output) { if (source.Length != output.Length) { throw new ArgumentException("Source and output must have the same length", nameof(output)); } int len = source.Length; if (len == 0) { return; } double mean = 0; double lastValid = double.NaN; // Find first valid value to seed lastValid for (int k = 0; k < len; k++) { if (double.IsFinite(source[k])) { lastValid = source[k]; break; } } // Welford's algorithm for running mean with FMA for (int i = 0; i < len; i++) { double val = source[i]; if (double.IsFinite(val)) { lastValid = val; } else { val = lastValid; } // M_n = M_(n-1) + alpha * delta using FMA for single-rounding precision double alpha = 1.0 / (i + 1); double delta = val - mean; mean = Math.FusedMultiplyAdd(alpha, delta, mean); output[i] = mean; } } /// /// Runs a batch calculation on history and returns /// a "Hot" Cma instance ready to process the next tick immediately. /// /// Historical time series /// A tuple containing the full calculation results and the hot indicator instance public static (TSeries Results, Cma Indicator) Calculate(TSeries source) { var cma = new Cma(); TSeries results = cma.Update(source); return (results, cma); } /// /// Resets the CMA state. /// public override void Reset() { _state = default; _p_state = default; Last = default; } }