Skip to main content

wickra_core/indicators/
classic_pivots.rs

1//! Classic (Floor-Trader) Pivot Points.
2
3use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6/// Classic Pivot Points output: pivot plus three resistances and three supports.
7#[derive(Debug, Clone, Copy, PartialEq)]
8pub struct ClassicPivotsOutput {
9    /// Pivot Point: `(H + L + C) / 3`.
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    /// Resistance 3: `H + 2·(PP − L)`.
16    pub r3: f64,
17    /// Support 1: `2·PP − H`.
18    pub s1: f64,
19    /// Support 2: `PP − (H − L)`.
20    pub s2: f64,
21    /// Support 3: `L − 2·(H − PP)`.
22    pub s3: f64,
23}
24
25/// Classic (Floor-Trader) Pivot Points — the standard pivot/resistance/support
26/// levels computed from a completed candle's high, low and close.
27///
28/// ```text
29/// PP = (H + L + C) / 3
30/// R1 = 2·PP − L          S1 = 2·PP − H
31/// R2 = PP + (H − L)      S2 = PP − (H − L)
32/// R3 = H + 2·(PP − L)    S3 = L − 2·(H − PP)
33/// ```
34///
35/// Pivots are typically computed once per session (day, week, month) from the
36/// **previous** session's bar and used as fixed reference levels for the next
37/// session. The streaming API here simply re-evaluates the formula on every
38/// candle it sees, which makes it a one-step transform you can wire to any
39/// pre-aggregated session bar. There are no parameters and no warmup — the
40/// first candle produces the first set of levels.
41///
42/// # Example
43///
44/// ```
45/// use wickra_core::{Candle, ClassicPivots, Indicator};
46///
47/// let prev = Candle::new(100.0, 110.0, 90.0, 105.0, 1.0, 0).unwrap();
48/// let mut pp = ClassicPivots::new();
49/// let levels = pp.update(prev).unwrap();
50/// assert!((levels.pp - 101.6666666666).abs() < 1e-9);
51/// assert!(levels.r1 > levels.pp);
52/// assert!(levels.s1 < levels.pp);
53/// ```
54#[derive(Debug, Clone, Default)]
55pub struct ClassicPivots {
56    ready: bool,
57}
58
59impl ClassicPivots {
60    /// Construct a new Classic Pivot Points indicator. The indicator has no
61    /// parameters and no warmup.
62    pub const fn new() -> Self {
63        Self { ready: false }
64    }
65}
66
67impl Indicator for ClassicPivots {
68    type Input = Candle;
69    type Output = ClassicPivotsOutput;
70
71    #[inline]
72    fn update(&mut self, candle: Candle) -> Option<ClassicPivotsOutput> {
73        let (h, l, c) = (candle.high, candle.low, candle.close);
74        let pp = (h + l + c) / 3.0;
75        let range = h - l;
76        let out = ClassicPivotsOutput {
77            pp,
78            r1: 2.0 * pp - l,
79            r2: pp + range,
80            r3: h + 2.0 * (pp - l),
81            s1: 2.0 * pp - h,
82            s2: pp - range,
83            s3: l - 2.0 * (h - pp),
84        };
85        self.ready = true;
86        Some(out)
87    }
88
89    fn reset(&mut self) {
90        self.ready = false;
91    }
92
93    #[inline]
94    fn warmup_period(&self) -> usize {
95        1
96    }
97
98    #[inline]
99    fn is_ready(&self) -> bool {
100        self.ready
101    }
102
103    #[inline]
104    fn name(&self) -> &'static str {
105        "ClassicPivots"
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use crate::traits::BatchExt;
113
114    fn c(h: f64, l: f64, close: f64, ts: i64) -> Candle {
115        Candle::new(close, h, l, close, 1.0, ts).unwrap()
116    }
117
118    #[test]
119    fn formula_reference_values() {
120        // H=110, L=90, C=105 -> PP = 305/3 ≈ 101.6667.
121        let levels = ClassicPivots::new()
122            .update(c(110.0, 90.0, 105.0, 0))
123            .unwrap();
124        let pp = 305.0 / 3.0;
125        let range = 20.0;
126        assert!((levels.pp - pp).abs() < 1e-12);
127        assert!((levels.r1 - (2.0 * pp - 90.0)).abs() < 1e-12);
128        assert!((levels.s1 - (2.0 * pp - 110.0)).abs() < 1e-12);
129        assert!((levels.r2 - (pp + range)).abs() < 1e-12);
130        assert!((levels.s2 - (pp - range)).abs() < 1e-12);
131        assert!((levels.r3 - (110.0 + 2.0 * (pp - 90.0))).abs() < 1e-12);
132        assert!((levels.s3 - (90.0 - 2.0 * (110.0 - pp))).abs() < 1e-12);
133    }
134
135    #[test]
136    fn ordering_resistance_above_pivot_above_support() {
137        // For any non-degenerate bar with H > L, R-levels exceed PP and S-levels lie below.
138        let levels = ClassicPivots::new()
139            .update(c(200.0, 100.0, 150.0, 0))
140            .unwrap();
141        assert!(levels.r3 >= levels.r2);
142        assert!(levels.r2 >= levels.r1);
143        assert!(levels.r1 >= levels.pp);
144        assert!(levels.pp >= levels.s1);
145        assert!(levels.s1 >= levels.s2);
146        assert!(levels.s2 >= levels.s3);
147    }
148
149    #[test]
150    fn constant_series_collapses_levels() {
151        // H = L = C means range = 0 and every level equals the close.
152        let levels = ClassicPivots::new().update(c(50.0, 50.0, 50.0, 0)).unwrap();
153        assert_eq!(levels.pp, 50.0);
154        assert_eq!(levels.r1, 50.0);
155        assert_eq!(levels.s1, 50.0);
156        assert_eq!(levels.r2, 50.0);
157        assert_eq!(levels.s2, 50.0);
158        assert_eq!(levels.r3, 50.0);
159        assert_eq!(levels.s3, 50.0);
160    }
161
162    #[test]
163    fn ready_after_first_update_warmup_is_one() {
164        let mut pp = ClassicPivots::new();
165        assert!(!pp.is_ready());
166        assert_eq!(pp.warmup_period(), 1);
167        pp.update(c(11.0, 9.0, 10.0, 0));
168        assert!(pp.is_ready());
169    }
170
171    #[test]
172    fn reset_clears_state() {
173        let mut pp = ClassicPivots::new();
174        pp.update(c(11.0, 9.0, 10.0, 0));
175        assert!(pp.is_ready());
176        pp.reset();
177        assert!(!pp.is_ready());
178    }
179
180    #[test]
181    fn batch_equals_streaming() {
182        let candles: Vec<Candle> = (0_i32..40)
183            .map(|i| {
184                c(
185                    f64::from(i) + 2.0,
186                    f64::from(i),
187                    f64::from(i) + 1.0,
188                    i.into(),
189                )
190            })
191            .collect();
192        let mut a = ClassicPivots::new();
193        let mut b = ClassicPivots::new();
194        assert_eq!(
195            a.batch(&candles),
196            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
197        );
198    }
199
200    #[test]
201    fn accessors_and_metadata() {
202        let pp = ClassicPivots::new();
203        assert_eq!(pp.warmup_period(), 1);
204        assert_eq!(pp.name(), "ClassicPivots");
205    }
206}