quantwave_core/indicators/
rodc.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::smoothing::SMA;
3use crate::traits::Next;
4use crate::utils::RingBuffer as 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: &[
11 "zigzag",
12 "whipsaw",
13 "momentum",
14 "volatility",
15 "directional change",
16 ],
17 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",
18 params: &[
19 ParamDef {
20 name: "window_size",
21 default: "30",
22 description: "Lookback window for zigzag calculation",
23 },
24 ParamDef {
25 name: "threshold",
26 default: "0.0015",
27 description: "Zigzag reversal threshold (absolute price change)",
28 },
29 ParamDef {
30 name: "smooth_period",
31 default: "3",
32 description: "Smoothing period for the resulting rate",
33 },
34 ],
35 formula_source: "TASC March 2024",
36 formula_latex: r#"
37\[
38RODC = SMA(100 \times \frac{NumUD}{WindowSize}, SmoothPeriod)
39\]
40"#,
41 gold_standard_file: "rodc_30_15_3.json",
42 category: "Volatility",
43};
44
45#[derive(Debug, Clone)]
49pub struct RODC {
50 window_size: usize,
51 threshold: f64,
52 sma: SMA,
53 price_window: VecDeque<f64>,
54}
55
56impl RODC {
57 pub fn new(window_size: usize, threshold: f64, smooth_period: usize) -> Self {
58 Self {
59 window_size,
60 threshold,
61 sma: SMA::new(smooth_period),
62 price_window: VecDeque::with_capacity(window_size + 1),
63 }
64 }
65}
66
67impl Next<f64> for RODC {
68 type Output = f64;
69
70 fn next(&mut self, price: f64) -> Self::Output {
71 self.price_window.push_back(price);
72
73 if self.price_window.len() <= self.window_size {
74 return 0.0;
80 }
81
82 if self.price_window.len() > self.window_size + 1 {
83 self.price_window.pop_front();
84 }
85
86 let mut n_ud = 1;
88 let mut mode_up = true;
89
90 let mut x_ext = *self.price_window.front().unwrap();
92
93 for i in 1..self.price_window.len() {
95 let x_cls = self.price_window[i];
96
97 if !mode_up {
98 if x_ext > x_cls {
99 x_ext = x_cls;
101 } else if x_cls - x_ext >= self.threshold {
102 mode_up = true;
104 n_ud += 1;
105 x_ext = x_cls;
106 }
107 } else {
108 if x_ext < x_cls {
109 x_ext = x_cls;
111 } else if x_ext - x_cls >= self.threshold {
112 mode_up = false;
114 n_ud += 1;
115 x_ext = x_cls;
116 }
117 }
118 }
119
120 let raw_rodc = 100.0 * n_ud as f64 / self.window_size as f64;
121 self.sma.next(raw_rodc)
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn test_rodc_basic() {
131 let mut rodc = RODC::new(4, 2.0, 1);
133
134 assert_eq!(rodc.next(10.0), 0.0);
136 assert_eq!(rodc.next(11.0), 0.0);
137 assert_eq!(rodc.next(12.0), 0.0);
138 assert_eq!(rodc.next(13.0), 0.0);
139
140 assert_eq!(rodc.next(14.0), 25.0);
148
149 assert_eq!(rodc.next(12.0), 50.0);
157
158 assert_eq!(rodc.next(15.0), 75.0);
166 }
167}