Skip to main content

kestrel_chartkit/indicator/
pivot_sets.rs

1use super::{Indicator, IndicatorAlert, IndicatorOutput};
2use crate::model::Bar;
3use crate::timeframe::{Timeframe, TimeframeError};
4use std::collections::HashMap;
5
6#[cfg(feature = "serde")]
7use serde::{Deserialize, Serialize};
8
9/// Type of pivot calculation set.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
12pub enum PivotSetType {
13    #[default]
14    Classic,
15    Fibonacci,
16    Camarilla,
17    Woodie,
18    DeMark,
19    Cpr, // Central Pivot Range
20}
21
22/// Multi-pivot sets engine calculating classic, fibonacci, camarilla, woodie, demark and CPR levels.
23#[derive(Debug, Clone)]
24pub struct PivotSetsEngine {
25    pivot_type: PivotSetType,
26    period_high: f64,
27    period_low: f64,
28    period_close: f64,
29    period_open: f64,
30    curr_period_high: f64,
31    curr_period_low: f64,
32    curr_period_open: f64,
33    curr_period_close: f64,
34    period_timeframe: Timeframe,
35    utc_offset_seconds: i32,
36    current_period_start: Option<i64>,
37}
38
39impl PivotSetsEngine {
40    pub fn new(pivot_type: PivotSetType) -> Self {
41        Self {
42            pivot_type,
43            period_high: 0.0,
44            period_low: 0.0,
45            period_close: 0.0,
46            period_open: 0.0,
47            curr_period_high: 0.0,
48            curr_period_low: f64::MAX,
49            curr_period_open: 0.0,
50            curr_period_close: 0.0,
51            period_timeframe: Timeframe::Day(1),
52            utc_offset_seconds: 0,
53            current_period_start: None,
54        }
55    }
56
57    pub fn with_timeframe(
58        pivot_type: PivotSetType,
59        period_timeframe: Timeframe,
60    ) -> Result<Self, TimeframeError> {
61        let mut engine = Self::new(pivot_type);
62        engine.period_timeframe = period_timeframe.validate()?;
63        Ok(engine)
64    }
65
66    pub fn with_utc_offset(mut self, utc_offset_seconds: i32) -> Self {
67        self.utc_offset_seconds = utc_offset_seconds;
68        self
69    }
70
71    pub fn with_defaults() -> Self {
72        Self::new(PivotSetType::Classic)
73    }
74}
75
76impl Indicator for PivotSetsEngine {
77    fn name(&self) -> &str {
78        "pivot_sets"
79    }
80
81    fn warmup_period(&self) -> usize {
82        2
83    }
84
85    fn reset(&mut self) {
86        self.period_high = 0.0;
87        self.period_low = 0.0;
88        self.period_close = 0.0;
89        self.period_open = 0.0;
90        self.curr_period_high = 0.0;
91        self.curr_period_low = f64::MAX;
92        self.curr_period_open = 0.0;
93        self.curr_period_close = 0.0;
94        self.current_period_start = None;
95    }
96
97    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
98        let period_start = self
99            .period_timeframe
100            .bucket_start(bar.timestamp, self.utc_offset_seconds);
101        let is_new_period = self
102            .current_period_start
103            .is_some_and(|previous| previous != period_start);
104        self.current_period_start = Some(period_start);
105
106        if is_new_period {
107            self.period_high = self.curr_period_high;
108            self.period_low = self.curr_period_low;
109            self.period_close = self.curr_period_close;
110            self.period_open = self.curr_period_open;
111
112            self.curr_period_high = bar.high;
113            self.curr_period_low = bar.low;
114            self.curr_period_open = bar.open;
115            self.curr_period_close = bar.close;
116        } else {
117            if self.curr_period_open == 0.0 {
118                self.curr_period_open = bar.open;
119            }
120            self.curr_period_high = self.curr_period_high.max(bar.high);
121            self.curr_period_low = self.curr_period_low.min(bar.low);
122            self.curr_period_close = bar.close;
123        }
124
125        let (h, l, c, o) = if self.period_high > 0.0 {
126            (
127                self.period_high,
128                self.period_low,
129                self.period_close,
130                self.period_open,
131            )
132        } else {
133            (
134                self.curr_period_high,
135                self.curr_period_low,
136                bar.close,
137                self.curr_period_open,
138            )
139        };
140
141        let mut extra = HashMap::new();
142        let p: f64;
143
144        match self.pivot_type {
145            PivotSetType::Classic => {
146                p = (h + l + c) / 3.0;
147                extra.insert("p".to_string(), p);
148                extra.insert("r1".to_string(), 2.0 * p - l);
149                extra.insert("s1".to_string(), 2.0 * p - h);
150                extra.insert("r2".to_string(), p + (h - l));
151                extra.insert("s2".to_string(), p - (h - l));
152                extra.insert("r3".to_string(), h + 2.0 * (p - l));
153                extra.insert("s3".to_string(), l - 2.0 * (h - p));
154            }
155            PivotSetType::Fibonacci => {
156                p = (h + l + c) / 3.0;
157                let range = h - l;
158                extra.insert("p".to_string(), p);
159                extra.insert("r1".to_string(), p + 0.382 * range);
160                extra.insert("s1".to_string(), p - 0.382 * range);
161                extra.insert("r2".to_string(), p + 0.618 * range);
162                extra.insert("s2".to_string(), p - 0.618 * range);
163                extra.insert("r3".to_string(), p + 1.000 * range);
164                extra.insert("s3".to_string(), p - 1.000 * range);
165            }
166            PivotSetType::Camarilla => {
167                p = (h + l + c) / 3.0;
168                let range = h - l;
169                extra.insert("p".to_string(), p);
170                extra.insert("r1".to_string(), c + range * 1.1 / 12.0);
171                extra.insert("s1".to_string(), c - range * 1.1 / 12.0);
172                extra.insert("r2".to_string(), c + range * 1.1 / 6.0);
173                extra.insert("s2".to_string(), c - range * 1.1 / 6.0);
174                extra.insert("r3".to_string(), c + range * 1.1 / 4.0);
175                extra.insert("s3".to_string(), c - range * 1.1 / 4.0);
176                extra.insert("r4".to_string(), c + range * 1.1 / 2.0);
177                extra.insert("s4".to_string(), c - range * 1.1 / 2.0);
178            }
179            PivotSetType::Woodie => {
180                p = (h + l + 2.0 * c) / 4.0;
181                extra.insert("p".to_string(), p);
182                extra.insert("r1".to_string(), 2.0 * p - l);
183                extra.insert("s1".to_string(), 2.0 * p - h);
184                extra.insert("r2".to_string(), p + (h - l));
185                extra.insert("s2".to_string(), p - (h - l));
186            }
187            PivotSetType::DeMark => {
188                let x = if c < o {
189                    h + 2.0 * l + c
190                } else if c > o {
191                    2.0 * h + l + c
192                } else {
193                    h + l + 2.0 * c
194                };
195                p = x / 4.0;
196                extra.insert("p".to_string(), p);
197                extra.insert("r1".to_string(), x / 2.0 - l);
198                extra.insert("s1".to_string(), x / 2.0 - h);
199            }
200            PivotSetType::Cpr => {
201                p = (h + l + c) / 3.0;
202                let bc = (h + l) / 2.0;
203                let tc = (p - bc) + p;
204                extra.insert("p".to_string(), p);
205                extra.insert("tc".to_string(), tc.max(bc));
206                extra.insert("bc".to_string(), tc.min(bc));
207            }
208        }
209
210        Some(IndicatorOutput::with_extra(p, extra))
211    }
212
213    fn alerts(&self) -> Vec<IndicatorAlert> {
214        Vec::new()
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn test_pivot_sets_classic() {
224        let mut ps = PivotSetsEngine::new(PivotSetType::Classic);
225        let bar1 = Bar::new(0, 100.0, 110.0, 90.0, 105.0, 1000.0);
226        let bar2 = Bar::new(86400, 105.0, 115.0, 100.0, 110.0, 1000.0);
227
228        ps.on_bar(&bar1);
229        let out = ps.on_bar(&bar2).unwrap();
230        // Pivot P = (110 + 90 + 105) / 3 = 101.666...
231        assert!((out.extra["p"] - 101.666_666_666_666_67).abs() < 1e-12);
232        assert!(out.extra.contains_key("r1"));
233        assert!(out.extra.contains_key("s1"));
234    }
235
236    #[test]
237    fn period_close_comes_from_previous_period() {
238        let mut ps = PivotSetsEngine::new(PivotSetType::Classic);
239        ps.on_bar(&Bar::new(0, 100.0, 110.0, 90.0, 101.0, 1.0));
240        ps.on_bar(&Bar::new(60, 101.0, 112.0, 91.0, 107.0, 1.0));
241        let output = ps
242            .on_bar(&Bar::new(86_400, 200.0, 210.0, 190.0, 205.0, 1.0))
243            .unwrap();
244        assert!((output.extra["p"] - (112.0 + 90.0 + 107.0) / 3.0).abs() < 1e-12);
245    }
246}