//! Alerts — condition evaluation helpers. //! //! - `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 mask is non-zero /// Fire an alert when `series` crosses a threshold level. /// /// `direction`: `1` = cross above, `-1` = cross below. /// /// Returns a `Vec` with `1` at crossing bars, `0` elsewhere. /// Element 0 is always 0. pub fn check_threshold(series: &[f64], level: f64, direction: i32) -> Vec { let n = series.len(); let mut out = vec![0i8; n]; if n < 2 { return out; } for i in 1..n { let prev = series[i - 1]; let curr = series[i]; if prev.is_nan() || curr.is_nan() { continue; } if (direction == 1 && prev <= level && curr > level) || (direction == -1 && prev >= level && curr < level) { out[i] = 1; } } out } /// Detect cross-over / cross-under events between two series. /// /// Returns `Vec`: `1` = bullish cross (fast above slow), `-1` = bearish, `0` = none. /// Element 0 is always 0. pub fn check_cross(fast: &[f64], slow: &[f64]) -> Vec { let n = fast.len(); let mut out = vec![0i8; n]; if n < 2 { return out; } for i in 1..n { let fp = fast[i - 1]; let fc = fast[i]; let sp = slow[i - 1]; let sc = slow[i]; if fp.is_nan() || fc.is_nan() || sp.is_nan() || sc.is_nan() { continue; } if fp <= sp && fc > sc { out[i] = 1; } else if fp >= sp && fc < sc { out[i] = -1; } } out } /// Collect bar indices where `mask` is non-zero. pub fn collect_alert_bars(mask: &[i8]) -> Vec { mask.iter() .enumerate() .filter(|(_, &v)| v != 0) .map(|(i, _)| i as i64) .collect() } #[cfg(test)] mod tests { use super::*; #[test] fn test_check_threshold_cross_above() { let series = vec![10.0, 20.0, 30.0, 40.0, 50.0]; let result = check_threshold(&series, 25.0, 1); assert_eq!(result, vec![0, 0, 1, 0, 0]); } #[test] fn test_check_threshold_cross_below() { let series = vec![50.0, 40.0, 30.0, 20.0, 10.0]; let result = check_threshold(&series, 25.0, -1); assert_eq!(result, vec![0, 0, 0, 1, 0]); } #[test] fn test_check_cross_bullish() { let fast = vec![1.0, 2.0, 5.0]; let slow = vec![3.0, 3.0, 3.0]; let result = check_cross(&fast, &slow); assert_eq!(result, vec![0, 0, 1]); } #[test] fn test_check_cross_bearish() { let fast = vec![5.0, 4.0, 1.0]; let slow = vec![3.0, 3.0, 3.0]; let result = check_cross(&fast, &slow); assert_eq!(result, vec![0, 0, -1]); } #[test] fn test_collect_alert_bars() { let mask = vec![0i8, 1, 0, -1, 0, 1]; let result = collect_alert_bars(&mask); assert_eq!(result, vec![1, 3, 5]); } #[test] fn test_empty() { assert_eq!(check_threshold(&[], 0.0, 1), Vec::::new()); assert_eq!(check_cross(&[], &[]), Vec::::new()); assert_eq!(collect_alert_bars(&[]), Vec::::new()); } #[test] fn test_nan_handling() { let series = vec![10.0, f64::NAN, 30.0, 40.0]; let result = check_threshold(&series, 25.0, 1); // NaN bars are skipped assert_eq!(result[1], 0); assert_eq!(result[2], 0); // prev is NaN } }