1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
use crate::indicators::smoothing::SMA;
use crate::traits::Next;
use std::collections::VecDeque;
pub const METADATA: IndicatorMetadata = IndicatorMetadata {
name: "Rate of Directional Change",
description: "Measures the frequency of directional changes (zigzag flips) within a moving window to identify whipsaw market conditions.",
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.",
keywords: &["zigzag", "whipsaw", "momentum", "volatility", "directional change"],
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",
params: &[
ParamDef {
name: "window_size",
default: "30",
description: "Lookback window for zigzag calculation",
},
ParamDef {
name: "threshold",
default: "0.0015",
description: "Zigzag reversal threshold (absolute price change)",
},
ParamDef {
name: "smooth_period",
default: "3",
description: "Smoothing period for the resulting rate",
},
],
formula_source: "TASC March 2024",
formula_latex: r#"
\[
RODC = SMA(100 \times \frac{NumUD}{WindowSize}, SmoothPeriod)
\]
"#,
gold_standard_file: "rodc_30_15_3.json",
category: "Volatility",
};
/// Rate of Directional Change (RODC)
///
/// Measures the frequency of directional changes within a moving window.
#[derive(Debug, Clone)]
pub struct RODC {
window_size: usize,
threshold: f64,
sma: SMA,
price_window: VecDeque<f64>,
}
impl RODC {
pub fn new(window_size: usize, threshold: f64, smooth_period: usize) -> Self {
Self {
window_size,
threshold,
sma: SMA::new(smooth_period),
price_window: VecDeque::with_capacity(window_size + 1),
}
}
}
impl Next<f64> for RODC {
type Output = f64;
fn next(&mut self, price: f64) -> Self::Output {
self.price_window.push_back(price);
if self.price_window.len() <= self.window_size {
// Need at least window_size + 1 points to have window_size gaps/segments
// However, the original code starts from Close[BkData].
// If we have less data, we can either return 0 or calculate on what we have.
// TradeStation code is usually executed on a chart where history is available.
// For streaming, we'll return 0 until we have enough data.
return 0.0;
}
if self.price_window.len() > self.window_size + 1 {
self.price_window.pop_front();
}
// Calculate zigzag flips within the current window
let mut n_ud = 1;
let mut mode_up = true;
// Start from the oldest price in the window
let mut x_ext = *self.price_window.front().unwrap();
// Iterate forward through the window (excluding the very first point which is x_ext)
for i in 1..self.price_window.len() {
let x_cls = self.price_window[i];
if !mode_up {
if x_ext > x_cls {
// Still mode down, update extreme low
x_ext = x_cls;
} else if x_cls - x_ext >= self.threshold {
// Reversal to mode up
mode_up = true;
n_ud += 1;
x_ext = x_cls;
}
} else {
if x_ext < x_cls {
// Still mode up, update extreme high
x_ext = x_cls;
} else if x_ext - x_cls >= self.threshold {
// Reversal to mode down
mode_up = false;
n_ud += 1;
x_ext = x_cls;
}
}
}
let raw_rodc = 100.0 * n_ud as f64 / self.window_size as f64;
self.sma.next(raw_rodc)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rodc_basic() {
// window_size = 4, threshold = 2.0, smooth = 1 (no smoothing)
let mut rodc = RODC::new(4, 2.0, 1);
// Initializing... need 5 points
assert_eq!(rodc.next(10.0), 0.0);
assert_eq!(rodc.next(11.0), 0.0);
assert_eq!(rodc.next(12.0), 0.0);
assert_eq!(rodc.next(13.0), 0.0);
// Bar 5: [10, 11, 12, 13, 14]
// Window starts at 10. n_ud = 1. mode_up = true. x_ext = 10.
// 11 > 10, x_ext = 11.
// 12 > 11, x_ext = 12.
// 13 > 12, x_ext = 13.
// 14 > 13, x_ext = 14.
// Result: 100 * 1 / 4 = 25.0
assert_eq!(rodc.next(14.0), 25.0);
// Bar 6: [11, 12, 13, 14, 12]
// x_ext = 11. mode_up = true.
// 12 > 11, x_ext = 12.
// 13 > 12, x_ext = 13.
// 14 > 13, x_ext = 14.
// 12: 14 - 12 = 2.0 >= threshold. Flip! mode_up = false. n_ud = 2. x_ext = 12.
// Result: 100 * 2 / 4 = 50.0
assert_eq!(rodc.next(12.0), 50.0);
// Bar 7: [12, 13, 14, 12, 15]
// x_ext = 12. mode_up = true.
// 13 > 12, x_ext = 13.
// 14 > 13, x_ext = 14.
// 12: flip. n_ud = 2. x_ext = 12. mode_up = false.
// 15: 15 - 12 = 3.0 >= threshold. Flip! n_ud = 3. x_ext = 15. mode_up = true.
// Result: 100 * 3 / 4 = 75.0
assert_eq!(rodc.next(15.0), 75.0);
}
}