using System.Runtime.CompilerServices; namespace QuanTAlib; /// /// A monotonic deque for O(1) amortized sliding window min/max queries. /// Maintains elements in strictly monotonic order (non-increasing for max, non-decreasing for min). /// Used by channel indicators (Donchian, MinMax) for efficient rolling extrema. /// /// /// Algorithm: For each new element, expire indices outside the window, then pop elements /// from the back that would violate monotonicity, then push the new index. /// Time complexity: O(1) amortized per operation (each element pushed/popped at most once). /// Space complexity: O(period) for the deque array. /// [SkipLocalsInit] public sealed class MonotonicDeque { private readonly long[] _deque; private readonly int _period; private int _head; private int _count; /// /// Gets the current front index (the index of the current extremum). /// public long FrontIndex => _count > 0 ? _deque[_head] : -1; /// /// Gets the current element count in the deque. /// public int Count => _count; /// /// Creates a new monotonic deque with the specified period. /// /// The sliding window size. public MonotonicDeque(int period) { if (period <= 0) { throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than 0"); } _period = period; _deque = new long[period]; _head = 0; _count = 0; } /// /// Pushes a value for maximum tracking (maintains non-increasing order). /// Smaller or equal values are removed from the back before pushing. /// /// The logical index of the value (used for expiration). /// The value to push. /// The circular buffer containing values (indexed by logicalIndex % period). [MethodImpl(MethodImplOptions.AggressiveInlining)] public void PushMax(long logicalIndex, double value, double[] buffer) { // Expire old indices outside the window long expire = logicalIndex - _period; while (_count > 0 && _deque[_head] <= expire) { _head = (_head + 1) % _period; _count--; } // Pop elements from back that are <= value (maintain non-increasing order) while (_count > 0) { int backIdx = (_head + _count - 1) % _period; int bufIdx = (int)(_deque[backIdx] % _period); if (buffer[bufIdx] <= value) { _count--; } else { break; } } // Push new index int tail = (_head + _count) % _period; _deque[tail] = logicalIndex; _count++; } /// /// Pushes a value for minimum tracking (maintains non-decreasing order). /// Larger or equal values are removed from the back before pushing. /// /// The logical index of the value (used for expiration). /// The value to push. /// The circular buffer containing values (indexed by logicalIndex % period). [MethodImpl(MethodImplOptions.AggressiveInlining)] public void PushMin(long logicalIndex, double value, double[] buffer) { // Expire old indices outside the window long expire = logicalIndex - _period; while (_count > 0 && _deque[_head] <= expire) { _head = (_head + 1) % _period; _count--; } // Pop elements from back that are >= value (maintain non-decreasing order) while (_count > 0) { int backIdx = (_head + _count - 1) % _period; int bufIdx = (int)(_deque[backIdx] % _period); if (buffer[bufIdx] >= value) { _count--; } else { break; } } // Push new index int tail = (_head + _count) % _period; _deque[tail] = logicalIndex; _count++; } /// /// Gets the current extremum value from the buffer. /// /// The circular buffer containing values. /// The value at the front of the deque, or NaN if empty. [MethodImpl(MethodImplOptions.AggressiveInlining)] public double GetExtremum(double[] buffer) { return _count > 0 ? buffer[(int)(_deque[_head] % _period)] : double.NaN; } /// /// Resets the deque to empty state. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Reset() { _head = 0; _count = 0; } /// /// Rebuilds the max deque from scratch using the buffer contents. /// Used after bar corrections (isNew=false) to maintain consistency. /// /// The circular buffer containing values. /// The current logical index. /// The number of valid elements in the buffer. public void RebuildMax(double[] buffer, long currentIndex, int count) { Reset(); if (count == 0) { return; } long startLogical = currentIndex - count + 1; for (int i = 0; i < count; i++) { long logicalIndex = startLogical + i; int bufIdx = (int)(logicalIndex % _period); PushMax(logicalIndex, buffer[bufIdx], buffer); } } /// /// Rebuilds the min deque from scratch using the buffer contents. /// Used after bar corrections (isNew=false) to maintain consistency. /// /// The circular buffer containing values. /// The current logical index. /// The number of valid elements in the buffer. public void RebuildMin(double[] buffer, long currentIndex, int count) { Reset(); if (count == 0) { return; } long startLogical = currentIndex - count + 1; for (int i = 0; i < count; i++) { long logicalIndex = startLogical + i; int bufIdx = (int)(logicalIndex % _period); PushMin(logicalIndex, buffer[bufIdx], buffer); } } }