Files
ferro-ta/src/regime/mod.rs
T

81 lines
2.7 KiB
Rust
Raw Normal View History

2026-03-30 12:45:52 +05:30
//! Regime detection and structural breaks (thin PyO3 wrapper over ferro_ta_core::regime).
2026-03-23 23:34:28 +05:30
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
2026-03-30 12:45:52 +05:30
use crate::validation;
2026-03-23 23:34:28 +05:30
/// Label each bar as **trend** (1) or **range** (0) based on ADX level.
#[pyfunction]
pub fn regime_adx<'py>(
py: Python<'py>,
adx: PyReadonlyArray1<'py, f64>,
threshold: f64,
) -> PyResult<Bound<'py, PyArray1<i8>>> {
let a = adx.as_slice()?;
2026-03-30 12:45:52 +05:30
let result = ferro_ta_core::regime::regime_adx(a, threshold);
Ok(result.into_pyarray(py))
2026-03-23 23:34:28 +05:30
}
/// Label each bar as trend (1) or range (0) using ADX + ATR-ratio rule.
#[pyfunction]
pub fn regime_combined<'py>(
py: Python<'py>,
adx: PyReadonlyArray1<'py, f64>,
atr: PyReadonlyArray1<'py, f64>,
close: PyReadonlyArray1<'py, f64>,
adx_threshold: f64,
atr_pct_threshold: f64,
) -> PyResult<Bound<'py, PyArray1<i8>>> {
let a = adx.as_slice()?;
let r = atr.as_slice()?;
let c = close.as_slice()?;
let n = a.len();
2026-03-30 12:45:52 +05:30
validation::validate_equal_length(&[(n, "adx"), (r.len(), "atr"), (c.len(), "close")])?;
let result = ferro_ta_core::regime::regime_combined(a, r, c, adx_threshold, atr_pct_threshold);
Ok(result.into_pyarray(py))
2026-03-23 23:34:28 +05:30
}
2026-03-30 12:45:52 +05:30
/// Detect structural breaks using a CUSUM approach.
2026-03-23 23:34:28 +05:30
#[pyfunction]
pub fn detect_breaks_cusum<'py>(
py: Python<'py>,
series: PyReadonlyArray1<'py, f64>,
window: usize,
threshold: f64,
slack: f64,
) -> PyResult<Bound<'py, PyArray1<i8>>> {
2026-03-30 12:45:52 +05:30
validation::validate_timeperiod(window, "window", 2)?;
2026-03-23 23:34:28 +05:30
let s = series.as_slice()?;
2026-03-30 12:45:52 +05:30
let result = ferro_ta_core::regime::detect_breaks_cusum(s, window, threshold, slack);
Ok(result.into_pyarray(py))
2026-03-23 23:34:28 +05:30
}
2026-03-30 12:45:52 +05:30
/// Detect volatility regime breaks using rolling variance ratio.
2026-03-23 23:34:28 +05:30
#[pyfunction]
pub fn rolling_variance_break<'py>(
py: Python<'py>,
series: PyReadonlyArray1<'py, f64>,
short_window: usize,
long_window: usize,
threshold: f64,
) -> PyResult<Bound<'py, PyArray1<i8>>> {
2026-03-30 12:45:52 +05:30
validation::validate_timeperiod(short_window, "short_window", 2)?;
2026-03-23 23:34:28 +05:30
if long_window <= short_window {
2026-04-01 20:12:19 +05:30
return Err(PyValueError::new_err("long_window must be > short_window"));
2026-03-23 23:34:28 +05:30
}
let s = series.as_slice()?;
2026-04-01 20:12:19 +05:30
let result =
ferro_ta_core::regime::rolling_variance_break(s, short_window, long_window, threshold);
2026-03-30 12:45:52 +05:30
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!(regime_adx, m)?)?;
m.add_function(wrap_pyfunction!(regime_combined, m)?)?;
m.add_function(wrap_pyfunction!(detect_breaks_cusum, m)?)?;
m.add_function(wrap_pyfunction!(rolling_variance_break, m)?)?;
Ok(())
}