2026-03-30 12:45:52 +05:30
|
|
|
//! Alerts — condition evaluation helpers (thin PyO3 wrapper over ferro_ta_core::alerts).
|
2026-03-23 23:34:28 +05:30
|
|
|
|
|
|
|
|
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
|
|
|
|
use pyo3::exceptions::PyValueError;
|
|
|
|
|
use pyo3::prelude::*;
|
|
|
|
|
|
|
|
|
|
/// Fire an alert when *series* crosses a threshold level.
|
|
|
|
|
///
|
|
|
|
|
/// Parameters
|
|
|
|
|
/// ----------
|
2026-03-30 12:45:52 +05:30
|
|
|
/// series : 1-D float64 array
|
2026-03-23 23:34:28 +05:30
|
|
|
/// level : float — threshold value
|
2026-03-30 12:45:52 +05:30
|
|
|
/// direction : int — ``1`` (cross above) or ``-1`` (cross below)
|
2026-03-23 23:34:28 +05:30
|
|
|
///
|
|
|
|
|
/// Returns
|
|
|
|
|
/// -------
|
2026-03-30 12:45:52 +05:30
|
|
|
/// 1-D int8 array — 1 at crossing bars, 0 elsewhere.
|
2026-03-23 23:34:28 +05:30
|
|
|
#[pyfunction]
|
|
|
|
|
pub fn check_threshold<'py>(
|
|
|
|
|
py: Python<'py>,
|
|
|
|
|
series: PyReadonlyArray1<'py, f64>,
|
|
|
|
|
level: f64,
|
|
|
|
|
direction: i32,
|
|
|
|
|
) -> PyResult<Bound<'py, PyArray1<i8>>> {
|
|
|
|
|
if direction != 1 && direction != -1 {
|
|
|
|
|
return Err(PyValueError::new_err(
|
|
|
|
|
"direction must be 1 (cross above) or -1 (cross below)",
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
let s = series.as_slice()?;
|
2026-03-30 12:45:52 +05:30
|
|
|
let result = ferro_ta_core::alerts::check_threshold(s, level, direction);
|
|
|
|
|
Ok(result.into_pyarray(py))
|
2026-03-23 23:34:28 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Detect cross-over / cross-under events between two series.
|
|
|
|
|
///
|
|
|
|
|
/// Returns
|
|
|
|
|
/// -------
|
2026-03-30 12:45:52 +05:30
|
|
|
/// 1-D int8 array: ``1`` = bullish, ``-1`` = bearish, ``0`` = none.
|
2026-03-23 23:34:28 +05:30
|
|
|
#[pyfunction]
|
|
|
|
|
pub fn check_cross<'py>(
|
|
|
|
|
py: Python<'py>,
|
|
|
|
|
fast: PyReadonlyArray1<'py, f64>,
|
|
|
|
|
slow: PyReadonlyArray1<'py, f64>,
|
|
|
|
|
) -> PyResult<Bound<'py, PyArray1<i8>>> {
|
|
|
|
|
let f = fast.as_slice()?;
|
|
|
|
|
let s = slow.as_slice()?;
|
2026-03-30 12:45:52 +05:30
|
|
|
if f.len() != s.len() {
|
2026-03-23 23:34:28 +05:30
|
|
|
return Err(PyValueError::new_err(
|
|
|
|
|
"fast and slow must have the same length",
|
|
|
|
|
));
|
|
|
|
|
}
|
2026-03-30 12:45:52 +05:30
|
|
|
let result = ferro_ta_core::alerts::check_cross(f, s);
|
|
|
|
|
Ok(result.into_pyarray(py))
|
2026-03-23 23:34:28 +05:30
|
|
|
}
|
|
|
|
|
|
2026-03-30 12:45:52 +05:30
|
|
|
/// Collect bar indices where *mask* is non-zero.
|
2026-03-23 23:34:28 +05:30
|
|
|
#[pyfunction]
|
|
|
|
|
pub fn collect_alert_bars<'py>(
|
|
|
|
|
py: Python<'py>,
|
|
|
|
|
mask: PyReadonlyArray1<'py, i8>,
|
|
|
|
|
) -> PyResult<Bound<'py, PyArray1<i64>>> {
|
|
|
|
|
let m = mask.as_slice()?;
|
2026-03-30 12:45:52 +05:30
|
|
|
let result = ferro_ta_core::alerts::collect_alert_bars(m);
|
|
|
|
|
Ok(result.into_pyarray(py))
|
2026-03-23 23:34:28 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
|
|
|
m.add_function(wrap_pyfunction!(check_threshold, m)?)?;
|
|
|
|
|
m.add_function(wrap_pyfunction!(check_cross, m)?)?;
|
|
|
|
|
m.add_function(wrap_pyfunction!(collect_alert_bars, m)?)?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|