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