pub fn regime_adx(adx: &[f64], threshold: f64) -> Vec<i8> {
adx.iter()
.map(|&v| {
if v.is_nan() {
-1i8
} else if v > threshold {
1i8
} else {
0i8
}
})
.collect()
}
pub fn regime_combined(
adx: &[f64],
atr: &[f64],
close: &[f64],
adx_threshold: f64,
atr_pct_threshold: f64,
) -> Vec<i8> {
let n = adx.len();
(0..n)
.map(|i| {
let av = adx[i];
let rv = atr[i];
let cv = close[i];
if av.is_nan() || rv.is_nan() || cv.is_nan() || cv == 0.0 {
-1i8
} else if av > adx_threshold && (rv / cv) > atr_pct_threshold {
1i8
} else {
0i8
}
})
.collect()
}
pub fn detect_breaks_cusum(series: &[f64], window: usize, threshold: f64, slack: f64) -> Vec<i8> {
let n = series.len();
let mut out = vec![0i8; n];
if n < window || window < 2 {
return out;
}
let mut cusum_pos = 0.0_f64;
let mut cusum_neg = 0.0_f64;
for i in window..n {
let slice = &series[(i - window)..i];
let mean: f64 = slice.iter().sum::<f64>() / window as f64;
let var: f64 =
slice.iter().map(|&v| (v - mean) * (v - mean)).sum::<f64>() / (window - 1) as f64;
let std = var.sqrt();
if std == 0.0 || std.is_nan() || series[i].is_nan() {
continue;
}
let z = (series[i] - mean) / std;
cusum_pos = (cusum_pos + z - slack).max(0.0);
cusum_neg = (cusum_neg - z - slack).max(0.0);
if cusum_pos > threshold || cusum_neg > threshold {
out[i] = 1;
cusum_pos = 0.0;
cusum_neg = 0.0;
}
}
out
}
pub fn rolling_variance_break(
series: &[f64],
short_window: usize,
long_window: usize,
threshold: f64,
) -> Vec<i8> {
let n = series.len();
let mut out = vec![0i8; n];
if n < long_window || short_window < 2 || long_window <= short_window {
return out;
}
let variance = |slice: &[f64]| -> f64 {
let k = slice.len();
let mean: f64 = slice.iter().sum::<f64>() / k as f64;
slice.iter().map(|&v| (v - mean) * (v - mean)).sum::<f64>() / (k - 1) as f64
};
for i in long_window..n {
let long_slice = &series[(i - long_window)..i];
let short_slice = &series[(i - short_window)..i];
let long_var = variance(long_slice);
let short_var = variance(short_slice);
if long_var == 0.0 || long_var.is_nan() || short_var.is_nan() {
continue;
}
if short_var / long_var > threshold {
out[i] = 1;
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_regime_adx_basic() {
let adx = vec![f64::NAN, 20.0, 30.0, 10.0, 50.0];
let result = regime_adx(&adx, 25.0);
assert_eq!(result, vec![-1, 0, 1, 0, 1]);
}
#[test]
fn test_regime_combined() {
let adx = vec![30.0, 30.0, 10.0];
let atr = vec![1.0, 0.001, 1.0];
let close = vec![100.0, 100.0, 100.0];
let result = regime_combined(&adx, &atr, &close, 25.0, 0.005);
assert_eq!(result[0], 1); assert_eq!(result[1], 0); assert_eq!(result[2], 0); }
#[test]
fn test_detect_breaks_cusum_short_input() {
let series = vec![1.0, 2.0];
let result = detect_breaks_cusum(&series, 5, 3.0, 0.5);
assert!(result.iter().all(|&v| v == 0));
}
#[test]
fn test_rolling_variance_break_short_input() {
let series = vec![1.0, 2.0, 3.0];
let result = rolling_variance_break(&series, 2, 5, 2.0);
assert!(result.iter().all(|&v| v == 0));
}
#[test]
fn test_empty() {
assert!(regime_adx(&[], 25.0).is_empty());
assert!(regime_combined(&[], &[], &[], 25.0, 0.005).is_empty());
assert!(detect_breaks_cusum(&[], 2, 3.0, 0.5).is_empty());
assert!(rolling_variance_break(&[], 2, 5, 2.0).is_empty());
}
}