Skip to main content

quantwave_core/indicators/incremental/
willr.rs

1//! Native streaming Williams %R — TA-Lib parity (`talib_rs::momentum::willr`).
2
3use crate::traits::Next;
4
5/// Williams %R — input `(high, low, close)`.
6#[derive(Debug, Clone)]
7#[allow(non_camel_case_types)]
8pub struct WILLR {
9    pub timeperiod: usize,
10    high: Vec<f64>,
11    low: Vec<f64>,
12    highest: f64,
13    highest_idx: usize,
14    lowest: f64,
15    lowest_idx: usize,
16    trailing_idx: usize,
17}
18
19impl WILLR {
20    pub fn new(timeperiod: usize) -> Self {
21        Self {
22            timeperiod,
23            high: Vec::new(),
24            low: Vec::new(),
25            highest: f64::NEG_INFINITY,
26            highest_idx: 0,
27            lowest: f64::INFINITY,
28            lowest_idx: 0,
29            trailing_idx: 0,
30        }
31    }
32
33    #[inline]
34    fn willr_from_range(highest: f64, lowest: f64, close: f64) -> f64 {
35        let range = highest - lowest;
36        if range > 0.0 {
37            -100.0 * (highest - close) / range
38        } else {
39            0.0
40        }
41    }
42}
43
44impl Next<(f64, f64, f64)> for WILLR {
45    type Output = f64;
46
47    fn next(&mut self, (h, l, c): (f64, f64, f64)) -> Self::Output {
48        let timeperiod = self.timeperiod;
49        if timeperiod < 2 {
50            return f64::NAN;
51        }
52        let lookback = timeperiod - 1;
53
54        self.high.push(h);
55        self.low.push(l);
56        let today = self.high.len() - 1;
57
58        if today < lookback {
59            if today == 0 {
60                self.highest = h;
61                self.highest_idx = 0;
62                self.lowest = l;
63                self.lowest_idx = 0;
64            } else {
65                if h >= self.highest {
66                    self.highest = h;
67                    self.highest_idx = today;
68                }
69                if l <= self.lowest {
70                    self.lowest = l;
71                    self.lowest_idx = today;
72                }
73            }
74            return f64::NAN;
75        }
76
77        if today == lookback {
78            if h >= self.highest {
79                self.highest = h;
80                self.highest_idx = today;
81            }
82            if l <= self.lowest {
83                self.lowest = l;
84                self.lowest_idx = today;
85            }
86            let out = Self::willr_from_range(self.highest, self.lowest, c);
87            self.trailing_idx = 1;
88            return out;
89        }
90
91        let high = &self.high;
92        let low = &self.low;
93
94        if self.highest_idx < self.trailing_idx {
95            self.highest_idx = self.trailing_idx;
96            self.highest = high[self.trailing_idx];
97            for (j, &val) in high[self.trailing_idx + 1..=today].iter().enumerate() {
98                if val >= self.highest {
99                    self.highest = val;
100                    self.highest_idx = self.trailing_idx + 1 + j;
101                }
102            }
103        } else if h >= self.highest {
104            self.highest_idx = today;
105            self.highest = h;
106        }
107
108        if self.lowest_idx < self.trailing_idx {
109            self.lowest_idx = self.trailing_idx;
110            self.lowest = low[self.trailing_idx];
111            for (j, &val) in low[self.trailing_idx + 1..=today].iter().enumerate() {
112                if val <= self.lowest {
113                    self.lowest = val;
114                    self.lowest_idx = self.trailing_idx + 1 + j;
115                }
116            }
117        } else if l <= self.lowest {
118            self.lowest_idx = today;
119            self.lowest = l;
120        }
121
122        let out = Self::willr_from_range(self.highest, self.lowest, c);
123        self.trailing_idx += 1;
124        out
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use proptest::prelude::*;
132
133    fn ordered_hlc(
134        h: &[f64],
135        l: &[f64],
136        c: &[f64],
137    ) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
138        let len = h.len().min(l.len()).min(c.len());
139        let mut high = Vec::with_capacity(len);
140        let mut low = Vec::with_capacity(len);
141        let mut close = Vec::with_capacity(len);
142        for i in 0..len {
143            let vh = h[i];
144            let vl = l[i];
145            let vc = c[i];
146            high.push(vh.max(vl).max(vc));
147            low.push(vh.min(vl).min(vc));
148            close.push(vc);
149        }
150        (high, low, close)
151    }
152
153    proptest! {
154        #[test]
155        fn test_willr_parity(
156            h in prop::collection::vec(1.0..100.0, 10..100),
157            l in prop::collection::vec(1.0..100.0, 10..100),
158            c in prop::collection::vec(1.0..100.0, 10..100),
159        ) {
160            let (high, low, close) = ordered_hlc(&h, &l, &c);
161            let len = high.len();
162            if len == 0 { return Ok(()); }
163            let period = 14;
164            let mut willr = WILLR::new(period);
165            let streaming: Vec<f64> =
166                (0..len).map(|i| willr.next((high[i], low[i], close[i]))).collect();
167            let batch = talib_rs::momentum::willr(&high, &low, &close, period)
168                .unwrap_or_else(|_| vec![f64::NAN; len]);
169            for (s, b) in streaming.iter().zip(batch.iter()) {
170                if s.is_nan() {
171                    assert!(b.is_nan());
172                } else if !b.is_nan() {
173                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
174                }
175            }
176        }
177    }
178}