quantwave_core/indicators/
rodc.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::smoothing::SMA;
3use crate::traits::Next;
4use std::collections::VecDeque;
5
6pub const METADATA: IndicatorMetadata = IndicatorMetadata {
7 name: "Rate of Directional Change",
8 description: "Measures the frequency of directional changes (zigzag flips) within a moving window to identify whipsaw market conditions.",
9 usage: "Use to filter out false signals in trend-following strategies. High RODC values indicate a whipsaw environment, while low values suggest a trending market.",
10 keywords: &["zigzag", "whipsaw", "momentum", "volatility", "directional change"],
11 ehlers_summary: "RODC tracks the number of alternating up and down zigzag segments within a fixed window. By normalizing this count and smoothing it, the indicator provides a measure of how 'noisy' the price action is. It declines in trending environments and increases during whipsaws. — Richard Poster, TASC March 2024",
12 params: &[
13 ParamDef {
14 name: "window_size",
15 default: "30",
16 description: "Lookback window for zigzag calculation",
17 },
18 ParamDef {
19 name: "threshold",
20 default: "0.0015",
21 description: "Zigzag reversal threshold (absolute price change)",
22 },
23 ParamDef {
24 name: "smooth_period",
25 default: "3",
26 description: "Smoothing period for the resulting rate",
27 },
28 ],
29 formula_source: "TASC March 2024",
30 formula_latex: r#"
31\[
32RODC = SMA(100 \times \frac{NumUD}{WindowSize}, SmoothPeriod)
33\]
34"#,
35 gold_standard_file: "rodc_30_15_3.json",
36 category: "Volatility",
37};
38
39#[derive(Debug, Clone)]
43pub struct RODC {
44 window_size: usize,
45 threshold: f64,
46 sma: SMA,
47 price_window: VecDeque<f64>,
48}
49
50impl RODC {
51 pub fn new(window_size: usize, threshold: f64, smooth_period: usize) -> Self {
52 Self {
53 window_size,
54 threshold,
55 sma: SMA::new(smooth_period),
56 price_window: VecDeque::with_capacity(window_size + 1),
57 }
58 }
59}
60
61impl Next<f64> for RODC {
62 type Output = f64;
63
64 fn next(&mut self, price: f64) -> Self::Output {
65 self.price_window.push_back(price);
66
67 if self.price_window.len() <= self.window_size {
68 return 0.0;
74 }
75
76 if self.price_window.len() > self.window_size + 1 {
77 self.price_window.pop_front();
78 }
79
80 let mut n_ud = 1;
82 let mut mode_up = true;
83
84 let mut x_ext = *self.price_window.front().unwrap();
86
87 for i in 1..self.price_window.len() {
89 let x_cls = self.price_window[i];
90
91 if !mode_up {
92 if x_ext > x_cls {
93 x_ext = x_cls;
95 } else if x_cls - x_ext >= self.threshold {
96 mode_up = true;
98 n_ud += 1;
99 x_ext = x_cls;
100 }
101 } else {
102 if x_ext < x_cls {
103 x_ext = x_cls;
105 } else if x_ext - x_cls >= self.threshold {
106 mode_up = false;
108 n_ud += 1;
109 x_ext = x_cls;
110 }
111 }
112 }
113
114 let raw_rodc = 100.0 * n_ud as f64 / self.window_size as f64;
115 self.sma.next(raw_rodc)
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn test_rodc_basic() {
125 let mut rodc = RODC::new(4, 2.0, 1);
127
128 assert_eq!(rodc.next(10.0), 0.0);
130 assert_eq!(rodc.next(11.0), 0.0);
131 assert_eq!(rodc.next(12.0), 0.0);
132 assert_eq!(rodc.next(13.0), 0.0);
133
134 assert_eq!(rodc.next(14.0), 25.0);
142
143 assert_eq!(rodc.next(12.0), 50.0);
151
152 assert_eq!(rodc.next(15.0), 75.0);
160 }
161}