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    fn update(&mut self, candle: Candle) -> Option<ClassicPivotsOutput> {
72        let (h, l, c) = (candle.high, candle.low, candle.close);
73        let pp = (h + l + c) / 3.0;
74        let range = h - l;
75        let out = ClassicPivotsOutput {
76            pp,
77            r1: 2.0 * pp - l,
78            r2: pp + range,
79            r3: h + 2.0 * (pp - l),
80            s1: 2.0 * pp - h,
81            s2: pp - range,
82            s3: l - 2.0 * (h - pp),
83        };
84        self.ready = true;
85        Some(out)
86    }
87
88    fn reset(&mut self) {
89        self.ready = false;
90    }
91
92    fn warmup_period(&self) -> usize {
93        1
94    }
95
96    fn is_ready(&self) -> bool {
97        self.ready
98    }
99
100    fn name(&self) -> &'static str {
101        "ClassicPivots"
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use crate::traits::BatchExt;
109
110    fn c(h: f64, l: f64, close: f64, ts: i64) -> Candle {
111        Candle::new(close, h, l, close, 1.0, ts).unwrap()
112    }
113
114    #[test]
115    fn formula_reference_values() {
116        // H=110, L=90, C=105 -> PP = 305/3 ≈ 101.6667.
117        let levels = ClassicPivots::new()
118            .update(c(110.0, 90.0, 105.0, 0))
119            .unwrap();
120        let pp = 305.0 / 3.0;
121        let range = 20.0;
122        assert!((levels.pp - pp).abs() < 1e-12);
123        assert!((levels.r1 - (2.0 * pp - 90.0)).abs() < 1e-12);
124        assert!((levels.s1 - (2.0 * pp - 110.0)).abs() < 1e-12);
125        assert!((levels.r2 - (pp + range)).abs() < 1e-12);
126        assert!((levels.s2 - (pp - range)).abs() < 1e-12);
127        assert!((levels.r3 - (110.0 + 2.0 * (pp - 90.0))).abs() < 1e-12);
128        assert!((levels.s3 - (90.0 - 2.0 * (110.0 - pp))).abs() < 1e-12);
129    }
130
131    #[test]
132    fn ordering_resistance_above_pivot_above_support() {
133        // For any non-degenerate bar with H > L, R-levels exceed PP and S-levels lie below.
134        let levels = ClassicPivots::new()
135            .update(c(200.0, 100.0, 150.0, 0))
136            .unwrap();
137        assert!(levels.r3 >= levels.r2);
138        assert!(levels.r2 >= levels.r1);
139        assert!(levels.r1 >= levels.pp);
140        assert!(levels.pp >= levels.s1);
141        assert!(levels.s1 >= levels.s2);
142        assert!(levels.s2 >= levels.s3);
143    }
144
145    #[test]
146    fn constant_series_collapses_levels() {
147        // H = L = C means range = 0 and every level equals the close.
148        let levels = ClassicPivots::new().update(c(50.0, 50.0, 50.0, 0)).unwrap();
149        assert_eq!(levels.pp, 50.0);
150        assert_eq!(levels.r1, 50.0);
151        assert_eq!(levels.s1, 50.0);
152        assert_eq!(levels.r2, 50.0);
153        assert_eq!(levels.s2, 50.0);
154        assert_eq!(levels.r3, 50.0);
155        assert_eq!(levels.s3, 50.0);
156    }
157
158    #[test]
159    fn ready_after_first_update_warmup_is_one() {
160        let mut pp = ClassicPivots::new();
161        assert!(!pp.is_ready());
162        assert_eq!(pp.warmup_period(), 1);
163        pp.update(c(11.0, 9.0, 10.0, 0));
164        assert!(pp.is_ready());
165    }
166
167    #[test]
168    fn reset_clears_state() {
169        let mut pp = ClassicPivots::new();
170        pp.update(c(11.0, 9.0, 10.0, 0));
171        assert!(pp.is_ready());
172        pp.reset();
173        assert!(!pp.is_ready());
174    }
175
176    #[test]
177    fn batch_equals_streaming() {
178        let candles: Vec<Candle> = (0_i32..40)
179            .map(|i| {
180                c(
181                    f64::from(i) + 2.0,
182                    f64::from(i),
183                    f64::from(i) + 1.0,
184                    i.into(),
185                )
186            })
187            .collect();
188        let mut a = ClassicPivots::new();
189        let mut b = ClassicPivots::new();
190        assert_eq!(
191            a.batch(&candles),
192            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
193        );
194    }
195
196    #[test]
197    fn accessors_and_metadata() {
198        let pp = ClassicPivots::new();
199        assert_eq!(pp.warmup_period(), 1);
200        assert_eq!(pp.name(), "ClassicPivots");
201    }
202}