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