using System; using System.Collections; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// A high-performance circular buffer for double values optimized for SIMD operations. /// Uses pinned memory and maintains running sum for O(1) average calculations. /// /// /// Key characteristics: /// - Fixed capacity set at construction /// - Pinned memory for SIMD compatibility /// - O(1) Add and Sum operations via running sum /// - SIMD-accelerated Min/Max operations /// - Direct span access when buffer is contiguous /// - Thread-unsafe for maximum performance /// [SkipLocalsInit] public sealed class RingBuffer : IEnumerable { private readonly double[] _buffer; private readonly int _capacity; private int _head; private int _count; private double _sum; // Snapshot state private int _savedHead; private int _savedCount; private double _savedSum; private double _savedValue; /// /// Creates a new RingBuffer with the specified capacity. /// Uses pinned memory for SIMD compatibility. /// /// Maximum number of elements (must be > 0) public RingBuffer(int capacity) { if (capacity <= 0) throw new ArgumentException("Capacity must be greater than 0", nameof(capacity)); _capacity = capacity; _buffer = GC.AllocateArray(capacity, pinned: true); _head = 0; _count = 0; _sum = 0; } /// /// Maximum number of elements the buffer can hold. /// public int Capacity { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _capacity; } /// /// Current number of elements in the buffer. /// public int Count { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _count; } /// /// True if the buffer is full (Count == Capacity). /// public bool IsFull { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _count == _capacity; } /// /// Running sum of all elements in the buffer. /// O(1) operation using maintained running sum. /// public double Sum { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _sum; } /// /// Recalculates the sum by iterating over all elements. /// Useful for correcting floating-point drift after many updates. /// public double RecalculateSum() { double sum = 0; var span = GetSpan(); for (int i = 0; i < span.Length; i++) { sum += span[i]; } _sum = sum; return sum; } /// /// Average of all elements in the buffer. /// Returns 0 if buffer is empty. /// public double Average { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _count > 0 ? _sum / _count : 0; } /// /// Gets the newest (most recently added) value. /// Returns double.NaN if buffer is empty. /// public double Newest { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { if (_count == 0) return double.NaN; int idx = (_head - 1 + _capacity) % _capacity; return _buffer[idx]; } } /// /// Gets the oldest value in the buffer. /// Returns double.NaN if buffer is empty. /// public double Oldest { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { if (_count == 0) return double.NaN; int start = _count == _capacity ? _head : 0; return _buffer[start]; } } /// /// Gets the index in the internal buffer where the oldest element is located. /// public int StartIndex { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _count == _capacity ? _head : 0; } /// /// Gets a read-only span over the internal buffer array for direct SIMD access. /// public ReadOnlySpan InternalBuffer { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _buffer.AsSpan(); } /// /// Adds a value to the buffer. /// If full, the oldest value is overwritten and its value is subtracted from the sum. /// Returns the value that was removed (0 if buffer was not full). /// /// Value to add /// The removed oldest value, or 0 if buffer was not full [MethodImpl(MethodImplOptions.AggressiveInlining)] public double Add(double value) { double removed = 0; if (_count == _capacity) { removed = _buffer[_head]; _sum -= removed; } else { _count++; } _buffer[_head] = value; _sum += value; _head = (_head + 1) % _capacity; return removed; } /// /// Adds a value with support for bar correction semantics. /// /// Value to add /// True for new bar, false for update to current bar [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Add(double value, bool isNew) { if (isNew || _count == 0) { Add(value); } else { UpdateNewest(value); } } /// /// Updates the newest (most recently added) value. /// This is used for bar correction (isNew=false semantics). /// /// New value to replace the newest [MethodImpl(MethodImplOptions.AggressiveInlining)] public void UpdateNewest(double value) { if (_count == 0) return; int idx = (_head - 1 + _capacity) % _capacity; double oldValue = _buffer[idx]; _sum -= oldValue; _sum += value; _buffer[idx] = value; } /// /// Gets or sets element at the specified index (0 = oldest, Count-1 = newest). /// Supports negative indexing via Index type. /// public double this[Index index] { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => GetAt(index); [MethodImpl(MethodImplOptions.AggressiveInlining)] set => SetAt(index, value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private double GetAt(Index index) { int actualIndex = index.IsFromEnd ? _count - index.Value : index.Value; ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)actualIndex, (uint)_count); int start = _count == _capacity ? _head : 0; int bufferIdx = (start + actualIndex) % _capacity; return _buffer[bufferIdx]; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void SetAt(Index index, double value) { int actualIndex = index.IsFromEnd ? _count - index.Value : index.Value; ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)actualIndex, (uint)_count); int start = _count == _capacity ? _head : 0; int bufferIdx = (start + actualIndex) % _capacity; _sum -= _buffer[bufferIdx]; _sum += value; _buffer[bufferIdx] = value; } /// /// Returns a span over the buffer contents. /// If buffer is contiguous, returns direct span (SIMD-friendly). /// If wrapped, returns span over a copy. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public ReadOnlySpan GetSpan() { if (_count == 0) return ReadOnlySpan.Empty; int start = _count == _capacity ? _head : 0; if (start + _count <= _capacity) { return new ReadOnlySpan(_buffer, start, _count); } return new ReadOnlySpan(ToArray()); } /// /// Returns a span over the entire internal buffer (for advanced SIMD use). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public ReadOnlySpan GetInternalSpan() => _buffer.AsSpan(); /// /// Returns the maximum value in the buffer using SIMD acceleration. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public double Max() { if (_count == 0) return double.NaN; return MaxSimd(); } /// /// Returns the minimum value in the buffer using SIMD acceleration. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public double Min() { if (_count == 0) return double.NaN; return MinSimd(); } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private double MaxSimd() { var span = GetSpan(); var vectorSize = Vector.Count; var maxVector = new Vector(double.MinValue); int i = 0; ref double spanRef = ref MemoryMarshal.GetReference(span); for (; i <= span.Length - vectorSize; i += vectorSize) { maxVector = Vector.Max(maxVector, Unsafe.As>(ref Unsafe.Add(ref spanRef, i))); } double max = double.MinValue; for (int j = 0; j < vectorSize; j++) { max = Math.Max(max, maxVector[j]); } for (; i < span.Length; i++) { max = Math.Max(max, span[i]); } return max; } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private double MinSimd() { var span = GetSpan(); var vectorSize = Vector.Count; var minVector = new Vector(double.MaxValue); int i = 0; ref double spanRef = ref MemoryMarshal.GetReference(span); for (; i <= span.Length - vectorSize; i += vectorSize) { minVector = Vector.Min(minVector, Unsafe.As>(ref Unsafe.Add(ref spanRef, i))); } double min = double.MaxValue; for (int j = 0; j < vectorSize; j++) { min = Math.Min(min, minVector[j]); } for (; i < span.Length; i++) { min = Math.Min(min, span[i]); } return min; } /// /// Clears all elements from the buffer. /// public void Clear() { Array.Clear(_buffer, 0, _buffer.Length); _head = 0; _count = 0; _sum = 0; } /// /// Copies the buffer elements to a new array in chronological order. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public double[] ToArray() { if (_count == 0) return Array.Empty(); double[] array = new double[_count]; CopyTo(array, 0); return array; } /// /// Copies elements to destination array starting at destinationIndex. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void CopyTo(double[] destination, int destinationIndex) { if (_count == 0) return; int start = _count == _capacity ? _head : 0; if (start + _count <= _capacity) { Array.Copy(_buffer, start, destination, destinationIndex, _count); } else { int firstPartLength = _capacity - start; Array.Copy(_buffer, start, destination, destinationIndex, firstPartLength); Array.Copy(_buffer, 0, destination, destinationIndex + firstPartLength, _count - firstPartLength); } } /// /// Creates a copy of the current state for bar correction support. /// public RingBuffer Clone() { var clone = new RingBuffer(_capacity); Array.Copy(_buffer, clone._buffer, _capacity); clone._head = _head; clone._count = _count; clone._sum = _sum; return clone; } /// /// Copies state from another RingBuffer. /// Both buffers must have the same capacity. /// public void CopyFrom(RingBuffer source) { if (source._capacity != _capacity) throw new ArgumentException("Source buffer must have same capacity", nameof(source)); Array.Copy(source._buffer, _buffer, _capacity); _head = source._head; _count = source._count; _sum = source._sum; } /// /// Captures the current state of the buffer. /// Must be called BEFORE adding a new value if you intend to Restore later. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Snapshot() { _savedHead = _head; _savedCount = _count; _savedSum = _sum; _savedValue = _buffer[_head]; } /// /// Restores the buffer to the state captured by Snapshot. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Restore() { _head = _savedHead; _count = _savedCount; _sum = _savedSum; _buffer[_head] = _savedValue; } /// /// Returns an enumerator that iterates through the buffer in chronological order. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public Enumerator GetEnumerator() => new(this); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// /// High-performance enumerator for the RingBuffer. /// public struct Enumerator : IEnumerator, IEquatable { private readonly RingBuffer _buffer; private readonly int _start; private readonly int _count; private int _index; private double _current; [MethodImpl(MethodImplOptions.AggressiveInlining)] internal Enumerator(RingBuffer buffer) { _buffer = buffer; _count = buffer._count; _start = buffer._count == buffer._capacity ? buffer._head : 0; _index = -1; _current = default; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool MoveNext() { if (_index + 1 >= _count) return false; _index++; int bufferIdx = (_start + _index) % _buffer._capacity; _current = _buffer._buffer[bufferIdx]; return true; } public readonly double Current => _current; readonly object IEnumerator.Current => Current; [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Reset() { _index = -1; _current = default; } public readonly void Dispose() { } public readonly bool Equals(Enumerator other) => ReferenceEquals(_buffer, other._buffer) && _start == other._start && _count == other._count && _index == other._index && _current.Equals(other._current); public override readonly bool Equals(object? obj) => obj is Enumerator other && Equals(other); public override readonly int GetHashCode() => HashCode.Combine(RuntimeHelpers.GetHashCode(_buffer), _start, _count, _index, _current); public static bool operator ==(Enumerator left, Enumerator right) => left.Equals(right); public static bool operator !=(Enumerator left, Enumerator right) => !left.Equals(right); } }