#[cfg(feature = "serde")]
use serde::Serialize;
use super::{efficiency_ratio, true_range};
use crate::indicator::adx::Adx;
use crate::Bar;
use crate::Indicator;
const ADX_TREND: f64 = 25.0;
const CHOP_TREND: f64 = 38.2; const EFFICIENCY_TREND: f64 = 0.5;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum RegimeState {
Trending,
Ranging,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct RegimeReading {
pub state: RegimeState,
pub adx: f64,
pub choppiness: f64,
pub efficiency: f64,
pub trend_votes: u8,
}
pub fn classify_regime(bars: &[Bar], adx_len: usize, window: usize) -> Option<RegimeReading> {
if window < 2 || adx_len < 1 {
return None;
}
let adx = latest_adx(bars, adx_len)?;
let choppiness = choppiness(bars, window)?;
let efficiency = efficiency_ratio(bars, window)?;
let trend_votes = (adx >= ADX_TREND) as u8
+ (choppiness <= CHOP_TREND) as u8
+ (efficiency >= EFFICIENCY_TREND) as u8;
let state = if trend_votes >= 2 {
RegimeState::Trending
} else {
RegimeState::Ranging
};
Some(RegimeReading {
state,
adx,
choppiness,
efficiency,
trend_votes,
})
}
fn latest_adx(bars: &[Bar], adx_len: usize) -> Option<f64> {
let mut adx = Adx::new(adx_len, adx_len, 3, 20.0);
let mut last = None;
for bar in bars {
if let Some(out) = adx.on_bar(bar) {
last = Some(out.value);
}
}
last
}
fn choppiness(bars: &[Bar], n: usize) -> Option<f64> {
if bars.len() < n + 1 {
return None;
}
let window = &bars[bars.len() - n..];
let prev_close = bars[bars.len() - n - 1].close;
let mut tr_sum = 0.0;
let mut prev = Some(prev_close);
let mut highest = f64::NEG_INFINITY;
let mut lowest = f64::INFINITY;
for bar in window {
tr_sum += true_range(bar, prev);
highest = highest.max(bar.high);
lowest = lowest.min(bar.low);
prev = Some(bar.close);
}
let range = highest - lowest;
if range <= 0.0 || tr_sum <= 0.0 {
return None;
}
Some(100.0 * (tr_sum / range).log10() / (n as f64).log10())
}
#[cfg(test)]
mod tests {
use super::*;
fn bar(o: f64, h: f64, l: f64, c: f64) -> Bar {
Bar {
timestamp: 0,
open: o,
high: h,
low: l,
close: c,
volume: 0.0,
}
}
#[test]
fn straight_trend_reads_as_trending() {
let bars: Vec<Bar> = (0..60)
.map(|i| {
let c = 100.0 + i as f64;
bar(c - 0.5, c + 0.2, c - 0.7, c)
})
.collect();
let r = classify_regime(&bars, 14, 20).expect("enough bars");
assert!((r.efficiency - 1.0).abs() < 1e-9, "ramp ER should be 1");
assert_eq!(r.state, RegimeState::Trending);
assert!(r.trend_votes >= 2);
}
#[test]
fn oscillation_reads_as_ranging() {
let bars: Vec<Bar> = (0..60)
.map(|i| {
let c = if i % 2 == 0 { 100.0 } else { 101.0 };
bar(c, c + 0.5, c - 0.5, c)
})
.collect();
let r = classify_regime(&bars, 14, 20).expect("enough bars");
assert!(
r.efficiency < 0.3,
"zig-zag ER should be low: {}",
r.efficiency
);
assert_eq!(r.state, RegimeState::Ranging);
}
#[test]
fn insufficient_bars_is_none() {
let bars: Vec<Bar> = (0..5).map(|_| bar(100.0, 101.0, 99.0, 100.0)).collect();
assert!(classify_regime(&bars, 14, 20).is_none());
}
#[test]
fn efficiency_ratio_half_on_one_retrace() {
let bars = vec![
bar(100.0, 100.0, 100.0, 100.0),
bar(110.0, 110.0, 110.0, 110.0),
bar(105.0, 105.0, 105.0, 105.0),
];
let er = efficiency_ratio(&bars, 2).unwrap();
assert!((er - (5.0 / 15.0)).abs() < 1e-9);
}
}