wickra_core/indicators/
classic_pivots.rs1use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone, Copy, PartialEq)]
8pub struct ClassicPivotsOutput {
9 pub pp: f64,
11 pub r1: f64,
13 pub r2: f64,
15 pub r3: f64,
17 pub s1: f64,
19 pub s2: f64,
21 pub s3: f64,
23}
24
25#[derive(Debug, Clone, Default)]
55pub struct ClassicPivots {
56 ready: bool,
57}
58
59impl ClassicPivots {
60 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 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 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 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}