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    #[inline]
60    fn update(&mut self, candle: Candle) -> Option<WoodiePivotsOutput> {
61        let (h, l, c) = (candle.high, candle.low, candle.close);
62        let pp = (h + l + 2.0 * c) / 4.0;
63        let range = h - l;
64        let out = WoodiePivotsOutput {
65            pp,
66            r1: 2.0 * pp - l,
67            r2: pp + range,
68            s1: 2.0 * pp - h,
69            s2: pp - range,
70        };
71        self.ready = true;
72        Some(out)
73    }
74
75    fn reset(&mut self) {
76        self.ready = false;
77    }
78
79    #[inline]
80    fn warmup_period(&self) -> usize {
81        1
82    }
83
84    #[inline]
85    fn is_ready(&self) -> bool {
86        self.ready
87    }
88
89    #[inline]
90    fn name(&self) -> &'static str {
91        "WoodiePivots"
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use crate::traits::BatchExt;
99
100    fn c(h: f64, l: f64, close: f64, ts: i64) -> Candle {
101        Candle::new(close, h, l, close, 1.0, ts).unwrap()
102    }
103
104    #[test]
105    fn formula_reference_values() {
106        // H=110, L=90, C=108 -> PP = (110+90+216)/4 = 104.
107        let levels = WoodiePivots::new()
108            .update(c(110.0, 90.0, 108.0, 0))
109            .unwrap();
110        assert!((levels.pp - 104.0).abs() < 1e-12);
111        assert!((levels.r1 - (2.0 * 104.0 - 90.0)).abs() < 1e-12);
112        assert!((levels.s1 - (2.0 * 104.0 - 110.0)).abs() < 1e-12);
113        assert!((levels.r2 - (104.0 + 20.0)).abs() < 1e-12);
114        assert!((levels.s2 - (104.0 - 20.0)).abs() < 1e-12);
115    }
116
117    #[test]
118    fn pp_differs_from_classic_when_close_is_skewed() {
119        // Classic PP = (H+L+C)/3; Woodie PP weights close twice. They agree
120        // only when C equals (H+L)/2.
121        let levels = WoodiePivots::new()
122            .update(c(120.0, 80.0, 110.0, 0))
123            .unwrap();
124        let classic_pp = (120.0 + 80.0 + 110.0) / 3.0;
125        assert!((levels.pp - classic_pp).abs() > 1e-6);
126        // Equal when close = midpoint.
127        let mid = WoodiePivots::new()
128            .update(c(120.0, 80.0, 100.0, 0))
129            .unwrap();
130        let classic_mid = (120.0 + 80.0 + 100.0) / 3.0;
131        assert!((mid.pp - classic_mid).abs() < 1e-9);
132    }
133
134    #[test]
135    fn ordering_resistance_above_pivot_above_support() {
136        let levels = WoodiePivots::new()
137            .update(c(120.0, 80.0, 110.0, 0))
138            .unwrap();
139        assert!(levels.r2 >= levels.r1);
140        assert!(levels.r1 >= levels.pp);
141        assert!(levels.pp >= levels.s1);
142        assert!(levels.s1 >= levels.s2);
143    }
144
145    #[test]
146    fn constant_series_collapses_levels() {
147        let levels = WoodiePivots::new().update(c(50.0, 50.0, 50.0, 0)).unwrap();
148        assert_eq!(levels.pp, 50.0);
149        assert_eq!(levels.r2, 50.0);
150        assert_eq!(levels.s2, 50.0);
151    }
152
153    #[test]
154    fn warmup_and_ready() {
155        let mut p = WoodiePivots::new();
156        assert!(!p.is_ready());
157        assert_eq!(p.warmup_period(), 1);
158        p.update(c(11.0, 9.0, 10.0, 0));
159        assert!(p.is_ready());
160    }
161
162    #[test]
163    fn reset_clears_state() {
164        let mut p = WoodiePivots::new();
165        p.update(c(11.0, 9.0, 10.0, 0));
166        p.reset();
167        assert!(!p.is_ready());
168    }
169
170    #[test]
171    fn batch_equals_streaming() {
172        let candles: Vec<Candle> = (0_i32..40)
173            .map(|i| {
174                c(
175                    f64::from(i) + 2.0,
176                    f64::from(i),
177                    f64::from(i) + 1.0,
178                    i.into(),
179                )
180            })
181            .collect();
182        let mut a = WoodiePivots::new();
183        let mut b = WoodiePivots::new();
184        assert_eq!(
185            a.batch(&candles),
186            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
187        );
188    }
189
190    #[test]
191    fn accessors_and_metadata() {
192        let p = WoodiePivots::new();
193        assert_eq!(p.warmup_period(), 1);
194        assert_eq!(p.name(), "WoodiePivots");
195    }
196}