Skip to main content

indicators/momentum/
williams_r.rs

1//! Williams %R.
2//!
3//! Python source: `indicators/other/williams_r.py :: class WilliamsRIndicator`
4//!
5//! # Python algorithm (to port)
6//! ```python
7//! highest_high = data["High"].rolling(window=self.period).max()
8//! lowest_low   = data["Low"].rolling(window=self.period).min()
9//! will_r       = -100 * (highest_high - data["Close"]) / (highest_high - lowest_low)
10//! ```
11//!
12//! Oscillates between -100 and 0.  Above -20 → overbought; below -80 → oversold.
13//!
14//! Output column: `"WR_{period}"`.
15
16use std::collections::HashMap;
17
18use crate::error::IndicatorError;
19use crate::indicator::{Indicator, IndicatorOutput};
20use crate::registry::param_usize;
21use crate::types::Candle;
22
23#[derive(Debug, Clone)]
24pub struct WrParams {
25    pub period: usize,
26}
27impl Default for WrParams {
28    fn default() -> Self {
29        Self { period: 14 }
30    }
31}
32
33#[derive(Debug, Clone)]
34pub struct WilliamsR {
35    pub params: WrParams,
36}
37
38impl WilliamsR {
39    pub fn new(params: WrParams) -> Self {
40        Self { params }
41    }
42    pub fn with_period(period: usize) -> Self {
43        Self::new(WrParams { period })
44    }
45    fn output_key(&self) -> String {
46        format!("WR_{}", self.params.period)
47    }
48}
49
50impl Indicator for WilliamsR {
51    fn name(&self) -> &'static str {
52        "WilliamsR"
53    }
54    fn required_len(&self) -> usize {
55        self.params.period
56    }
57    fn required_columns(&self) -> &[&'static str] {
58        &["high", "low", "close"]
59    }
60
61    /// TODO: port Python rolling max/min %R formula.
62    fn calculate(&self, candles: &[Candle]) -> Result<IndicatorOutput, IndicatorError> {
63        self.check_len(candles)?;
64
65        let n = candles.len();
66        let p = self.params.period;
67        let mut values = vec![f64::NAN; n];
68
69        // TODO: port Python rolling max/min.
70        for i in (p - 1)..n {
71            let window = &candles[(i + 1 - p)..=i];
72            let highest_h = window
73                .iter()
74                .map(|c| c.high)
75                .fold(f64::NEG_INFINITY, f64::max);
76            let lowest_l = window.iter().map(|c| c.low).fold(f64::INFINITY, f64::min);
77            let range = highest_h - lowest_l;
78            values[i] = if range == 0.0 {
79                f64::NAN
80            } else {
81                -100.0 * (highest_h - candles[i].close) / range
82            };
83        }
84
85        Ok(IndicatorOutput::from_pairs([(self.output_key(), values)]))
86    }
87}
88
89pub fn factory<S: ::std::hash::BuildHasher>(params: &HashMap<String, String, S>) -> Result<Box<dyn Indicator>, IndicatorError> {
90    Ok(Box::new(WilliamsR::new(WrParams {
91        period: param_usize(params, "period", 14)?,
92    })))
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    fn candles(data: &[(f64, f64, f64)]) -> Vec<Candle> {
100        data.iter()
101            .enumerate()
102            .map(|(i, &(h, l, c))| Candle {
103                time: i64::try_from(i).expect("time index fits i64"),
104                open: c,
105                high: h,
106                low: l,
107                close: c,
108                volume: 1.0,
109            })
110            .collect()
111    }
112
113    fn rising(n: usize) -> Vec<Candle> {
114        (0..n)
115            .map(|i| {
116                let f = i as f64;
117                Candle {
118                    time: i64::try_from(i).expect("time index fits i64"),
119                    open: f,
120                    high: f + 1.0,
121                    low: f - 1.0,
122                    close: f + 0.5,
123                    volume: 1.0,
124                }
125            })
126            .collect()
127    }
128
129    #[test]
130    fn wr_range_neg100_to_0() {
131        let out = WilliamsR::with_period(14).calculate(&rising(20)).unwrap();
132        for &v in out.get("WR_14").unwrap() {
133            if !v.is_nan() {
134                assert!((-100.0..=0.0).contains(&v), "out of range: {v}");
135            }
136        }
137    }
138
139    #[test]
140    fn wr_close_at_high_is_zero() {
141        // close == highest_high → WR = 0.
142        let bars = vec![(12.0f64, 8.0, 12.0); 14];
143        let bars = candles(&bars);
144        let out = WilliamsR::with_period(14).calculate(&bars).unwrap();
145        let vals = out.get("WR_14").unwrap();
146        assert!((vals[13] - 0.0).abs() < 1e-9, "got {}", vals[13]);
147    }
148
149    #[test]
150    fn wr_close_at_low_is_neg100() {
151        let bars = vec![(12.0f64, 8.0, 8.0); 14];
152        let bars = candles(&bars);
153        let out = WilliamsR::with_period(14).calculate(&bars).unwrap();
154        let vals = out.get("WR_14").unwrap();
155        assert!((vals[13] - (-100.0)).abs() < 1e-9, "got {}", vals[13]);
156    }
157
158    #[test]
159    fn factory_creates_wr() {
160        assert_eq!(factory(&HashMap::new()).unwrap().name(), "WilliamsR");
161    }
162}