Files
ferro-ta/crates/ferro_ta_core/src/math.rs
T
Pratik Bhadane cf5d7764ba chore: bump version to 1.1.1 and update changelog
- Updated version numbers across Cargo.toml, Cargo.lock, pyproject.toml, and conda/meta.yaml to 1.1.1.
- Added new features and improvements in CHANGELOG.md for version 1.1.1, including full feature parity across Rust, Python, and WASM targets, and numerous new indicator functions in ferro_ta_core.
2026-04-01 23:03:05 +05:30

218 lines
6.5 KiB
Rust

//! Math utilities.
use std::collections::VecDeque;
/// Compute the rolling sum over `timeperiod` bars.
///
/// Returns a `Vec<f64>` of length `n`. The first `timeperiod - 1` values
/// are `NaN`. Uses an incremental algorithm (add new, subtract old) for O(n).
///
/// # Arguments
/// * `real` - Input series.
/// * `timeperiod` - Rolling window size (must be >= 1).
pub fn sum(real: &[f64], timeperiod: usize) -> Vec<f64> {
let n = real.len();
let mut result = vec![f64::NAN; n];
if timeperiod < 1 || n < timeperiod {
return result;
}
let mut win: f64 = real[..timeperiod].iter().sum();
result[timeperiod - 1] = win;
for i in timeperiod..n {
win += real[i] - real[i - timeperiod];
result[i] = win;
}
result
}
/// Compute the rolling maximum over `timeperiod` bars.
///
/// Delegates to [`sliding_max`] for O(n) performance via a monotonic deque.
/// The first `timeperiod - 1` values are `NaN`.
///
/// # Arguments
/// * `real` - Input series.
/// * `timeperiod` - Rolling window size (must be >= 1).
pub fn max(real: &[f64], timeperiod: usize) -> Vec<f64> {
sliding_max(real, timeperiod)
}
/// Compute the rolling minimum over `timeperiod` bars.
///
/// Delegates to [`sliding_min`] for O(n) performance via a monotonic deque.
/// The first `timeperiod - 1` values are `NaN`.
///
/// # Arguments
/// * `real` - Input series.
/// * `timeperiod` - Rolling window size (must be >= 1).
pub fn min(real: &[f64], timeperiod: usize) -> Vec<f64> {
sliding_min(real, timeperiod)
}
/// Compute the sliding maximum over `timeperiod` bars in O(n) time.
///
/// Uses a monotonic decreasing deque so each element is pushed/popped at
/// most once. The first `timeperiod - 1` values are `NaN`.
///
/// # Arguments
/// * `real` - Input series.
/// * `timeperiod` - Rolling window size (must be >= 1).
pub fn sliding_max(real: &[f64], timeperiod: usize) -> Vec<f64> {
let n = real.len();
let mut result = vec![f64::NAN; n];
if timeperiod < 1 || n < timeperiod {
return result;
}
let mut dq: VecDeque<usize> = VecDeque::new();
for i in 0..n {
// Remove indices outside the window
while dq.front().map(|&j| j + timeperiod <= i).unwrap_or(false) {
dq.pop_front();
}
// Maintain decreasing deque
while dq.back().map(|&j| real[j] <= real[i]).unwrap_or(false) {
dq.pop_back();
}
dq.push_back(i);
if i + 1 >= timeperiod {
result[i] = real[*dq.front().unwrap()];
}
}
result
}
/// Compute the sliding minimum over `timeperiod` bars in O(n) time.
///
/// Uses a monotonic increasing deque so each element is pushed/popped at
/// most once. The first `timeperiod - 1` values are `NaN`.
///
/// # Arguments
/// * `real` - Input series.
/// * `timeperiod` - Rolling window size (must be >= 1).
pub fn sliding_min(real: &[f64], timeperiod: usize) -> Vec<f64> {
let n = real.len();
let mut result = vec![f64::NAN; n];
if timeperiod < 1 || n < timeperiod {
return result;
}
let mut dq: VecDeque<usize> = VecDeque::new();
for i in 0..n {
// Remove indices outside the window
while dq.front().map(|&j| j + timeperiod <= i).unwrap_or(false) {
dq.pop_front();
}
// Maintain increasing deque
while dq.back().map(|&j| real[j] >= real[i]).unwrap_or(false) {
dq.pop_back();
}
dq.push_back(i);
if i + 1 >= timeperiod {
result[i] = real[*dq.front().unwrap()];
}
}
result
}
// ---------------------------------------------------------------------------
// Element-wise arithmetic operators
// ---------------------------------------------------------------------------
/// Element-wise addition of two arrays.
pub fn add(a: &[f64], b: &[f64]) -> Vec<f64> {
a.iter().zip(b.iter()).map(|(&x, &y)| x + y).collect()
}
/// Element-wise subtraction of two arrays.
pub fn sub(a: &[f64], b: &[f64]) -> Vec<f64> {
a.iter().zip(b.iter()).map(|(&x, &y)| x - y).collect()
}
/// Element-wise multiplication of two arrays.
pub fn mult(a: &[f64], b: &[f64]) -> Vec<f64> {
a.iter().zip(b.iter()).map(|(&x, &y)| x * y).collect()
}
/// Element-wise division of two arrays (NaN where b=0).
pub fn div(a: &[f64], b: &[f64]) -> Vec<f64> {
a.iter()
.zip(b.iter())
.map(|(&x, &y)| if y != 0.0 { x / y } else { f64::NAN })
.collect()
}
// ---------------------------------------------------------------------------
// Element-wise math transforms
// ---------------------------------------------------------------------------
macro_rules! unary_transform {
($name:ident, $method:ident) => {
pub fn $name(real: &[f64]) -> Vec<f64> {
real.iter().map(|&x| x.$method()).collect()
}
};
}
unary_transform!(math_acos, acos);
unary_transform!(math_asin, asin);
unary_transform!(math_atan, atan);
unary_transform!(math_ceil, ceil);
unary_transform!(math_cos, cos);
unary_transform!(math_cosh, cosh);
unary_transform!(math_exp, exp);
unary_transform!(math_floor, floor);
unary_transform!(math_ln, ln);
unary_transform!(math_log10, log10);
unary_transform!(math_sin, sin);
unary_transform!(math_sinh, sinh);
unary_transform!(math_sqrt, sqrt);
unary_transform!(math_tan, tan);
unary_transform!(math_tanh, tanh);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sum_basic() {
let v = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let r = sum(&v, 3);
assert!(r[0].is_nan());
assert!((r[2] - 6.0).abs() < 1e-10);
assert!((r[4] - 12.0).abs() < 1e-10);
}
#[test]
fn max_basic() {
let v = vec![3.0, 1.0, 4.0, 1.0, 5.0];
let r = max(&v, 3);
assert!((r[2] - 4.0).abs() < 1e-10);
assert!((r[4] - 5.0).abs() < 1e-10);
}
#[test]
fn sliding_max_matches_naive() {
let v = vec![3.0, 1.0, 4.0, 1.0, 5.0, 9.0, 2.0, 6.0];
let naive = max(&v, 3);
let fast = sliding_max(&v, 3);
for i in 0..v.len() {
assert_eq!(naive[i].is_nan(), fast[i].is_nan());
if !naive[i].is_nan() {
assert!((naive[i] - fast[i]).abs() < 1e-10);
}
}
}
#[test]
fn sliding_min_matches_naive() {
let v = vec![3.0, 1.0, 4.0, 1.0, 5.0, 9.0, 2.0, 6.0];
let naive = min(&v, 3);
let fast = sliding_min(&v, 3);
for i in 0..v.len() {
assert_eq!(naive[i].is_nan(), fast[i].is_nan());
if !naive[i].is_nan() {
assert!((naive[i] - fast[i]).abs() < 1e-10);
}
}
}
}