// O(n) rolling-window primitives built on monotonic deques.
//
// The deque is an index ring buffer; `last_nan` tracks NaN contamination so
// the outputs match the window-scan semantics used across moontalib: a
// window containing NaN yields NaN (or index -1 for the argmax/argmin
// variants). Ties resolve to the most recent bar, matching the update rules
// of the window-scan implementations.
///|
/// Rolling maximum over a window of `period` values, computed in O(n).
/// Ties resolve to the most recent bar; a window containing NaN yields NaN;
/// the output has the same length as the input with a `period - 1` leading
/// NaN prefix.
pub fn rolling_max(
data : Array[Double],
period : Int32,
) -> Array[Double] raise TaError {
guard_period(data, period)
rolling_extreme(data, period, true)
}
///|
/// Rolling minimum over a window of `period` values, computed in O(n).
pub fn rolling_min(
data : Array[Double],
period : Int32,
) -> Array[Double] raise TaError {
guard_period(data, period)
rolling_extreme(data, period, false)
}
///|
/// Index of the most recent maximum inside each window (0-based), or -1
/// when the window contains NaN; computed in O(n).
pub fn rolling_argmax(
data : Array[Double],
period : Int32,
) -> Array[Int32] raise TaError {
guard_period(data, period)
rolling_argextreme(data, period, true)
}
///|
/// Index of the most recent minimum inside each window (0-based), or -1
/// when the window contains NaN; computed in O(n).
pub fn rolling_argmin(
data : Array[Double],
period : Int32,
) -> Array[Int32] raise TaError {
guard_period(data, period)
rolling_argextreme(data, period, false)
}
///|
/// Shared period validation for the rolling primitives.
fn guard_period(data : Array[Double], period : Int32) -> Unit raise TaError {
if data.length() == 0 {
raise EmptyInput
}
if period <= 0 {
raise InvalidPeriod("period must be positive, got \{period}")
}
if period > data.length() {
raise NotEnoughData(
"period \{period} exceeds input length \{data.length()}",
)
}
}
///|
/// Shared monotonic-deque core. `want_max` keeps a value-decreasing
/// candidate deque (max), `want_min` a value-increasing one (min); popping
/// on ties keeps the most recent extreme index.
fn rolling_extreme(
data : Array[Double],
period : Int32,
want_max : Bool,
) -> Array[Double] {
let n = data.length()
let out : Array[Double] = []
let cap = period + 1
let dq : FixedArray[Int32] = FixedArray::make(cap, 0)
let mut head = 0
let mut tail = 0
let mut size = 0
let mut last_nan = -1
for i in 0.. 0 {
let back = dq[(tail - 1 + cap) % cap]
let take = if want_max { data[back] <= v } else { data[back] >= v }
if take {
tail = (tail - 1 + cap) % cap
size -= 1
} else {
break
}
}
dq[tail] = i
tail = (tail + 1) % cap
size += 1
}
while size > 0 && dq[head] < i - period + 1 {
head = (head + 1) % cap
size -= 1
}
if i < period - 1 || last_nan >= i - period + 1 || size == 0 {
out.push(nan())
} else {
out.push(data[dq[head]])
}
}
out
}
///|
/// Index variant of the monotonic-deque core: the head holds the most
/// recent extreme index; NaN windows report -1.
fn rolling_argextreme(
data : Array[Double],
period : Int32,
want_max : Bool,
) -> Array[Int32] {
let n = data.length()
let out : Array[Int32] = []
let cap = period + 1
let dq : FixedArray[Int32] = FixedArray::make(cap, 0)
let mut head = 0
let mut tail = 0
let mut size = 0
let mut last_nan = -1
for i in 0.. 0 {
let back = dq[(tail - 1 + cap) % cap]
let take = if want_max { data[back] <= v } else { data[back] >= v }
if take {
tail = (tail - 1 + cap) % cap
size -= 1
} else {
break
}
}
dq[tail] = i
tail = (tail + 1) % cap
size += 1
}
while size > 0 && dq[head] < i - period + 1 {
head = (head + 1) % cap
size -= 1
}
if i < period - 1 || last_nan >= i - period + 1 || size == 0 {
out.push(-1)
} else {
out.push(dq[head])
}
}
out
}