Skip to main content

quantwave_core/indicators/
rodc.rs

1use 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/// Rate of Directional Change (RODC)
46///
47/// Measures the frequency of directional changes within a moving window.
48#[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            // Need at least window_size + 1 points to have window_size gaps/segments
75            // However, the original code starts from Close[BkData].
76            // If we have less data, we can either return 0 or calculate on what we have.
77            // TradeStation code is usually executed on a chart where history is available.
78            // For streaming, we'll return 0 until we have enough data.
79            return 0.0;
80        }
81
82        if self.price_window.len() > self.window_size + 1 {
83            self.price_window.pop_front();
84        }
85
86        // Calculate zigzag flips within the current window
87        let mut n_ud = 1;
88        let mut mode_up = true;
89
90        // Start from the oldest price in the window
91        let mut x_ext = *self.price_window.front().unwrap();
92
93        // Iterate forward through the window (excluding the very first point which is x_ext)
94        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                    // Still mode down, update extreme low
100                    x_ext = x_cls;
101                } else if x_cls - x_ext >= self.threshold {
102                    // Reversal to mode up
103                    mode_up = true;
104                    n_ud += 1;
105                    x_ext = x_cls;
106                }
107            } else {
108                if x_ext < x_cls {
109                    // Still mode up, update extreme high
110                    x_ext = x_cls;
111                } else if x_ext - x_cls >= self.threshold {
112                    // Reversal to mode down
113                    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        // window_size = 4, threshold = 2.0, smooth = 1 (no smoothing)
132        let mut rodc = RODC::new(4, 2.0, 1);
133
134        // Initializing... need 5 points
135        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        // Bar 5: [10, 11, 12, 13, 14]
141        // Window starts at 10. n_ud = 1. mode_up = true. x_ext = 10.
142        // 11 > 10, x_ext = 11.
143        // 12 > 11, x_ext = 12.
144        // 13 > 12, x_ext = 13.
145        // 14 > 13, x_ext = 14.
146        // Result: 100 * 1 / 4 = 25.0
147        assert_eq!(rodc.next(14.0), 25.0);
148
149        // Bar 6: [11, 12, 13, 14, 12]
150        // x_ext = 11. mode_up = true.
151        // 12 > 11, x_ext = 12.
152        // 13 > 12, x_ext = 13.
153        // 14 > 13, x_ext = 14.
154        // 12: 14 - 12 = 2.0 >= threshold. Flip! mode_up = false. n_ud = 2. x_ext = 12.
155        // Result: 100 * 2 / 4 = 50.0
156        assert_eq!(rodc.next(12.0), 50.0);
157
158        // Bar 7: [12, 13, 14, 12, 15]
159        // x_ext = 12. mode_up = true.
160        // 13 > 12, x_ext = 13.
161        // 14 > 13, x_ext = 14.
162        // 12: flip. n_ud = 2. x_ext = 12. mode_up = false.
163        // 15: 15 - 12 = 3.0 >= threshold. Flip! n_ud = 3. x_ext = 15. mode_up = true.
164        // Result: 100 * 3 / 4 = 75.0
165        assert_eq!(rodc.next(15.0), 75.0);
166    }
167}