Skip to main content

wickra_core/indicators/
woodie_pivots.rs

1//! Woodie Pivot Points (Tom Williams).
2
3use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6/// Woodie Pivot Points output: two resistances, pivot, two supports.
7#[derive(Debug, Clone, Copy, PartialEq)]
8pub struct WoodiePivotsOutput {
9    /// Pivot Point: `(H + L + 2·C) / 4`.
10    pub pp: f64,
11    /// Resistance 1: `2·PP − L`.
12    pub r1: f64,
13    /// Resistance 2: `PP + (H − L)`.
14    pub r2: f64,
15    /// Support 1: `2·PP − H`.
16    pub s1: f64,
17    /// Support 2: `PP − (H − L)`.
18    pub s2: f64,
19}
20
21/// Woodie Pivot Points — Tom Williams' close-weighted pivot variant.
22///
23/// ```text
24/// PP = (H + L + 2·C) / 4
25/// R1 = 2·PP − L        S1 = 2·PP − H
26/// R2 = PP + (H − L)    S2 = PP − (H − L)
27/// ```
28///
29/// The double-weighted close shifts the pivot toward where most of the
30/// session's activity actually settled — useful in trending markets where the
31/// close is more meaningful than the midpoint.
32///
33/// # Example
34///
35/// ```
36/// use wickra_core::{Candle, Indicator, WoodiePivots};
37///
38/// let prev = Candle::new(100.0, 110.0, 90.0, 108.0, 1.0, 0).unwrap();
39/// let levels = WoodiePivots::new().update(prev).unwrap();
40/// // Close-weighted PP = (110 + 90 + 2·108)/4 = 104.
41/// assert!((levels.pp - 104.0).abs() < 1e-9);
42/// ```
43#[derive(Debug, Clone, Default)]
44pub struct WoodiePivots {
45    ready: bool,
46}
47
48impl WoodiePivots {
49    /// Construct a new Woodie Pivot Points indicator.
50    pub const fn new() -> Self {
51        Self { ready: false }
52    }
53}
54
55impl Indicator for WoodiePivots {
56    type Input = Candle;
57    type Output = WoodiePivotsOutput;
58
59    fn update(&mut self, candle: Candle) -> Option<WoodiePivotsOutput> {
60        let (h, l, c) = (candle.high, candle.low, candle.close);
61        let pp = (h + l + 2.0 * c) / 4.0;
62        let range = h - l;
63        let out = WoodiePivotsOutput {
64            pp,
65            r1: 2.0 * pp - l,
66            r2: pp + range,
67            s1: 2.0 * pp - h,
68            s2: pp - range,
69        };
70        self.ready = true;
71        Some(out)
72    }
73
74    fn reset(&mut self) {
75        self.ready = false;
76    }
77
78    fn warmup_period(&self) -> usize {
79        1
80    }
81
82    fn is_ready(&self) -> bool {
83        self.ready
84    }
85
86    fn name(&self) -> &'static str {
87        "WoodiePivots"
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use crate::traits::BatchExt;
95
96    fn c(h: f64, l: f64, close: f64, ts: i64) -> Candle {
97        Candle::new(close, h, l, close, 1.0, ts).unwrap()
98    }
99
100    #[test]
101    fn formula_reference_values() {
102        // H=110, L=90, C=108 -> PP = (110+90+216)/4 = 104.
103        let levels = WoodiePivots::new()
104            .update(c(110.0, 90.0, 108.0, 0))
105            .unwrap();
106        assert!((levels.pp - 104.0).abs() < 1e-12);
107        assert!((levels.r1 - (2.0 * 104.0 - 90.0)).abs() < 1e-12);
108        assert!((levels.s1 - (2.0 * 104.0 - 110.0)).abs() < 1e-12);
109        assert!((levels.r2 - (104.0 + 20.0)).abs() < 1e-12);
110        assert!((levels.s2 - (104.0 - 20.0)).abs() < 1e-12);
111    }
112
113    #[test]
114    fn pp_differs_from_classic_when_close_is_skewed() {
115        // Classic PP = (H+L+C)/3; Woodie PP weights close twice. They agree
116        // only when C equals (H+L)/2.
117        let levels = WoodiePivots::new()
118            .update(c(120.0, 80.0, 110.0, 0))
119            .unwrap();
120        let classic_pp = (120.0 + 80.0 + 110.0) / 3.0;
121        assert!((levels.pp - classic_pp).abs() > 1e-6);
122        // Equal when close = midpoint.
123        let mid = WoodiePivots::new()
124            .update(c(120.0, 80.0, 100.0, 0))
125            .unwrap();
126        let classic_mid = (120.0 + 80.0 + 100.0) / 3.0;
127        assert!((mid.pp - classic_mid).abs() < 1e-9);
128    }
129
130    #[test]
131    fn ordering_resistance_above_pivot_above_support() {
132        let levels = WoodiePivots::new()
133            .update(c(120.0, 80.0, 110.0, 0))
134            .unwrap();
135        assert!(levels.r2 >= levels.r1);
136        assert!(levels.r1 >= levels.pp);
137        assert!(levels.pp >= levels.s1);
138        assert!(levels.s1 >= levels.s2);
139    }
140
141    #[test]
142    fn constant_series_collapses_levels() {
143        let levels = WoodiePivots::new().update(c(50.0, 50.0, 50.0, 0)).unwrap();
144        assert_eq!(levels.pp, 50.0);
145        assert_eq!(levels.r2, 50.0);
146        assert_eq!(levels.s2, 50.0);
147    }
148
149    #[test]
150    fn warmup_and_ready() {
151        let mut p = WoodiePivots::new();
152        assert!(!p.is_ready());
153        assert_eq!(p.warmup_period(), 1);
154        p.update(c(11.0, 9.0, 10.0, 0));
155        assert!(p.is_ready());
156    }
157
158    #[test]
159    fn reset_clears_state() {
160        let mut p = WoodiePivots::new();
161        p.update(c(11.0, 9.0, 10.0, 0));
162        p.reset();
163        assert!(!p.is_ready());
164    }
165
166    #[test]
167    fn batch_equals_streaming() {
168        let candles: Vec<Candle> = (0_i32..40)
169            .map(|i| {
170                c(
171                    f64::from(i) + 2.0,
172                    f64::from(i),
173                    f64::from(i) + 1.0,
174                    i.into(),
175                )
176            })
177            .collect();
178        let mut a = WoodiePivots::new();
179        let mut b = WoodiePivots::new();
180        assert_eq!(
181            a.batch(&candles),
182            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
183        );
184    }
185
186    #[test]
187    fn accessors_and_metadata() {
188        let p = WoodiePivots::new();
189        assert_eq!(p.warmup_period(), 1);
190        assert_eq!(p.name(), "WoodiePivots");
191    }
192}