kestrel_chartkit/indicator/
pivot_sets.rs1use 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#[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, }
21
22#[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 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}