wickra_core/indicators/
williams_fractals.rs1use std::collections::VecDeque;
4
5use crate::ohlcv::Candle;
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone, Copy, PartialEq)]
15pub struct WilliamsFractalsOutput {
16 pub up: Option<f64>,
19 pub down: Option<f64>,
22}
23
24#[derive(Debug, Clone)]
50pub struct WilliamsFractals {
51 window: VecDeque<(f64, f64)>,
53}
54
55impl Default for WilliamsFractals {
56 fn default() -> Self {
57 Self::new()
58 }
59}
60
61impl WilliamsFractals {
62 pub fn new() -> Self {
65 Self {
66 window: VecDeque::with_capacity(5),
67 }
68 }
69}
70
71impl Indicator for WilliamsFractals {
72 type Input = Candle;
73 type Output = WilliamsFractalsOutput;
74
75 fn update(&mut self, candle: Candle) -> Option<WilliamsFractalsOutput> {
76 if self.window.len() == 5 {
77 self.window.pop_front();
78 }
79 self.window.push_back((candle.high, candle.low));
80 if self.window.len() < 5 {
81 return None;
82 }
83 let (h0, _) = self.window[0];
84 let (h1, _) = self.window[1];
85 let (h2, l2) = self.window[2];
86 let (h3, _) = self.window[3];
87 let (h4, _) = self.window[4];
88 let (_, l0) = self.window[0];
89 let (_, l1) = self.window[1];
90 let (_, l3) = self.window[3];
91 let (_, l4) = self.window[4];
92
93 let up = if h2 > h0 && h2 > h1 && h2 > h3 && h2 > h4 {
94 Some(h2)
95 } else {
96 None
97 };
98 let down = if l2 < l0 && l2 < l1 && l2 < l3 && l2 < l4 {
99 Some(l2)
100 } else {
101 None
102 };
103 Some(WilliamsFractalsOutput { up, down })
104 }
105
106 fn reset(&mut self) {
107 self.window.clear();
108 }
109
110 fn warmup_period(&self) -> usize {
111 5
112 }
113
114 fn is_ready(&self) -> bool {
115 self.window.len() == 5
116 }
117
118 fn name(&self) -> &'static str {
119 "WilliamsFractals"
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use crate::traits::BatchExt;
127
128 fn c(h: f64, l: f64, ts: i64) -> Candle {
129 Candle::new(l, h, l, l, 1.0, ts).unwrap()
130 }
131
132 #[test]
133 fn isolated_peak_is_detected_as_up_fractal() {
134 let mut wf = WilliamsFractals::new();
135 let highs = [1.0, 2.0, 5.0, 2.0, 1.0];
137 let mut last = None;
138 for (i, &h) in highs.iter().enumerate() {
139 last = wf.update(c(h, h - 0.5, i64::try_from(i).unwrap()));
140 }
141 let o = last.expect("fifth bar emits");
142 assert_eq!(o.up, Some(5.0));
143 assert_eq!(o.down, None);
144 }
145
146 #[test]
147 fn isolated_trough_is_detected_as_down_fractal() {
148 let mut wf = WilliamsFractals::new();
149 let lows = [5.0, 4.0, 1.0, 4.0, 5.0];
151 let mut last = None;
152 for (i, &l) in lows.iter().enumerate() {
153 last = wf.update(c(l + 0.5, l, i64::try_from(i).unwrap()));
154 }
155 let o = last.expect("fifth bar emits");
156 assert_eq!(o.down, Some(1.0));
157 assert_eq!(o.up, None);
158 }
159
160 #[test]
161 fn monotonic_series_yields_no_fractals() {
162 let mut wf = WilliamsFractals::new();
163 let mut emitted = 0_usize;
164 for i in 0..10 {
165 let h = f64::from(i) + 2.0;
166 let l = f64::from(i);
167 if let Some(o) = wf.update(c(h, l, i64::from(i))) {
168 emitted += 1;
169 assert_eq!(o.up, None);
170 assert_eq!(o.down, None);
171 }
172 }
173 assert!(emitted >= 6);
174 }
175
176 #[test]
177 fn equal_neighbour_is_not_a_fractal() {
178 let mut wf = WilliamsFractals::new();
180 let highs = [1.0, 5.0, 5.0, 2.0, 1.0];
181 let mut last = None;
182 for (i, &h) in highs.iter().enumerate() {
183 last = wf.update(c(h, h - 0.5, i64::try_from(i).unwrap()));
184 }
185 let o = last.unwrap();
186 assert_eq!(o.up, None);
187 }
188
189 #[test]
190 fn first_four_bars_return_none() {
191 let mut wf = WilliamsFractals::new();
192 for i in 0..4 {
193 assert_eq!(wf.update(c(10.0, 9.0, i)), None);
194 }
195 assert!(!wf.is_ready());
196 }
197
198 #[test]
199 fn warmup_period_is_five() {
200 assert_eq!(WilliamsFractals::new().warmup_period(), 5);
201 }
202
203 #[test]
204 fn reset_clears_state() {
205 let mut wf = WilliamsFractals::new();
206 for i in 0..5 {
207 wf.update(c(10.0, 9.0, i));
208 }
209 assert!(wf.is_ready());
210 wf.reset();
211 assert!(!wf.is_ready());
212 assert_eq!(wf.update(c(10.0, 9.0, 0)), None);
213 }
214
215 #[test]
216 fn batch_equals_streaming() {
217 let candles: Vec<Candle> = (0..40)
218 .map(|i| c(f64::from(i) + 2.0, f64::from(i), i64::from(i)))
219 .collect();
220 let mut a = WilliamsFractals::new();
221 let mut b = WilliamsFractals::new();
222 assert_eq!(
223 a.batch(&candles),
224 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
225 );
226 }
227
228 #[test]
229 fn accessors_and_metadata() {
230 let wf = WilliamsFractals::new();
231 assert_eq!(wf.warmup_period(), 5);
232 assert_eq!(wf.name(), "WilliamsFractals");
233 }
234
235 #[test]
236 fn default_matches_new() {
237 let a = WilliamsFractals::new();
238 let b = WilliamsFractals::default();
239 assert_eq!(a.is_ready(), b.is_ready());
240 assert_eq!(a.warmup_period(), b.warmup_period());
241 }
242}