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:
+13
-110
@@ -1,40 +1,20 @@
|
||||
//! Alerts — condition evaluation helpers.
|
||||
//!
|
||||
//! These Rust functions evaluate conditions over price/indicator series and
|
||||
//! return boolean or integer arrays indicating where conditions fire. They
|
||||
//! are designed to be called once per batch (backtest) or per bar (live) and
|
||||
//! return the full history of firings.
|
||||
//!
|
||||
//! Functions
|
||||
//! ---------
|
||||
//! - `check_threshold` — fires when a series crosses above/below a level
|
||||
//! - `check_cross` — fires when *fast* crosses above or below *slow*
|
||||
//! - `collect_alert_bars` — returns indices of bars where a bool mask is True
|
||||
//! Alerts — condition evaluation helpers (thin PyO3 wrapper over ferro_ta_core::alerts).
|
||||
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::exceptions::PyValueError;
|
||||
use pyo3::prelude::*;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// check_threshold
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Fire an alert when *series* crosses a threshold level.
|
||||
///
|
||||
/// Parameters
|
||||
/// ----------
|
||||
/// series : 1-D float64 array — indicator values (e.g. RSI)
|
||||
/// series : 1-D float64 array
|
||||
/// level : float — threshold value
|
||||
/// direction : int
|
||||
/// ``1`` → fire when series crosses **above** *level* (value goes from
|
||||
/// ≤ level to > level).
|
||||
/// ``-1`` → fire when series crosses **below** *level* (value goes from
|
||||
/// ≥ level to < level).
|
||||
/// direction : int — ``1`` (cross above) or ``-1`` (cross below)
|
||||
///
|
||||
/// Returns
|
||||
/// -------
|
||||
/// 1-D int8 array — 1 at the bar where the crossing occurs, 0 elsewhere.
|
||||
/// Element 0 is always 0 (no crossing possible without a prior bar).
|
||||
/// 1-D int8 array — 1 at crossing bars, 0 elsewhere.
|
||||
#[pyfunction]
|
||||
pub fn check_threshold<'py>(
|
||||
py: Python<'py>,
|
||||
@@ -48,50 +28,15 @@ pub fn check_threshold<'py>(
|
||||
));
|
||||
}
|
||||
let s = series.as_slice()?;
|
||||
let n = s.len();
|
||||
let mut out = vec![0i8; n];
|
||||
if n < 2 {
|
||||
return Ok(out.into_pyarray(py));
|
||||
}
|
||||
for i in 1..n {
|
||||
let prev = s[i - 1];
|
||||
let curr = s[i];
|
||||
if prev.is_nan() || curr.is_nan() {
|
||||
continue;
|
||||
}
|
||||
if direction == 1 {
|
||||
// cross above: was at or below level, now above
|
||||
if prev <= level && curr > level {
|
||||
out[i] = 1;
|
||||
}
|
||||
} else {
|
||||
// cross below: was at or above level, now below
|
||||
if prev >= level && curr < level {
|
||||
out[i] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out.into_pyarray(py))
|
||||
let result = ferro_ta_core::alerts::check_threshold(s, level, direction);
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// check_cross
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Detect cross-over / cross-under events between two series.
|
||||
///
|
||||
/// Parameters
|
||||
/// ----------
|
||||
/// fast : 1-D float64 array — the "fast" series (e.g. short SMA)
|
||||
/// slow : 1-D float64 array — the "slow" series (e.g. long SMA)
|
||||
///
|
||||
/// Returns
|
||||
/// -------
|
||||
/// 1-D int8 array:
|
||||
/// ``1`` at bars where *fast* crosses **above** *slow* (bullish cross)
|
||||
/// ``-1`` at bars where *fast* crosses **below** *slow* (bearish cross)
|
||||
/// ``0`` elsewhere
|
||||
/// Element 0 is always 0.
|
||||
/// 1-D int8 array: ``1`` = bullish, ``-1`` = bearish, ``0`` = none.
|
||||
#[pyfunction]
|
||||
pub fn check_cross<'py>(
|
||||
py: Python<'py>,
|
||||
@@ -100,68 +45,26 @@ pub fn check_cross<'py>(
|
||||
) -> PyResult<Bound<'py, PyArray1<i8>>> {
|
||||
let f = fast.as_slice()?;
|
||||
let s = slow.as_slice()?;
|
||||
let n = f.len();
|
||||
if n != s.len() {
|
||||
if f.len() != s.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"fast and slow must have the same length",
|
||||
));
|
||||
}
|
||||
let mut out = vec![0i8; n];
|
||||
if n < 2 {
|
||||
return Ok(out.into_pyarray(py));
|
||||
}
|
||||
for i in 1..n {
|
||||
let fp = f[i - 1];
|
||||
let fc = f[i];
|
||||
let sp = s[i - 1];
|
||||
let sc = s[i];
|
||||
if fp.is_nan() || fc.is_nan() || sp.is_nan() || sc.is_nan() {
|
||||
continue;
|
||||
}
|
||||
// Bullish: fast was below slow, now above
|
||||
if fp <= sp && fc > sc {
|
||||
out[i] = 1;
|
||||
}
|
||||
// Bearish: fast was above slow, now below
|
||||
else if fp >= sp && fc < sc {
|
||||
out[i] = -1;
|
||||
}
|
||||
}
|
||||
Ok(out.into_pyarray(py))
|
||||
let result = ferro_ta_core::alerts::check_cross(f, s);
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// collect_alert_bars
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Collect bar indices where *mask* is non-zero (i.e. condition fired).
|
||||
///
|
||||
/// Parameters
|
||||
/// ----------
|
||||
/// mask : 1-D int8 array (output of ``check_threshold`` or ``check_cross``)
|
||||
///
|
||||
/// Returns
|
||||
/// -------
|
||||
/// 1-D int64 array — indices of fired bars (ascending order)
|
||||
/// Collect bar indices where *mask* is non-zero.
|
||||
#[pyfunction]
|
||||
pub fn collect_alert_bars<'py>(
|
||||
py: Python<'py>,
|
||||
mask: PyReadonlyArray1<'py, i8>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i64>>> {
|
||||
let m = mask.as_slice()?;
|
||||
let indices: Vec<i64> = m
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, &v)| v != 0)
|
||||
.map(|(i, _)| i as i64)
|
||||
.collect();
|
||||
Ok(indices.into_pyarray(py))
|
||||
let result = ferro_ta_core::alerts::collect_alert_bars(m);
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Register
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_function(wrap_pyfunction!(check_threshold, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(check_cross, m)?)?;
|
||||
|
||||
Reference in New Issue
Block a user