using System; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.X86; namespace QuanTAlib; /// /// SMA: Simple Moving Average /// /// /// SMA calculates the arithmetic mean of the last N values. /// Uses a RingBuffer for storage and manual running sum for O(1) operations. /// /// Key characteristics: /// - Equal weighting of all values in the period /// - No lag bias - responds equally to all values in window /// - Smooth output with good noise reduction /// - O(1) time complexity for both update and bar correction /// - O(1) space complexity for state save/restore (scalars only) /// /// Calculation method: /// SMA = Sum(values in period) / period /// /// Bar correction (isNew=false): /// - Restores to state after last isNew=true /// - Then replaces the last value with new correction value /// - All O(1) using scalar state /// /// Sources: /// - https://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:moving_averages /// - https://www.investopedia.com/terms/s/sma.asp /// [SkipLocalsInit] public sealed class Sma { private readonly int _period; private readonly RingBuffer _buffer; private double _sum; private double _p_sum; private double _p_lastInput; private double _lastValidValue; private double _p_lastValidValue; private int _tickCount; private const int ResyncInterval = 1000; /// /// Display name for the indicator. /// public string Name { get; } /// /// Creates SMA with specified period. /// /// Number of values to average (must be > 0) public Sma(int period) { if (period <= 0) throw new ArgumentException("Period must be greater than 0", nameof(period)); _period = period; _buffer = new RingBuffer(period); Name = $"Sma({period})"; } /// /// Current SMA value. /// public TValue Value { get; private set; } /// /// True if the SMA has enough data to produce valid results. /// SMA is "hot" when the buffer is full (has received at least 'period' values). /// public bool IsHot => _buffer.IsFull; /// /// 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; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void UpdateState(double val) { double removedValue = _buffer.Count == _buffer.Capacity ? _buffer.Oldest : 0.0; _sum = _sum - removedValue + val; _buffer.Add(val); _tickCount++; if (_buffer.IsFull && _tickCount >= ResyncInterval) { _tickCount = 0; _sum = _buffer.Sum(); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TValue input, bool isNew = true) { if (isNew) { double val = GetValidValue(input.Value); UpdateState(val); _p_sum = _sum; _p_lastInput = val; _p_lastValidValue = _lastValidValue; } else { _lastValidValue = _p_lastValidValue; double val = GetValidValue(input.Value); _sum = _p_sum - _p_lastInput + val; _buffer.UpdateNewest(val); } double result = _sum / _buffer.Count; Value = new TValue(input.Time, result); return Value; } public TSeries Update(TSeries source) { if (source.Count == 0) return new TSeries(new List(), new List()); 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; Calculate(sourceValues, vSpan, _period); sourceTimes.CopyTo(tSpan); int windowSize = Math.Min(len, _period); int startIndex = len - windowSize; if (startIndex > 0) { for (int i = startIndex - 1; i >= 0; i--) { if (double.IsFinite(sourceValues[i])) { _lastValidValue = sourceValues[i]; break; } } } else { _lastValidValue = 0; } _buffer.Clear(); _sum = 0; _tickCount = 0; for (int i = startIndex; i < len; i++) { double val = GetValidValue(sourceValues[i]); UpdateState(val); } _p_sum = _sum; _p_lastInput = sourceValues[len - 1]; _p_lastValidValue = _lastValidValue; Value = new TValue(tSpan[len - 1], vSpan[len - 1]); return new TSeries(t, v); } /// /// Calculates SMA for the entire series using a new instance. /// /// Input series /// SMA period /// SMA series public static TSeries Calculate(TSeries source, int period) { var sma = new Sma(period); return sma.Update(source); } /// /// Calculates SMA in-place, writing results to pre-allocated output span. /// Zero-allocation method for maximum performance. /// Uses stackalloc circular buffer for NaN-safe sliding window calculation. /// Automatically uses SIMD acceleration for large, clean datasets. /// /// Input values /// Output span (must be same length as source) /// SMA period (must be > 0) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Calculate(ReadOnlySpan source, Span output, int period) { if (source.Length != output.Length) throw new ArgumentException("Source and output must have the same length"); if (period <= 0) throw new ArgumentException("Period must be greater than 0", nameof(period)); int len = source.Length; if (len == 0) return; // Try SIMD path for large, clean datasets // Requirements: AVX2 support, large enough dataset, no NaN values const int SimdThreshold = 256; if (Avx2.IsSupported && len >= SimdThreshold && !HasNonFiniteValues(source)) { CalculateSimdCore(source, output, period); return; } // Scalar path with NaN handling CalculateScalarCore(source, output, period); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void CalculateScalarCore(ReadOnlySpan source, Span output, int period) { int len = source.Length; const int StackAllocThreshold = 256; Span buffer = period <= StackAllocThreshold ? stackalloc double[period] : new double[period]; double sum = 0; double lastValid = 0; int bufferIndex = 0; int i = 0; int warmupEnd = Math.Min(period, len); for (; i < warmupEnd; i++) { double val = source[i]; if (double.IsFinite(val)) lastValid = val; else val = lastValid; sum += val; buffer[i] = val; output[i] = sum / (i + 1); } int tickCount = 0; for (; i < len; i++) { double val = source[i]; if (double.IsFinite(val)) lastValid = val; else val = lastValid; sum = sum - buffer[bufferIndex] + val; buffer[bufferIndex] = val; bufferIndex++; if (bufferIndex >= period) bufferIndex = 0; output[i] = sum / period; tickCount++; if (tickCount >= ResyncInterval) { tickCount = 0; double recalcSum = 0; for (int k = 0; k < period; k++) { recalcSum += buffer[k]; } sum = recalcSum; } } } [MethodImpl(MethodImplOptions.AggressiveOptimization)] #pragma warning disable S6640 // Unsafe code is required for high-performance SIMD operations private static unsafe void CalculateSimdCore(ReadOnlySpan source, Span output, int period) { int len = source.Length; const int VectorWidth = 4; fixed (double* srcPtr = source) fixed (double* outPtr = output) { double invPeriod = 1.0 / period; int warmupEnd = Math.Min(period, len); double sum = 0; for (int i = 0; i < warmupEnd; i++) { sum += srcPtr[i]; outPtr[i] = sum / (i + 1); } if (len <= period) return; var vInvPeriod = Vector256.Create(invPeriod); var vZero = Vector256.Zero; int simdEnd = period + ((len - period) / VectorWidth) * VectorWidth; int tickCount = 0; for (int i = period; i < simdEnd; i += VectorWidth) { var vNew = Avx.LoadVector256(srcPtr + i); var vOld = Avx.LoadVector256(srcPtr + i - period); var vDelta = Avx.Subtract(vNew, vOld); var vShift1 = Avx2.Permute4x64(vDelta.AsUInt64(), 0b_10_01_00_00).AsDouble(); vShift1 = Avx.Blend(vZero, vShift1, 0b_1110); var vP1 = Avx.Add(vDelta, vShift1); var vShift2 = Avx2.Permute4x64(vP1.AsUInt64(), 0b_01_00_00_00).AsDouble(); vShift2 = Avx.Blend(vZero, vShift2, 0b_1100); var vP2 = Avx.Add(vP1, vShift2); var vSumPrev = Vector256.Create(sum); var vSums = Avx.Add(vSumPrev, vP2); var vResult = Avx.Multiply(vSums, vInvPeriod); Avx.Store(outPtr + i, vResult); sum = vSums.GetElement(3); tickCount += VectorWidth; if (tickCount >= ResyncInterval) { tickCount = 0; int lastIdx = i + VectorWidth - 1; double recalcSum = 0; for (int k = 0; k < period; k++) { recalcSum += srcPtr[lastIdx - k]; } sum = recalcSum; } } for (int i = simdEnd; i < len; i++) { sum = sum - srcPtr[i - period] + srcPtr[i]; outPtr[i] = sum * invPeriod; } } } #pragma warning restore S6640 /// /// Checks if span contains any non-finite values (NaN or Infinity). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool HasNonFiniteValues(ReadOnlySpan span) { for (int idx = 0; idx < span.Length; idx++) { if (!double.IsFinite(span[idx])) return true; } return false; } /// /// Resets the SMA state. /// public void Reset() { _buffer.Clear(); _sum = 0; _p_sum = 0; _p_lastInput = 0; _lastValidValue = 0; _p_lastValidValue = 0; _tickCount = 0; Value = default; } }