Skip to main content

finance_solution/stocks/ta/
willr.rs

1//! # Williams %R
2//!
3//! Oscillator on high/low/close over lookback \(N\):
4//!
5//! ```text
6//! %R = -100 * (HH − close) / (HH − LL)
7//! ```
8//!
9//! where \(HH = \max(high)\) and \(LL = \min(low)\) over the last \(N\) bars.
10//! Range is typically **\[−100, 0\]**. Flat window (\(HH = LL\)): carry previous %R, else **−50**.
11//!
12//! Default: **period 14** ([`WillrParams::period_14`]).
13//!
14//! ---
15//!
16//! ## Trading perspective
17//!
18//! | Region | Habit (classic, not a rule) |
19//! |--------|-----------------------------|
20//! | %R \> −20 | “Overbought” screen |
21//! | %R \< −80 | “Oversold” screen |
22//! | Cross back from extreme | Momentum resume / mean-reversion exit screen |
23//!
24//! ## vs other oscillators
25//!
26//! | | Williams %R | Stochastic | RSI |
27//! |--|-------------|------------|-----|
28//! | Scale | \[−100, 0\] | \[0, 100\] | \[0, 100\] |
29//! | Inputs | H/L/C window | H/L/C + smooth | Closes only |
30//! | Feel | Fast, inverted Stoch-like | Smoothed %K/%D | Wilder smooth, slower |
31//!
32//! Rough map: raw Stoch %K ≈ `100 + %R` (same HH/LL idea; sign/offset differ). Prefer
33//! **Stochastic** when you want %D signal line; **%R** when you want a single fast line.
34//!
35//! ## Pairs well with
36//!
37//! - **ADX / DI** — only fade %R extremes when ADX is low (range); avoid fading when ADX is high.
38//! - **SMA/EMA trend filter** — long setups only above rising MA, etc.
39//! - **Volume (OBV/MFI)** — confirm oversold bounce with rising money flow.
40//! - **ATR stops** — oscillator does not size risk; ATR does.
41//!
42//! ---
43//!
44//! ## Engineering
45//!
46//! [`WillrParams`] → [`willr`] / [`WillrState`] → [`willr_solution`].  
47//! Batch uses [`WillrState`] end-to-end. HH/LL are **amortized O(1)** (sliding max/min).
48//!
49//! ## Word problem
50//!
51//! > Highs 12, lows 10, close 11 for three bars with \(N=3\). What is %R on bar 2?
52//!
53//! Expect: \(HH=12\), \(LL=10\), %R = \(-100 \times (12-11)/(12-10) = -50\).
54//!
55//! ```
56//! use finance_solution::stocks::ta::{willr, WillrParams};
57//! let h = [12.0, 12.0, 12.0];
58//! let l = [10.0, 10.0, 10.0];
59//! let c = [11.0, 11.0, 11.0];
60//! let s = willr(&h, &l, &c, WillrParams::new(3)).unwrap();
61//! assert!((s.willr[2].unwrap() - (-50.0)).abs() < 1e-12);
62//! ```
63
64use crate::stocks::ta::common::{opt_cell, require_hlc};
65use crate::stocks::ta::ring::{SlidingMax, SlidingMin};
66use crate::util::error::{require_finite, FinanceError, FinanceResult};
67use crate::util::primitives::PeriodLength;
68use crate::{columns_with_strings, print_table_locale_opt};
69
70/// Williams %R lookback.
71#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
72pub struct WillrParams {
73    pub period: usize,
74}
75
76impl WillrParams {
77    pub const fn new(period: usize) -> Self {
78        Self { period }
79    }
80
81    /// Classic 14-bar Williams %R.
82    pub const fn period_14() -> Self {
83        Self { period: 14 }
84    }
85}
86
87/// Validated pack.
88#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
89pub struct ValidatedWillr {
90    params: WillrParams,
91}
92
93impl ValidatedWillr {
94    pub fn new(params: WillrParams) -> FinanceResult<Self> {
95        PeriodLength::new(params.period)?;
96        Ok(Self { params })
97    }
98
99    pub fn params(self) -> WillrParams {
100        self.params
101    }
102
103    pub fn compute(self, high: &[f64], low: &[f64], close: &[f64]) -> FinanceResult<WillrSeries> {
104        willr_validated(high, low, close, self)
105    }
106}
107
108#[derive(Clone, Debug, PartialEq)]
109pub struct WillrSeries {
110    pub willr: Vec<Option<f64>>,
111    pub params: WillrParams,
112}
113
114impl WillrSeries {
115    pub fn last(&self) -> Option<f64> {
116        self.willr.iter().rev().find_map(|x| *x)
117    }
118}
119
120/// Incremental Williams %R.
121///
122/// After warm-up each [`push`](Self::push) is amortized O(1).
123#[derive(Clone, Debug)]
124pub struct WillrState {
125    params: WillrParams,
126    high_max: SlidingMax,
127    low_min: SlidingMin,
128    prev: Option<f64>,
129    last: Option<f64>,
130}
131
132impl WillrState {
133    pub fn new(params: WillrParams) -> FinanceResult<Self> {
134        let _ = ValidatedWillr::new(params)?;
135        Ok(Self {
136            params,
137            high_max: SlidingMax::with_window(params.period),
138            low_min: SlidingMin::with_window(params.period),
139            prev: None,
140            last: None,
141        })
142    }
143
144    pub fn from_history(
145        params: WillrParams,
146        high: &[f64],
147        low: &[f64],
148        close: &[f64],
149    ) -> FinanceResult<Self> {
150        let mut s = Self::new(params)?;
151        let _ = s.push_bars(high, low, close)?;
152        Ok(s)
153    }
154
155    pub fn params(&self) -> WillrParams {
156        self.params
157    }
158
159    pub fn reset(&mut self) {
160        self.high_max.clear();
161        self.low_min.clear();
162        self.prev = None;
163        self.last = None;
164    }
165
166    pub fn push(&mut self, high: f64, low: f64, close: f64) -> FinanceResult<Option<f64>> {
167        require_finite("high", high)?;
168        require_finite("low", low)?;
169        require_finite("close", close)?;
170        if high < low {
171            return Err(FinanceError::InvalidCashflow {
172                message: "high must be >= low for each bar",
173            });
174        }
175        let hh = self.high_max.push(high).unwrap();
176        let ll = self.low_min.push(low).unwrap();
177        if !self.high_max.is_full() {
178            self.last = None;
179            return Ok(None);
180        }
181        let range = hh - ll;
182        let raw = if range == 0.0 {
183            self.prev.unwrap_or(-50.0)
184        } else {
185            -100.0 * (hh - close) / range
186        };
187        self.prev = Some(raw);
188        self.last = Some(raw);
189        Ok(Some(raw))
190    }
191
192    pub fn push_bars(
193        &mut self,
194        high: &[f64],
195        low: &[f64],
196        close: &[f64],
197    ) -> FinanceResult<Vec<Option<f64>>> {
198        require_hlc(high, low, close)?;
199        let mut out = Vec::with_capacity(close.len());
200        for i in 0..close.len() {
201            out.push(self.push(high[i], low[i], close[i])?);
202        }
203        Ok(out)
204    }
205
206    pub fn last(&self) -> Option<f64> {
207        self.last
208    }
209}
210
211pub fn willr(
212    high: &[f64],
213    low: &[f64],
214    close: &[f64],
215    params: WillrParams,
216) -> FinanceResult<WillrSeries> {
217    ValidatedWillr::new(params)?.compute(high, low, close)
218}
219
220fn willr_validated(
221    high: &[f64],
222    low: &[f64],
223    close: &[f64],
224    eng: ValidatedWillr,
225) -> FinanceResult<WillrSeries> {
226    let mut st = WillrState::new(eng.params)?;
227    let willr = st.push_bars(high, low, close)?;
228    Ok(WillrSeries {
229        willr,
230        params: eng.params,
231    })
232}
233
234#[derive(Clone, Debug)]
235pub struct WillrSolution {
236    series: WillrSeries,
237    close: Vec<f64>,
238    formula: String,
239}
240
241impl WillrSolution {
242    pub fn series(&self) -> &WillrSeries {
243        &self.series
244    }
245    pub fn formula(&self) -> &str {
246        &self.formula
247    }
248
249    pub fn print_table(&self) {
250        self.print_table_locale_opt(None, None);
251    }
252
253    pub fn print_table_locale(&self, locale: &num_format::Locale, precision: usize) {
254        self.print_table_locale_opt(Some(locale), Some(precision));
255    }
256
257    fn print_table_locale_opt(
258        &self,
259        locale: Option<&num_format::Locale>,
260        precision: Option<usize>,
261    ) {
262        let columns = columns_with_strings(&[
263            ("period", "i", true),
264            ("close", "f", true),
265            ("willr", "f", true),
266        ]);
267        let data = self
268            .close
269            .iter()
270            .enumerate()
271            .map(|(i, c)| vec![i.to_string(), c.to_string(), opt_cell(self.series.willr[i])])
272            .collect();
273        print_table_locale_opt(&columns, data, locale, precision);
274    }
275}
276
277/// # Examples
278/// ```
279/// use finance_solution::stocks::ta::{willr_solution, WillrParams};
280/// let h: Vec<_> = (0..20).map(|i| 11.0 + i as f64).collect();
281/// let l: Vec<_> = (0..20).map(|i| 9.0 + i as f64).collect();
282/// let c: Vec<_> = (0..20).map(|i| 10.0 + i as f64).collect();
283/// let sol = willr_solution(&h, &l, &c, WillrParams::period_14()).unwrap();
284/// assert!(sol.formula().contains("14"));
285/// ```
286pub fn willr_solution(
287    high: &[f64],
288    low: &[f64],
289    close: &[f64],
290    params: WillrParams,
291) -> FinanceResult<WillrSolution> {
292    let series = willr(high, low, close, params)?;
293    Ok(WillrSolution {
294        series,
295        close: close.to_vec(),
296        formula: format!("%R = -100 * (HH - C) / (HH - LL), period={}", params.period),
297    })
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303
304    #[test]
305    fn flat_mid_is_neg_50() {
306        let h = vec![12.0; 5];
307        let l = vec![10.0; 5];
308        let c = vec![11.0; 5];
309        let s = willr(&h, &l, &c, WillrParams::new(3)).unwrap();
310        assert!((s.willr[2].unwrap() - (-50.0)).abs() < 1e-12);
311    }
312
313    #[test]
314    fn at_high_is_zero() {
315        let h = [10.0, 11.0, 12.0];
316        let l = [8.0, 9.0, 10.0];
317        let c = [10.0, 11.0, 12.0]; // close at HH
318        let s = willr(&h, &l, &c, WillrParams::new(3)).unwrap();
319        assert!((s.willr[2].unwrap() - 0.0).abs() < 1e-12);
320    }
321
322    #[test]
323    fn at_low_is_neg_100() {
324        let h = [10.0, 11.0, 12.0];
325        let l = [8.0, 9.0, 10.0];
326        // Window LL = 8, HH = 12; close at LL → %R = −100
327        let c = [9.0, 9.5, 8.0];
328        let s = willr(&h, &l, &c, WillrParams::new(3)).unwrap();
329        assert!((s.willr[2].unwrap() - (-100.0)).abs() < 1e-12);
330    }
331
332    #[test]
333    fn state_parity() {
334        let h: Vec<_> = (0..30).map(|i| 101.0 + (i as f64) * 0.1).collect();
335        let l: Vec<_> = (0..30).map(|i| 99.0 + (i as f64) * 0.1).collect();
336        let c: Vec<_> = (0..30).map(|i| 100.0 + (i as f64) * 0.1).collect();
337        let p = WillrParams::period_14();
338        let batch = willr(&h, &l, &c, p).unwrap();
339        let mut st = WillrState::new(p).unwrap();
340        for i in 0..c.len() {
341            let o = st.push(h[i], l[i], c[i]).unwrap();
342            match (o, batch.willr[i]) {
343                (None, None) => {}
344                (Some(a), Some(b)) => assert!((a - b).abs() < 1e-12),
345                other => panic!("{other:?}"),
346            }
347        }
348    }
349
350    #[test]
351    fn high_lt_low_err() {
352        assert!(willr(&[1.0], &[2.0], &[1.5], WillrParams::new(1)).is_err());
353    }
354}