Skip to main content

kestrel_chartkit/indicator/
williams_r.rs

1use std::collections::{HashMap, VecDeque};
2
3use crate::model::Bar;
4
5use super::divergence::SlopeDivergence;
6use super::smoothing::{crossed_over, crossed_under, Ema, ExtremeWindow};
7use super::{Indicator, IndicatorAlert, IndicatorOutput};
8
9pub struct WilliamsR {
10    wpr_len: usize,
11    mid_line: f64,
12    oversold: f64,
13    overbought: f64,
14    require_extreme_zone: bool,
15    ctx_len: usize,
16
17    hl_window: VecDeque<(f64, f64)>,
18    avg: Ema,
19    signal_avg: Ema,
20    extreme_window: ExtremeWindow,
21    prev_wpr_line: Option<f64>,
22    prev_signal: Option<f64>,
23    bars_seen: usize,
24
25    ctx_hl_window: VecDeque<(f64, f64)>,
26    ctx_avg: Ema,
27    divergence: SlopeDivergence,
28
29    alerts: WilliamsRAlerts,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Default)]
33pub struct WilliamsRAlerts {
34    pub bull_extreme: bool,
35    pub bear_extreme: bool,
36    pub bull_mid_cross: bool,
37    pub bear_mid_cross: bool,
38    pub bull_divergence: bool,
39    pub bear_divergence: bool,
40    pub extreme_strength: f64,
41    pub divergence_strength: f64,
42}
43
44impl WilliamsR {
45    #[allow(clippy::too_many_arguments)]
46    pub fn new(
47        wpr_len: usize,
48        avg_len: usize,
49        sig_len: usize,
50        mid_line: f64,
51        overbought: f64,
52        oversold: f64,
53        lookback_extreme: usize,
54        require_extreme_zone: bool,
55        ctx_len: usize,
56        div_len: usize,
57        div_min: f64,
58    ) -> Self {
59        Self {
60            wpr_len,
61            mid_line,
62            oversold,
63            overbought,
64            require_extreme_zone,
65            ctx_len,
66            hl_window: VecDeque::with_capacity(wpr_len),
67            avg: Ema::new(avg_len),
68            signal_avg: Ema::new(sig_len),
69            extreme_window: ExtremeWindow::new(lookback_extreme),
70            prev_wpr_line: None,
71            prev_signal: None,
72            bars_seen: 0,
73            ctx_hl_window: VecDeque::with_capacity(ctx_len),
74            ctx_avg: Ema::new(avg_len),
75            divergence: SlopeDivergence::new(div_len, div_min),
76            alerts: WilliamsRAlerts::default(),
77        }
78    }
79
80    pub fn with_defaults() -> Self {
81        Self::new(14, 3, 3, 50.0, 80.0, 20.0, 5, true, 50, 4, 10.0)
82    }
83}
84
85impl Indicator for WilliamsR {
86    fn name(&self) -> &str {
87        "williams_r"
88    }
89
90    fn warmup_period(&self) -> usize {
91        self.wpr_len.max(self.ctx_len)
92    }
93
94    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
95        self.alerts = WilliamsRAlerts::default();
96        self.bars_seen += 1;
97
98        if self.ctx_hl_window.len() == self.ctx_len {
99            self.ctx_hl_window.pop_front();
100        }
101        self.ctx_hl_window.push_back((bar.high, bar.low));
102        let ctx_line = if self.ctx_hl_window.len() == self.ctx_len {
103            let ctx_highest_high = self
104                .ctx_hl_window
105                .iter()
106                .map(|(h, _)| *h)
107                .fold(f64::NEG_INFINITY, f64::max);
108            let ctx_lowest_low = self
109                .ctx_hl_window
110                .iter()
111                .map(|(_, l)| *l)
112                .fold(f64::INFINITY, f64::min);
113            let ctx_range = ctx_highest_high - ctx_lowest_low;
114            let ctx_raw = if ctx_range != 0.0 {
115                100.0 * (bar.close - ctx_lowest_low) / ctx_range
116            } else {
117                50.0
118            };
119            Some(self.ctx_avg.update(ctx_raw))
120        } else {
121            None
122        };
123
124        if self.hl_window.len() == self.wpr_len {
125            self.hl_window.pop_front();
126        }
127        self.hl_window.push_back((bar.high, bar.low));
128        if self.hl_window.len() < self.wpr_len {
129            return None;
130        }
131
132        let highest_high = self
133            .hl_window
134            .iter()
135            .map(|(h, _)| *h)
136            .fold(f64::NEG_INFINITY, f64::max);
137        let lowest_low = self
138            .hl_window
139            .iter()
140            .map(|(_, l)| *l)
141            .fold(f64::INFINITY, f64::min);
142        let range = highest_high - lowest_low;
143        let wpr_raw = if range != 0.0 {
144            100.0 * (bar.close - lowest_low) / range
145        } else {
146            50.0
147        };
148
149        let wpr_line = self.avg.update(wpr_raw);
150        let signal = self.signal_avg.update(wpr_line);
151
152        let extreme = self.extreme_window.push(wpr_line);
153        let was_oversold = extreme
154            .map(|(low, _)| low <= self.oversold)
155            .unwrap_or(false);
156        let was_overbought = extreme
157            .map(|(_, high)| high >= self.overbought)
158            .unwrap_or(false);
159
160        if let (Some(prev_wpr), Some(prev_sig)) = (self.prev_wpr_line, self.prev_signal) {
161            let bull_cross = crossed_over(prev_wpr, prev_sig, wpr_line, signal);
162            let bear_cross = crossed_under(prev_wpr, prev_sig, wpr_line, signal);
163            self.alerts.bull_extreme = bull_cross && (!self.require_extreme_zone || was_oversold);
164            self.alerts.bear_extreme = bear_cross && (!self.require_extreme_zone || was_overbought);
165            self.alerts.bull_mid_cross =
166                crossed_over(prev_wpr, self.mid_line, wpr_line, self.mid_line);
167            self.alerts.bear_mid_cross =
168                crossed_under(prev_wpr, self.mid_line, wpr_line, self.mid_line);
169
170            self.alerts.extreme_strength = if let Some((low, high)) = extreme {
171                if self.alerts.bull_extreme {
172                    ((self.oversold - low) / self.oversold.abs()).clamp(0.0, 1.0)
173                } else if self.alerts.bear_extreme {
174                    ((high - self.overbought) / self.overbought.abs()).clamp(0.0, 1.0)
175                } else {
176                    0.0
177                }
178            } else {
179                0.0
180            };
181        }
182        self.prev_wpr_line = Some(wpr_line);
183        self.prev_signal = Some(signal);
184
185        let mut extra = HashMap::new();
186        extra.insert("signal".to_string(), signal);
187        if let Some(ctx_line) = ctx_line {
188            let div = self.divergence.update(wpr_line, ctx_line);
189            self.alerts.bull_divergence = div.bull;
190            self.alerts.bear_divergence = div.bear;
191            self.alerts.divergence_strength = if div.bull || div.bear {
192                ((div.fast_dir.abs() - self.divergence.div_min()) / self.divergence.div_min())
193                    .clamp(0.0, 1.0)
194            } else {
195                0.0
196            };
197            extra.insert("ctx".to_string(), ctx_line);
198        }
199
200        Some(IndicatorOutput::with_extra(wpr_line, extra))
201    }
202
203    fn reset(&mut self) {
204        self.hl_window.clear();
205        self.avg.reset();
206        self.signal_avg.reset();
207        self.extreme_window.reset();
208        self.prev_wpr_line = None;
209        self.prev_signal = None;
210        self.bars_seen = 0;
211        self.ctx_hl_window.clear();
212        self.ctx_avg.reset();
213        self.divergence.reset();
214        self.alerts = WilliamsRAlerts::default();
215    }
216
217    fn alerts(&self) -> Vec<IndicatorAlert> {
218        let a = self.alerts;
219        let mut out = Vec::new();
220        if a.bull_extreme {
221            out.push(IndicatorAlert {
222                kind: "bull_extreme".to_string(),
223                note: "WPR · BULL CROSS OVERSOLD".to_string(),
224                strength: a.extreme_strength,
225            });
226        }
227        if a.bear_extreme {
228            out.push(IndicatorAlert {
229                kind: "bear_extreme".to_string(),
230                note: "WPR · BEAR CROSS OVERBOUGHT".to_string(),
231                strength: a.extreme_strength,
232            });
233        }
234        if a.bull_mid_cross {
235            out.push(IndicatorAlert {
236                kind: "bull_mid_cross".to_string(),
237                note: "WPR · CROSS ABOVE 50".to_string(),
238                strength: 1.0,
239            });
240        }
241        if a.bear_mid_cross {
242            out.push(IndicatorAlert {
243                kind: "bear_mid_cross".to_string(),
244                note: "WPR · CROSS BELOW 50".to_string(),
245                strength: 1.0,
246            });
247        }
248        if a.bull_divergence {
249            out.push(IndicatorAlert {
250                kind: "bull_divergence".to_string(),
251                note: "WILLIAMS %R · BULL DIVERGENCE".to_string(),
252                strength: a.divergence_strength,
253            });
254        }
255        if a.bear_divergence {
256            out.push(IndicatorAlert {
257                kind: "bear_divergence".to_string(),
258                note: "WILLIAMS %R · BEAR DIVERGENCE".to_string(),
259                strength: a.divergence_strength,
260            });
261        }
262        out
263    }
264}