1use super::anchor_period::ExchangeDays;
44use super::pivot_points::{
45 LevelLabels, PivotLabelSide, PivotMethod, PivotPoints, refresh_level_labels,
46};
47use crate::model::{Bar, Instrument};
48use crate::studies::stepper::Stepper;
49use crate::studies::{Indicator, IndicatorValue, IndicatorValues, StudyLabel};
50use chrono::{DateTime, NaiveDate, Utc};
51use egui::Color32;
52
53#[derive(Clone)]
55pub struct IntradayPivotPoints {
56 method: PivotMethod,
57 minutes: usize,
58 days: ExchangeDays,
59 values: IndicatorValues,
60 stepper: Stepper<State>,
61 colors: Vec<Color32>,
63 visible: bool,
64 show_labels: bool,
65 show_prices: bool,
66 label_side: PivotLabelSide,
67 precision: Option<usize>,
69 label_precision: Option<usize>,
70 labels: Vec<StudyLabel>,
71}
72
73#[derive(Clone)]
74struct State {
75 session: Option<NaiveDate>,
76 origin: DateTime<Utc>,
77 block: i64,
78 current: Option<Bar>,
79 levels: [f64; 11],
80}
81
82impl Default for State {
83 fn default() -> Self {
84 Self {
85 session: None,
86 origin: DateTime::UNIX_EPOCH,
87 block: 0,
88 current: None,
89 levels: [f64::NAN; 11],
90 }
91 }
92}
93
94impl IntradayPivotPoints {
95 pub fn new(minutes: usize) -> Self {
97 Self {
98 method: PivotMethod::Traditional,
99 minutes: minutes.clamp(1, 1440),
100 days: ExchangeDays::default(),
101 values: IndicatorValues::new(),
102 stepper: Stepper::default(),
103 colors: PivotPoints::new().colors(),
104 visible: true,
105 show_labels: true,
106 show_prices: false,
107 label_side: PivotLabelSide::Left,
108 precision: None,
109 label_precision: None,
110 labels: Vec::new(),
111 }
112 }
113
114 pub fn with_method(mut self, method: PivotMethod) -> Self {
116 self.method = method;
117 self
118 }
119
120 pub fn with_labels(mut self, names: bool, prices: bool) -> Self {
123 self.show_labels = names;
124 self.show_prices = prices;
125 self
126 }
127
128 pub fn with_label_side(mut self, side: PivotLabelSide) -> Self {
130 self.label_side = side;
131 self
132 }
133}
134
135impl Default for IntradayPivotPoints {
137 fn default() -> Self {
138 Self::new(60)
139 }
140}
141
142impl Indicator for IntradayPivotPoints {
143 crate::studies::inputs::study_inputs!(self;
144 method: choice("Type", [PivotMethod::Traditional, PivotMethod::Fibonacci, PivotMethod::Woodie, PivotMethod::Classic, PivotMethod::DM, PivotMethod::Camarilla]),
145 minutes: length("Source period (minutes)", 1, 1440),
146 show_labels: toggle("Show Labels"),
147 show_prices: toggle("Show Prices"),
148 label_side: choice("Labels Position", [PivotLabelSide::Left, PivotLabelSide::Right])
149 ; {});
150
151 fn name(&self) -> &str {
152 "Intraday Pivot Points"
153 }
154
155 fn desc(&self) -> &str {
156 "Intraday Pivot Points - Support and resistance levels from the previous block of the session"
157 }
158
159 fn calculate(&mut self, data: &[Bar]) {
160 self.update(data, 0);
161 }
162
163 fn update(&mut self, data: &[Bar], from: usize) {
164 let method = self.method;
165 let block_ms = self.minutes as i64 * 60_000;
166 let needs_open = matches!(method, PivotMethod::Woodie | PivotMethod::DM);
167 self.stepper
168 .run(data, from, &mut self.values, |state, bar| {
169 if !(bar.high.is_finite()
170 && bar.low.is_finite()
171 && bar.close.is_finite()
172 && (!needs_open || bar.open.is_finite()))
173 {
174 return IndicatorValue::None;
175 }
176 let day = self.days.day(bar, false);
177 let block = (bar.time - state.origin)
178 .num_milliseconds()
179 .div_euclid(block_ms);
180 match &mut state.current {
181 Some(current) if state.session == Some(day) => {
182 if block != state.block {
183 state.levels = PivotPoints::levels(method, current, bar.open);
184 state.block = block;
185 *current = bar.clone();
186 } else {
187 current.high = current.high.max(bar.high);
188 current.low = current.low.min(bar.low);
189 current.close = bar.close;
190 }
191 }
192 _ => {
193 state.session = Some(day);
194 state.origin = bar.time;
195 state.block = 0;
196 state.current = Some(bar.clone());
197 state.levels = [f64::NAN; 11];
198 }
199 }
200 IndicatorValue::Multiple(state.levels.to_vec())
201 });
202 let options = LevelLabels {
203 names: self.show_labels,
204 prices: self.show_prices,
205 side: self.label_side,
206 precision: self.label_precision.or(self.precision),
207 };
208 refresh_level_labels(&mut self.labels, &self.values, from, options);
209 }
210
211 fn is_incremental(&self) -> bool {
212 true
213 }
214
215 fn set_instrument(&mut self, instrument: Option<&Instrument>) -> bool {
216 let precision = instrument
217 .and_then(crate::scales::instrument_price_precision)
218 .map(usize::from);
219 let changed = precision != self.precision;
220 self.precision = precision;
221 self.days.set(instrument) | changed
222 }
223
224 fn labels(&self) -> &[StudyLabel] {
225 &self.labels
226 }
227
228 fn set_label_precision(&mut self, precision: Option<usize>) {
229 self.label_precision = precision;
230 }
231
232 fn plot_color(&self, line: usize, bar: usize) -> Option<Color32> {
234 let previous = self.values.value(line, bar.checked_sub(1)?)?;
235 (Some(previous) != self.values.value(line, bar)).then_some(Color32::TRANSPARENT)
236 }
237
238 fn values(&self) -> &IndicatorValues {
239 &self.values
240 }
241
242 fn colors(&self) -> Vec<Color32> {
243 self.colors.clone()
244 }
245
246 fn set_colors(&mut self, colors: Vec<Color32>) {
248 for (line, color) in self.colors.iter_mut().zip(colors) {
249 *line = color;
250 }
251 }
252
253 fn is_overlay(&self) -> bool {
254 true
255 }
256
257 fn is_visible(&self) -> bool {
258 self.visible
259 }
260
261 fn set_visible(&mut self, visible: bool) {
262 self.visible = visible;
263 }
264
265 fn clone_box(&self) -> Box<dyn Indicator> {
266 Box::new(self.clone())
267 }
268
269 fn line_names(&self) -> Vec<String> {
270 [
271 "P", "R1", "S1", "R2", "S2", "R3", "S3", "R4", "S4", "R5", "S5",
272 ]
273 .map(String::from)
274 .to_vec()
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281 use chrono::TimeZone;
282
283 #[test]
289 fn levels_come_from_the_previous_block_of_the_session() {
290 let start = Utc.with_ymd_and_hms(2026, 3, 2, 9, 0, 0).unwrap();
291 let next_day = Utc.with_ymd_and_hms(2026, 3, 3, 9, 0, 0).unwrap();
292 let data: Vec<Bar> = [
293 (start, 10.0, 8.0, 9.0),
294 (start + chrono::Duration::minutes(30), 12.0, 9.0, 11.0),
295 (start + chrono::Duration::minutes(60), 11.0, 7.0, 8.0),
296 (start + chrono::Duration::minutes(90), 13.0, 9.0, 12.0),
297 (start + chrono::Duration::minutes(120), 9.0, 8.0, 8.5),
298 (next_day, 9.0, 8.0, 8.5),
299 (next_day + chrono::Duration::minutes(30), 9.5, 8.0, 9.0),
300 (next_day + chrono::Duration::minutes(60), 9.5, 8.0, 9.0),
301 ]
302 .into_iter()
303 .map(|(time, high, low, close)| Bar::new(time, close, high, low, close, 1.0))
304 .collect();
305 let mut pivots = IntradayPivotPoints::new(60);
306 pivots.calculate(&data);
307 let level = |line, bar| pivots.values().value(line, bar);
308 for bar in [0, 1, 5, 6] {
309 assert_eq!(level(0, bar), None, "bar {bar}");
310 }
311 for (line, want) in [31.0 / 3.0, 38.0 / 3.0, 26.0 / 3.0, 43.0 / 3.0, 19.0 / 3.0]
312 .into_iter()
313 .enumerate()
314 {
315 for bar in [2, 3] {
316 assert!(
317 (level(line, bar).unwrap() - want).abs() < 1e-12,
318 "line {line} bar {bar}"
319 );
320 }
321 }
322 assert!((level(0, 4).unwrap() - 32.0 / 3.0).abs() < 1e-12);
323 assert!((level(1, 4).unwrap() - 43.0 / 3.0).abs() < 1e-12);
324 assert!((level(0, 7).unwrap() - (9.5 + 8.0 + 9.0) / 3.0).abs() < 1e-12);
326 }
327}