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);
}
}
}