chore: prepare v1.1.0 release

Update version numbers across Rust, Python, and documentation files to 1.1.0. Enhance the .gitignore to include macOS dSYM files and plans directory. Introduce new dependencies in the Rust core library and update the README to reflect recent performance benchmarks and backtesting engine capabilities. Add new artifacts to the benchmarks manifest and improve documentation for the backtesting engine API.
This commit is contained in:
Pratik Bhadane
2026-03-30 12:45:52 +05:30
parent 2d776b6f90
commit 436954138f
174 changed files with 29297 additions and 10773 deletions
+8 -129
View File
@@ -1,26 +1,10 @@
//! Rust rolling math operators — O(n) sliding window using monotonic deques.
//!
//! Functions exposed to Python:
//! rolling_sum — Rolling sum over `timeperiod` bars
//! rolling_max — Rolling maximum (O(n) via monotonic deque)
//! rolling_min — Rolling minimum (O(n) via monotonic deque)
//! rolling_maxindex — Index of rolling maximum
//! rolling_minindex — Index of rolling minimum
use std::collections::VecDeque;
//! Rolling math operators (thin PyO3 wrapper over ferro_ta_core::math_ops).
use crate::validation;
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
use pyo3::prelude::*;
// ---------------------------------------------------------------------------
// rolling_sum
// ---------------------------------------------------------------------------
/// Rolling sum over `timeperiod` bars.
///
/// Uses a prefix-sum array for O(n) computation.
/// Leading `timeperiod - 1` values are NaN.
#[pyfunction]
#[pyo3(signature = (real, timeperiod = 30))]
pub fn rolling_sum<'py>(
@@ -30,29 +14,11 @@ pub fn rolling_sum<'py>(
) -> PyResult<Bound<'py, PyArray1<f64>>> {
validation::validate_timeperiod(timeperiod, "timeperiod", 1)?;
let prices = real.as_slice()?;
let n = prices.len();
let mut result = vec![f64::NAN; n];
if n < timeperiod {
return Ok(result.into_pyarray(py));
}
// Prefix sum
let mut cs = vec![0.0f64; n + 1];
for i in 0..n {
cs[i + 1] = cs[i] + prices[i];
}
for i in (timeperiod - 1)..n {
result[i] = cs[i + 1] - cs[i + 1 - timeperiod];
}
let result = ferro_ta_core::math_ops::rolling_sum(prices, timeperiod);
Ok(result.into_pyarray(py))
}
// ---------------------------------------------------------------------------
// rolling_max
// ---------------------------------------------------------------------------
/// Rolling maximum over `timeperiod` bars (O(n) monotonic deque).
///
/// Leading `timeperiod - 1` values are NaN.
#[pyfunction]
#[pyo3(signature = (real, timeperiod = 30))]
pub fn rolling_max<'py>(
@@ -62,34 +28,11 @@ pub fn rolling_max<'py>(
) -> PyResult<Bound<'py, PyArray1<f64>>> {
validation::validate_timeperiod(timeperiod, "timeperiod", 1)?;
let prices = real.as_slice()?;
let n = prices.len();
let mut result = vec![f64::NAN; n];
let mut dq: VecDeque<usize> = VecDeque::new();
for i in 0..n {
// Remove indices out of the window
while dq.front().map(|&j| j + timeperiod <= i).unwrap_or(false) {
dq.pop_front();
}
// Maintain decreasing deque
while dq.back().map(|&j| prices[j] <= prices[i]).unwrap_or(false) {
dq.pop_back();
}
dq.push_back(i);
if i + 1 >= timeperiod {
result[i] = prices[*dq.front().unwrap()];
}
}
let result = ferro_ta_core::math_ops::rolling_max(prices, timeperiod);
Ok(result.into_pyarray(py))
}
// ---------------------------------------------------------------------------
// rolling_min
// ---------------------------------------------------------------------------
/// Rolling minimum over `timeperiod` bars (O(n) monotonic deque).
///
/// Leading `timeperiod - 1` values are NaN.
#[pyfunction]
#[pyo3(signature = (real, timeperiod = 30))]
pub fn rolling_min<'py>(
@@ -99,34 +42,11 @@ pub fn rolling_min<'py>(
) -> PyResult<Bound<'py, PyArray1<f64>>> {
validation::validate_timeperiod(timeperiod, "timeperiod", 1)?;
let prices = real.as_slice()?;
let n = prices.len();
let mut result = vec![f64::NAN; n];
let mut dq: VecDeque<usize> = VecDeque::new();
for i in 0..n {
while dq.front().map(|&j| j + timeperiod <= i).unwrap_or(false) {
dq.pop_front();
}
// Maintain increasing deque
while dq.back().map(|&j| prices[j] >= prices[i]).unwrap_or(false) {
dq.pop_back();
}
dq.push_back(i);
if i + 1 >= timeperiod {
result[i] = prices[*dq.front().unwrap()];
}
}
let result = ferro_ta_core::math_ops::rolling_min(prices, timeperiod);
Ok(result.into_pyarray(py))
}
// ---------------------------------------------------------------------------
// rolling_maxindex
// ---------------------------------------------------------------------------
/// Index of rolling maximum over `timeperiod` bars (O(n) monotonic deque).
///
/// Returns the 0-based index into the input array. During the warmup window
/// the value is `-1` (not valid — mask with warmup period if needed).
/// Index of rolling maximum over `timeperiod` bars.
#[pyfunction]
#[pyo3(signature = (real, timeperiod = 30))]
pub fn rolling_maxindex<'py>(
@@ -136,33 +56,11 @@ pub fn rolling_maxindex<'py>(
) -> PyResult<Bound<'py, PyArray1<i64>>> {
validation::validate_timeperiod(timeperiod, "timeperiod", 1)?;
let prices = real.as_slice()?;
let n = prices.len();
let mut result = vec![-1i64; n];
let mut dq: VecDeque<usize> = VecDeque::new();
for i in 0..n {
while dq.front().map(|&j| j + timeperiod <= i).unwrap_or(false) {
dq.pop_front();
}
while dq.back().map(|&j| prices[j] <= prices[i]).unwrap_or(false) {
dq.pop_back();
}
dq.push_back(i);
if i + 1 >= timeperiod {
result[i] = *dq.front().unwrap() as i64;
}
}
let result = ferro_ta_core::math_ops::rolling_maxindex(prices, timeperiod);
Ok(result.into_pyarray(py))
}
// ---------------------------------------------------------------------------
// rolling_minindex
// ---------------------------------------------------------------------------
/// Index of rolling minimum over `timeperiod` bars (O(n) monotonic deque).
///
/// Returns the 0-based index into the input array. During the warmup window
/// the value is `-1` (not valid — mask with warmup period if needed).
/// Index of rolling minimum over `timeperiod` bars.
#[pyfunction]
#[pyo3(signature = (real, timeperiod = 30))]
pub fn rolling_minindex<'py>(
@@ -172,29 +70,10 @@ pub fn rolling_minindex<'py>(
) -> PyResult<Bound<'py, PyArray1<i64>>> {
validation::validate_timeperiod(timeperiod, "timeperiod", 1)?;
let prices = real.as_slice()?;
let n = prices.len();
let mut result = vec![-1i64; n];
let mut dq: VecDeque<usize> = VecDeque::new();
for i in 0..n {
while dq.front().map(|&j| j + timeperiod <= i).unwrap_or(false) {
dq.pop_front();
}
while dq.back().map(|&j| prices[j] >= prices[i]).unwrap_or(false) {
dq.pop_back();
}
dq.push_back(i);
if i + 1 >= timeperiod {
result[i] = *dq.front().unwrap() as i64;
}
}
let result = ferro_ta_core::math_ops::rolling_minindex(prices, timeperiod);
Ok(result.into_pyarray(py))
}
// ---------------------------------------------------------------------------
// register
// ---------------------------------------------------------------------------
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(pyo3::wrap_pyfunction!(rolling_sum, m)?)?;
m.add_function(pyo3::wrap_pyfunction!(rolling_max, m)?)?;