Skip to main content

egui_charts/studies/builtin/
pivot_points.rs

1//! Pivot Points Standard.
2//!
3//! Support and resistance levels for each calendar period (day, week, month, quarter, year or
4//! several years of exchange trading days) from the open, high, low and close of the previous
5//! period in the data. With H, L, C and O the previous period's high, low, close and open,
6//! R = H - L and P = (H + L + C) / 3:
7//!
8//! | Type        | Levels |
9//! |-------------|--------|
10//! | Traditional | R1 = 2P - L, S1 = 2P - H, R2/S2 = P ± R, R3 = 2P + (H - 2L), S3 = 2P - (2H - L), R4 = 3P + (H - 3L), S4 = 3P - (3H - L), R5 = 4P + (H - 4L), S5 = 4P - (4H - L) |
11//! | Fibonacci   | R1/S1 = P ± 0.382R, R2/S2 = P ± 0.618R, R3/S3 = P ± R |
12//! | Woodie      | P = (H + L + 2 * current period's open) / 4, R1 = 2P - L, S1 = 2P - H, R2/S2 = P ± R, R3 = H + 2(P - L), S3 = L - 2(H - P), R4 = R3 + R, S4 = S3 - R |
13//! | Classic     | R1 = 2P - L, S1 = 2P - H, R2/S2 = P ± R, R3/S3 = P ± 2R, R4/S4 = P ± 3R |
14//! | DM          | X = H + L + 2C if C = O, 2H + L + C if C > O, else H + 2L + C; P = X / 4, R1 = X / 2 - L, S1 = X / 2 - H |
15//! | Camarilla   | R1..R4 / S1..S4 = C ± 1.1R / 12, 6, 4, 2; R5 = (H / L) * C, S5 = C - (R5 - C) |
16//!
17//! A level a type does not define is NaN. The levels after DeMark (DM) and Camarilla follow
18//! Tom DeMark and Nick Stott; the others are the conventional floor-trader pivots.
19//!
20//! # Output
21//!
22//! Eleven lines, `[P, R1, S1, R2, S2, R3, S3, R4, S4, R5, S5]`, constant within a period and
23//! drawn only for the last [`with_pivots_back`](PivotPoints::with_pivots_back) periods. A line
24//! is not drawn across the change from one period to the next. Each level is labelled with its
25//! name, and optionally its price, at its left or right end.
26//!
27//! # Example
28//!
29//! ```rust
30//! # let bars: Vec<egui_charts::model::Bar> = (0..600).map(|i| { let p = 100.0 + (i as f64 * 0.3).sin() * 5.0; egui_charts::model::Bar::new(chrono::DateTime::from_timestamp(i * 3_600, 0).unwrap(), p, p + 1.0, p - 1.0, p + 0.5, 1_000.0) }).collect();
31//! use egui_charts::model::Timeframe;
32//! use egui_charts::studies::{Indicator, PivotMethod, PivotPoints, PivotTimeframe};
33//!
34//! // Weekly Fibonacci pivots of hourly bars, the last five weeks.
35//! let mut pivots = PivotPoints::new()
36//!     .with_method(PivotMethod::Fibonacci)
37//!     .with_timeframe(PivotTimeframe::Weekly)
38//!     .with_pivots_back(5);
39//! pivots.set_timeframe(Timeframe::Hour1);
40//! pivots.calculate(&bars);
41//! assert_eq!(pivots.line_count(), 11);
42//! ```
43
44use super::anchor_period::{AnchorPeriod, ExchangeDays};
45use crate::model::{Bar, Instrument, Timeframe};
46use crate::studies::stepper::Stepper;
47use crate::studies::{
48    Indicator, IndicatorValue, IndicatorValues, LabelPlacement, StudyDataRequest, StudyLabel,
49};
50use chrono::{Datelike, NaiveDate};
51use egui::Color32;
52
53/// The level formulas used by [`PivotPoints`].
54#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
55#[non_exhaustive]
56pub enum PivotMethod {
57    /// Floor-trader pivots with five levels each side.
58    #[default]
59    Traditional,
60    /// Fibonacci ratios 0.382, 0.618 and 1 of the previous range around P.
61    Fibonacci,
62    /// P weighted by the current period's open, four levels each side.
63    Woodie,
64    /// Multiples of the previous range around P, four levels each side.
65    Classic,
66    /// Tom DeMark's pivot, conditional on the previous close against its open: P, R1 and S1.
67    DM,
68    /// Nick Stott's levels around the previous close, five each side.
69    Camarilla,
70}
71
72/// The calendar period of each set of pivots.
73#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
74#[non_exhaustive]
75pub enum PivotTimeframe {
76    /// By chart interval: daily up to 15 minutes, weekly below a day, monthly below a week,
77    /// yearly from a week.
78    #[default]
79    Auto,
80    /// Each trading day.
81    Daily,
82    /// Each ISO week.
83    Weekly,
84    /// Each calendar month.
85    Monthly,
86    /// Each calendar quarter.
87    Quarterly,
88    /// Each calendar year.
89    Yearly,
90    /// Each two calendar years, starting in an even year.
91    TwoYears,
92    /// Each three calendar years, starting in a year divisible by 3.
93    ThreeYears,
94    /// Each five calendar years, starting in a year divisible by 5.
95    FiveYears,
96    /// Each decade, 2020 to 2029 for example.
97    TenYears,
98}
99
100/// Which end of each pivot level its label sits at.
101#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
102#[non_exhaustive]
103pub enum PivotLabelSide {
104    /// At the start of the level, above its line.
105    #[default]
106    Left,
107    /// At the end of the level, above its line.
108    Right,
109}
110
111const LEVEL_NAMES: [&str; 11] = [
112    "P", "R1", "S1", "R2", "S2", "R3", "S3", "R4", "S4", "R5", "S5",
113];
114
115/// What the level labels of the pivot studies show.
116#[derive(Clone, Copy)]
117pub(super) struct LevelLabels {
118    pub names: bool,
119    pub prices: bool,
120    pub side: PivotLabelSide,
121    pub precision: Option<usize>,
122}
123
124impl LevelLabels {
125    fn text(self, line: usize, price: f64) -> String {
126        let name = LEVEL_NAMES[line];
127        let price = || crate::scales::StudyFormat::Price.format(price, self.precision);
128        match (self.names, self.prices) {
129            (true, true) => format!("{name} ({})", price()),
130            (true, false) => name.to_owned(),
131            _ => price(),
132        }
133    }
134}
135
136/// Rewrites `labels` from the bar before `from` on, given that `values[..from]` is unchanged. A
137/// label depends only on its own bar and the bars either side, so the earlier ones still hold.
138pub(super) fn refresh_level_labels(
139    labels: &mut Vec<StudyLabel>,
140    values: &IndicatorValues,
141    from: usize,
142    options: LevelLabels,
143) {
144    if !options.names && !options.prices {
145        labels.clear();
146        return;
147    }
148    let len = values.len();
149    let first = from.min(len).saturating_sub(1);
150    labels.truncate(labels.partition_point(|label| label.bar < first));
151    for bar in first..len {
152        for (line, _) in LEVEL_NAMES.iter().enumerate() {
153            let Some(price) = values.value(line, bar) else {
154                continue;
155            };
156            let (neighbour, placement) = match options.side {
157                PivotLabelSide::Left => (bar.checked_sub(1), LabelPlacement::AboveRight),
158                PivotLabelSide::Right => (
159                    Some(bar + 1).filter(|&next| next < len),
160                    LabelPlacement::AboveLeft,
161                ),
162            };
163            if neighbour.and_then(|at| values.value(line, at)) != Some(price) {
164                labels.push(StudyLabel {
165                    bar,
166                    price,
167                    text: options.text(line, price),
168                    placement,
169                    plot: line,
170                });
171            }
172        }
173    }
174}
175
176/// Pivot Points Standard: support and resistance levels for each calendar period from the
177/// previous period.
178///
179/// Periods are counted in exchange trading days, as
180/// [`VolumeWeightedAvgPrice`](super::VolumeWeightedAvgPrice) counts them. The chart supplies
181/// its interval for [`PivotTimeframe::Auto`] and the instrument's time zone and session;
182/// direct callers set them with [`Indicator::set_timeframe`] (one minute until then) and
183/// [`Indicator::set_instrument`] (UTC until then). The levels come from the chart's own bars, or
184/// with [`with_daily_bars`](Self::with_daily_bars) from the feed's daily bars. Overlay indicator.
185#[derive(Clone)]
186pub struct PivotPoints {
187    method: PivotMethod,
188    timeframe: PivotTimeframe,
189    pivots_back: usize,
190    interval: Timeframe,
191    days: ExchangeDays,
192    values: IndicatorValues,
193    stepper: Stepper<State>,
194    intraday: bool,
195    shown_start: usize,
196    /// One per line: P, then R1, S1 through R5, S5.
197    colors: Vec<Color32>,
198    visible: bool,
199    show_labels: bool,
200    show_prices: bool,
201    label_side: PivotLabelSide,
202    daily_bars: bool,
203    /// The instrument's decimals and those of the study's presentation settings.
204    precision: Option<usize>,
205    label_precision: Option<usize>,
206    /// The feed's daily bars by period, once the host has supplied them.
207    daily: Option<Vec<((i32, u32), Bar)>>,
208    labels: Vec<StudyLabel>,
209}
210
211#[derive(Clone)]
212struct Period {
213    key: (i32, u32),
214    start: usize,
215    ohlc: Bar,
216    levels: [f64; 11],
217}
218
219#[derive(Clone, Default)]
220struct State {
221    index: usize,
222    previous_date: Option<chrono::NaiveDate>,
223    intraday: bool,
224    periods: std::collections::VecDeque<Period>,
225}
226
227impl PivotPoints {
228    /// Traditional pivots, timeframe Auto, for the last 15 periods.
229    pub fn new() -> Self {
230        // The pivot and every resistance and support level share one colour.
231        let colors = vec![crate::studies::palette::DARK_ORANGE; 11];
232        Self {
233            method: PivotMethod::Traditional,
234            timeframe: PivotTimeframe::Auto,
235            pivots_back: 15,
236            interval: Timeframe::Min1,
237            days: ExchangeDays::default(),
238            values: IndicatorValues::new(),
239            stepper: Stepper::default(),
240            intraday: false,
241            shown_start: 0,
242            colors,
243            visible: true,
244            show_labels: true,
245            show_prices: false,
246            label_side: PivotLabelSide::Left,
247            daily_bars: false,
248            precision: None,
249            label_precision: None,
250            daily: None,
251            labels: Vec::new(),
252        }
253    }
254
255    /// The level formulas.
256    pub fn with_method(mut self, method: PivotMethod) -> Self {
257        self.method = method;
258        self
259    }
260
261    /// The calendar period of each set of pivots.
262    pub fn with_timeframe(mut self, timeframe: PivotTimeframe) -> Self {
263        self.timeframe = timeframe;
264        self
265    }
266
267    /// The number of most recent periods that show pivots, raised to 1 or more.
268    pub fn with_pivots_back(mut self, pivots_back: usize) -> Self {
269        self.pivots_back = pivots_back.max(1);
270        self
271    }
272
273    /// Sets the colors line by line, as [`Indicator::set_colors`] does.
274    pub fn with_colors(mut self, colors: Vec<Color32>) -> Self {
275        self.set_colors(colors);
276        self
277    }
278
279    /// Whether each level is labelled with its name (P, R1, S1 and so on) and with its price.
280    /// Names only by default.
281    pub fn with_labels(mut self, names: bool, prices: bool) -> Self {
282        self.show_labels = names;
283        self.show_prices = prices;
284        self
285    }
286
287    /// The end of each level its label sits at; the left end by default.
288    pub fn with_label_side(mut self, side: PivotLabelSide) -> Self {
289        self.label_side = side;
290        self
291    }
292
293    /// On intervals shorter than a day, takes each previous period's high, low and close (and,
294    /// for the methods that use it, the period's open) from the feed's daily bars, requested
295    /// through [`Indicator::data_requests`] and passed to [`Indicator::update_data`]. Without
296    /// daily bars from the host there are no levels. `calculate` and `update` use the chart's
297    /// own bars.
298    pub fn with_daily_bars(mut self, daily: bool) -> Self {
299        self.daily_bars = daily;
300        self
301    }
302
303    /// `None` for the periods of several years that [`AnchorPeriod`] does not hold.
304    fn period(&self) -> Option<AnchorPeriod> {
305        Some(match self.timeframe {
306            PivotTimeframe::Auto => {
307                let ms = self.interval.duration_ms();
308                if ms <= Timeframe::Min15.duration_ms() {
309                    AnchorPeriod::Session
310                } else if ms < Timeframe::Day1.duration_ms() {
311                    AnchorPeriod::Week
312                } else if ms < Timeframe::Week1.duration_ms() {
313                    AnchorPeriod::Month
314                } else {
315                    AnchorPeriod::Year
316                }
317            }
318            PivotTimeframe::Daily => AnchorPeriod::Session,
319            PivotTimeframe::Weekly => AnchorPeriod::Week,
320            PivotTimeframe::Monthly => AnchorPeriod::Month,
321            PivotTimeframe::Quarterly => AnchorPeriod::Quarter,
322            PivotTimeframe::Yearly => AnchorPeriod::Year,
323            PivotTimeframe::TenYears => AnchorPeriod::Decade,
324            PivotTimeframe::TwoYears | PivotTimeframe::ThreeYears | PivotTimeframe::FiveYears => {
325                return None;
326            }
327        })
328    }
329
330    /// Identifies the period that holds trading day `day`. Periods of several years start in a
331    /// year divisible by their length, so they do not move when older history loads.
332    fn key(period: Option<AnchorPeriod>, timeframe: PivotTimeframe, day: NaiveDate) -> (i32, u32) {
333        match (period, timeframe) {
334            (Some(period), _) => period.key(day),
335            (None, PivotTimeframe::TwoYears) => (day.year().div_euclid(2), 0),
336            (None, PivotTimeframe::ThreeYears) => (day.year().div_euclid(3), 0),
337            (None, _) => (day.year().div_euclid(5), 0),
338        }
339    }
340
341    fn usable(method: PivotMethod, bar: &Bar) -> bool {
342        bar.high.is_finite()
343            && bar.low.is_finite()
344            && bar.close.is_finite()
345            && (!matches!(method, PivotMethod::Woodie | PivotMethod::DM) || bar.open.is_finite())
346    }
347
348    fn label_options(&self) -> LevelLabels {
349        LevelLabels {
350            names: self.show_labels,
351            prices: self.show_prices,
352            side: self.label_side,
353            precision: self.label_precision.or(self.precision),
354        }
355    }
356
357    /// Labels the projected `levels`, one per finite level, at the chosen end of `bars`.
358    pub(super) fn label_projection(&mut self, bars: std::ops::Range<usize>, levels: &[f64; 11]) {
359        let options = self.label_options();
360        if !options.names && !options.prices {
361            return;
362        }
363        let (bar, placement) = match options.side {
364            PivotLabelSide::Left => (bars.start, LabelPlacement::AboveRight),
365            PivotLabelSide::Right => (bars.end, LabelPlacement::AboveLeft),
366        };
367        self.labels.extend(
368            levels
369                .iter()
370                .enumerate()
371                .filter(|(_, price)| price.is_finite())
372                .map(|(plot, &price)| StudyLabel {
373                    bar,
374                    price,
375                    text: options.text(plot, price),
376                    placement,
377                    plot,
378                }),
379        );
380    }
381
382    /// The daily bars grouped by period: the first bar of each with the running high, low and
383    /// close. Daily bars keep their UTC date, as a chart of daily bars does.
384    fn daily_periods(&self, bars: &[Bar]) -> Vec<((i32, u32), Bar)> {
385        let (period, timeframe) = (self.period(), self.timeframe);
386        let mut periods: Vec<((i32, u32), Bar)> = Vec::new();
387        for bar in bars.iter().filter(|bar| Self::usable(self.method, bar)) {
388            let key = Self::key(period, timeframe, bar.time.date_naive());
389            match periods.last_mut() {
390                Some((last, current)) if *last == key => {
391                    current.high = current.high.max(bar.high);
392                    current.low = current.low.min(bar.low);
393                    current.close = bar.close;
394                }
395                _ => periods.push((key, bar.clone())),
396            }
397        }
398        periods
399    }
400
401    /// Whether `new` differs from the daily periods that computed the levels so far: the open of
402    /// each period up to the latest and the rest of the earlier ones. A live tick on today's
403    /// daily bar changes neither, so it does not force a full recalculation.
404    fn daily_levels_changed(&self, new: &[((i32, u32), Bar)]) -> bool {
405        let Some(old) = &self.daily else {
406            return true;
407        };
408        let Some(latest) = self.stepper.state().periods.back().map(|period| period.key) else {
409            return false;
410        };
411        let used = |periods: &[((i32, u32), Bar)]| periods.partition_point(|(k, _)| *k <= latest);
412        let same = |a: f64, b: f64| a.to_bits() == b.to_bits();
413        used(old) != used(new)
414            || old[..used(old)]
415                .iter()
416                .zip(new)
417                .any(|((key, a), (new_key, b))| {
418                    key != new_key
419                        || !same(a.open, b.open)
420                        || (*key < latest
421                            && !(same(a.high, b.high)
422                                && same(a.low, b.low)
423                                && same(a.close, b.close)))
424                })
425    }
426
427    fn run(&mut self, data: &[Bar], mut from: usize) {
428        let (period, timeframe) = (self.period(), self.timeframe);
429        let method = self.method;
430        let entry_shown = self.shown_start;
431        let len = self.values.len();
432        if len == 0 || from < len - 1 || data.len() < len {
433            self.shown_start = 0;
434        }
435        loop {
436            self.stepper
437                .run(data, from, &mut self.values, |state, bar| {
438                    let index = state.index;
439                    state.index += 1;
440                    if !Self::usable(method, bar) {
441                        return IndicatorValue::None;
442                    }
443                    let date = bar.time.date_naive();
444                    state.intraday |= state.previous_date == Some(date);
445                    state.previous_date = Some(date);
446                    let key = Self::key(period, timeframe, self.days.day(bar, !self.intraday));
447                    if state.periods.back().is_none_or(|last| last.key != key) {
448                        let levels = match &self.daily {
449                            Some(daily) => {
450                                let at = daily.partition_point(|(k, _)| *k < key);
451                                let open = daily
452                                    .get(at)
453                                    .filter(|(k, _)| *k == key)
454                                    .map_or(bar.open, |(_, period)| period.open);
455                                at.checked_sub(1).map_or([f64::NAN; 11], |previous| {
456                                    Self::levels(method, &daily[previous].1, open)
457                                })
458                            }
459                            None => state.periods.back().map_or([f64::NAN; 11], |last| {
460                                Self::levels(method, &last.ohlc, bar.open)
461                            }),
462                        };
463                        state.periods.push_back(Period {
464                            key,
465                            start: index,
466                            ohlc: bar.clone(),
467                            levels,
468                        });
469                        if state.periods.len() > self.pivots_back {
470                            state.periods.pop_front();
471                        }
472                    } else {
473                        let current = state.periods.back_mut().unwrap();
474                        current.ohlc.high = current.ohlc.high.max(bar.high);
475                        current.ohlc.low = current.ohlc.low.min(bar.low);
476                        current.ohlc.close = bar.close;
477                    }
478                    IndicatorValue::Multiple(state.periods.back().unwrap().levels.to_vec())
479                });
480            if self.intraday == self.stepper.state().intraday {
481                break;
482            }
483            self.intraday = self.stepper.state().intraday;
484            self.shown_start = 0;
485            from = 0;
486        }
487        let periods = &self.stepper.state().periods;
488        let start = periods.front().map_or(data.len(), |period| period.start);
489        if start > self.shown_start {
490            for bar in self.shown_start..start {
491                self.values.set(bar, IndicatorValue::None);
492            }
493        } else if start < self.shown_start {
494            // Withdrawing a new period reveals the one that its display limit had expired.
495            for (at, period) in periods.iter().enumerate() {
496                let end = periods
497                    .get(at + 1)
498                    .map_or(data.len(), |next| next.start)
499                    .min(self.shown_start);
500                for (bar, source) in data.iter().enumerate().take(end).skip(period.start) {
501                    if Self::usable(method, source) {
502                        self.values
503                            .set(bar, IndicatorValue::Multiple(period.levels.to_vec()));
504                    }
505                }
506            }
507        }
508        // The expiry or the return of a period changes values before `from`.
509        let unchanged = if start != entry_shown { 0 } else { from };
510        self.shown_start = start;
511        let options = self.label_options();
512        refresh_level_labels(&mut self.labels, &self.values, unchanged, options);
513    }
514
515    /// The method and the latest period so far: its open, running high and low, latest close,
516    /// and first bar. `None` before a usable bar.
517    pub(super) fn latest_period(&self) -> Option<(PivotMethod, &Bar, usize)> {
518        let period = self.stepper.state().periods.back()?;
519        Some((self.method, &period.ohlc, period.start))
520    }
521
522    /// `[P, R1, S1, ..., R5, S5]` from the previous period and the current period's open.
523    pub(super) fn levels(method: PivotMethod, prev: &Bar, open: f64) -> [f64; 11] {
524        let (h, l, c) = (prev.high, prev.low, prev.close);
525        let r = h - l;
526        let p = (h + l + c) / 3.0;
527        let nan = f64::NAN;
528        match method {
529            PivotMethod::Traditional => [
530                p,
531                2.0 * p - l,
532                2.0 * p - h,
533                p + r,
534                p - r,
535                2.0 * p + (h - 2.0 * l),
536                2.0 * p - (2.0 * h - l),
537                3.0 * p + (h - 3.0 * l),
538                3.0 * p - (3.0 * h - l),
539                4.0 * p + (h - 4.0 * l),
540                4.0 * p - (4.0 * h - l),
541            ],
542            PivotMethod::Fibonacci => [
543                p,
544                p + 0.382 * r,
545                p - 0.382 * r,
546                p + 0.618 * r,
547                p - 0.618 * r,
548                p + r,
549                p - r,
550                nan,
551                nan,
552                nan,
553                nan,
554            ],
555            PivotMethod::Woodie => {
556                let p = (h + l + 2.0 * open) / 4.0;
557                let (r3, s3) = (h + 2.0 * (p - l), l - 2.0 * (h - p));
558                [
559                    p,
560                    2.0 * p - l,
561                    2.0 * p - h,
562                    p + r,
563                    p - r,
564                    r3,
565                    s3,
566                    r3 + r,
567                    s3 - r,
568                    nan,
569                    nan,
570                ]
571            }
572            PivotMethod::Classic => [
573                p,
574                2.0 * p - l,
575                2.0 * p - h,
576                p + r,
577                p - r,
578                p + 2.0 * r,
579                p - 2.0 * r,
580                p + 3.0 * r,
581                p - 3.0 * r,
582                nan,
583                nan,
584            ],
585            PivotMethod::DM => {
586                let x = if c == prev.open {
587                    h + l + 2.0 * c
588                } else if c > prev.open {
589                    2.0 * h + l + c
590                } else {
591                    h + 2.0 * l + c
592                };
593                [
594                    x / 4.0,
595                    x / 2.0 - l,
596                    x / 2.0 - h,
597                    nan,
598                    nan,
599                    nan,
600                    nan,
601                    nan,
602                    nan,
603                    nan,
604                    nan,
605                ]
606            }
607            PivotMethod::Camarilla => {
608                let k = 1.1 * r;
609                let r5 = h / l * c;
610                [
611                    p,
612                    c + k / 12.0,
613                    c - k / 12.0,
614                    c + k / 6.0,
615                    c - k / 6.0,
616                    c + k / 4.0,
617                    c - k / 4.0,
618                    c + k / 2.0,
619                    c - k / 2.0,
620                    r5,
621                    c - (r5 - c),
622                ]
623            }
624        }
625    }
626}
627
628/// Construct with the conventional default parameters.
629impl Default for PivotPoints {
630    fn default() -> Self {
631        Self::new()
632    }
633}
634
635impl Indicator for PivotPoints {
636    crate::studies::inputs::study_inputs!(self;
637        method: choice("Type", [PivotMethod::Traditional, PivotMethod::Fibonacci, PivotMethod::Woodie, PivotMethod::Classic, PivotMethod::DM, PivotMethod::Camarilla]),
638        timeframe: choice("Pivots Timeframe", [PivotTimeframe::Auto, PivotTimeframe::Daily, PivotTimeframe::Weekly, PivotTimeframe::Monthly, PivotTimeframe::Quarterly, PivotTimeframe::Yearly, PivotTimeframe::TwoYears, PivotTimeframe::ThreeYears, PivotTimeframe::FiveYears, PivotTimeframe::TenYears]),
639        pivots_back: length("Number of Pivots Back", 1, usize::MAX),
640        show_labels: toggle("Show Labels"),
641        show_prices: toggle("Show Prices"),
642        label_side: choice("Labels Position", [PivotLabelSide::Left, PivotLabelSide::Right]),
643        daily_bars: toggle("Use Daily Bars")
644    ; {});
645
646    fn name(&self) -> &str {
647        "Pivot Points Standard"
648    }
649
650    fn desc(&self) -> &str {
651        "Pivot Points Standard - Support and resistance levels from the previous calendar period"
652    }
653
654    fn calculate(&mut self, data: &[Bar]) {
655        self.update(data, 0);
656    }
657
658    fn update(&mut self, data: &[Bar], from: usize) {
659        let from = if self.daily.take().is_some() { 0 } else { from };
660        self.run(data, from);
661    }
662
663    fn data_requests(&self) -> Vec<StudyDataRequest> {
664        if self.daily_bars && self.interval.duration_ms() < Timeframe::Day1.duration_ms() {
665            vec![StudyDataRequest {
666                symbol: String::new(),
667                timeframe: Some(Timeframe::Day1),
668                metric: None,
669            }]
670        } else {
671            Vec::new()
672        }
673    }
674
675    fn update_data(
676        &mut self,
677        data: &[Bar],
678        from: usize,
679        sources: &[crate::studies::StudyData<'_>],
680    ) {
681        let requests = self.data_requests();
682        let Some(request) = requests.first() else {
683            return self.update(data, from);
684        };
685        let bars = sources
686            .iter()
687            .find(|source| source.request == *request)
688            .map_or(&[][..], |source| source.bars);
689        let daily = self.daily_periods(bars);
690        let from = if self.daily_levels_changed(&daily) {
691            0
692        } else {
693            from
694        };
695        self.daily = Some(daily);
696        self.run(data, from);
697    }
698
699    fn is_incremental(&self) -> bool {
700        true
701    }
702
703    fn set_timeframe(&mut self, timeframe: Timeframe) {
704        self.interval = timeframe;
705    }
706
707    fn set_instrument(&mut self, instrument: Option<&Instrument>) -> bool {
708        let precision = instrument
709            .and_then(crate::scales::instrument_price_precision)
710            .map(usize::from);
711        let changed = precision != self.precision;
712        self.precision = precision;
713        self.days.set(instrument) | changed
714    }
715
716    fn labels(&self) -> &[StudyLabel] {
717        &self.labels
718    }
719
720    fn set_label_precision(&mut self, precision: Option<usize>) {
721        self.label_precision = precision;
722    }
723
724    /// Transparent on the first bar of a period, so the change of level is not drawn.
725    fn plot_color(&self, line: usize, bar: usize) -> Option<Color32> {
726        let previous = self.values.value(line, bar.checked_sub(1)?)?;
727        (Some(previous) != self.values.value(line, bar)).then_some(Color32::TRANSPARENT)
728    }
729
730    fn values(&self) -> &IndicatorValues {
731        &self.values
732    }
733
734    fn colors(&self) -> Vec<Color32> {
735        self.colors.clone()
736    }
737
738    /// Sets the colors line by line; lines without a color keep theirs.
739    fn set_colors(&mut self, colors: Vec<Color32>) {
740        for (line, color) in self.colors.iter_mut().zip(colors) {
741            *line = color;
742        }
743    }
744
745    fn is_overlay(&self) -> bool {
746        true // Drawn on price chart
747    }
748
749    fn is_visible(&self) -> bool {
750        self.visible
751    }
752
753    fn set_visible(&mut self, visible: bool) {
754        self.visible = visible;
755    }
756
757    fn clone_box(&self) -> Box<dyn Indicator> {
758        Box::new(self.clone())
759    }
760
761    fn line_names(&self) -> Vec<String> {
762        LEVEL_NAMES.map(String::from).to_vec()
763    }
764}
765
766#[cfg(test)]
767mod tests {
768    use super::*;
769    use chrono::{TimeZone, Utc};
770
771    /// Auto takes the session up to 15 minutes, the week below a day, the month below a week
772    /// and the year from a week up.
773    #[test]
774    fn auto_follows_the_chart_interval() {
775        let mut pivots = PivotPoints::new();
776        for (interval, period) in [
777            (Timeframe::Min15, AnchorPeriod::Session),
778            (Timeframe::Min30, AnchorPeriod::Week),
779            (Timeframe::Hour4, AnchorPeriod::Week),
780            (Timeframe::Day1, AnchorPeriod::Month),
781            (Timeframe::Week1, AnchorPeriod::Year),
782        ] {
783            pivots.set_timeframe(interval);
784            assert_eq!(pivots.period(), Some(period), "{interval:?}");
785        }
786    }
787
788    /// Hourly bars of three days, as (high, low, close). Day 2 takes H 12, L 8, C 11 from day 1:
789    /// P 31/3, R1 38/3, S1 26/3, R2 43/3, S2 19/3, R3 50/3, S3 14/3, R4 19, S4 3, R5 64/3, S5 4/3.
790    /// Day 3 takes H 14, L 9, C 12 from day 2: P 35/3.
791    fn three_days() -> Vec<Bar> {
792        let hour = |day, h| Utc.with_ymd_and_hms(2026, 3, day, h, 0, 0).unwrap();
793        [
794            (hour(2, 9), 10.0, 8.0, 9.0),
795            (hour(2, 10), 12.0, 9.0, 11.0),
796            (hour(3, 9), 11.0, 9.0, 10.0),
797            (hour(3, 10), 14.0, 10.0, 12.0),
798            (hour(4, 9), 12.0, 10.0, 11.0),
799        ]
800        .into_iter()
801        .map(|(time, high, low, close)| Bar::new(time, close, high, low, close, 1.0))
802        .collect()
803    }
804
805    #[test]
806    fn every_defined_level_is_labelled_at_the_chosen_end_of_its_period() {
807        let daily = || PivotPoints::new().with_timeframe(PivotTimeframe::Daily);
808        let names = [
809            "P", "R1", "S1", "R2", "S2", "R3", "S3", "R4", "S4", "R5", "S5",
810        ];
811        let priced = [
812            "P (10.33)",
813            "R1 (12.67)",
814            "S1 (8.67)",
815            "R2 (14.33)",
816            "S2 (6.33)",
817            "R3 (16.67)",
818            "S3 (4.67)",
819            "R4 (19.00)",
820            "S4 (3.00)",
821            "R5 (21.33)",
822            "S5 (1.33)",
823        ];
824        let mut tick = daily().with_labels(true, true);
825        tick.set_instrument(Some(
826            &crate::model::Instrument::new("X").with_tick_size(0.001),
827        ));
828        // (name, study, bars of the labels, placement, texts of the first period's labels)
829        type Case<'a> = (
830            &'a str,
831            Box<dyn Indicator>,
832            &'a [usize],
833            LabelPlacement,
834            Vec<&'a str>,
835        );
836        let mut styled =
837            crate::studies::ConfiguredStudy::new(Box::new(daily().with_labels(false, true)));
838        styled.set_instrument(Some(
839            &crate::model::Instrument::new("X").with_tick_size(0.001),
840        ));
841        let mut settings = styled.settings();
842        settings.precision = Some(4);
843        styled.apply_settings(&[], settings).unwrap();
844        let cases: Vec<Case> = vec![
845            (
846                "defaults",
847                Box::new(daily()),
848                &[2, 4],
849                LabelPlacement::AboveRight,
850                names.to_vec(),
851            ),
852            (
853                "prices at the right end",
854                Box::new(
855                    daily()
856                        .with_labels(true, true)
857                        .with_label_side(PivotLabelSide::Right),
858                ),
859                &[3, 4],
860                LabelPlacement::AboveLeft,
861                priced.to_vec(),
862            ),
863            (
864                "prices only",
865                Box::new(daily().with_labels(false, true)),
866                &[2, 4],
867                LabelPlacement::AboveRight,
868                vec![
869                    "10.33", "12.67", "8.67", "14.33", "6.33", "16.67", "4.67", "19.00", "3.00",
870                    "21.33", "1.33",
871                ],
872            ),
873            (
874                "seven Fibonacci levels",
875                Box::new(daily().with_method(PivotMethod::Fibonacci)),
876                &[2, 4],
877                LabelPlacement::AboveRight,
878                names[..7].to_vec(),
879            ),
880            (
881                "expired period",
882                Box::new(daily().with_pivots_back(1)),
883                &[4],
884                LabelPlacement::AboveRight,
885                names.to_vec(),
886            ),
887            (
888                "instrument decimals",
889                Box::new(tick),
890                &[2, 4],
891                LabelPlacement::AboveRight,
892                vec![
893                    "P (10.333)",
894                    "R1 (12.667)",
895                    "S1 (8.667)",
896                    "R2 (14.333)",
897                    "S2 (6.333)",
898                    "R3 (16.667)",
899                    "S3 (4.667)",
900                    "R4 (19.000)",
901                    "S4 (3.000)",
902                    "R5 (21.333)",
903                    "S5 (1.333)",
904                ],
905            ),
906            (
907                "style precision over instrument decimals",
908                Box::new(styled),
909                &[2, 4],
910                LabelPlacement::AboveRight,
911                vec![
912                    "10.3333", "12.6667", "8.6667", "14.3333", "6.3333", "16.6667", "4.6667",
913                    "19.0000", "3.0000", "21.3333", "1.3333",
914                ],
915            ),
916        ];
917        for (name, mut study, bars, placement, texts) in cases {
918            study.calculate(&three_days());
919            let labels = study.labels();
920            let per_period = texts.len();
921            assert_eq!(labels.len(), per_period * bars.len(), "{name}");
922            for (at, label) in labels.iter().enumerate() {
923                let bar = bars[at / per_period];
924                let plot = at % per_period;
925                assert_eq!(
926                    (label.bar, label.plot, label.placement),
927                    (bar, plot, placement),
928                    "{name}"
929                );
930                assert_eq!(
931                    label.price,
932                    study.values().value(plot, bar).unwrap(),
933                    "{name}"
934                );
935            }
936            let first: Vec<_> = labels[..per_period]
937                .iter()
938                .map(|l| l.text.as_str())
939                .collect();
940            assert_eq!(first, texts, "{name}");
941        }
942
943        let mut off = daily().with_labels(false, false);
944        off.calculate(&three_days());
945        assert!(off.labels().is_empty());
946        let mut right = daily()
947            .with_labels(true, true)
948            .with_label_side(PivotLabelSide::Right);
949        right.calculate(&three_days());
950        assert_eq!(right.labels()[11].text, "P (11.67)");
951    }
952
953    /// One bar on 2 January of each year 2014 to 2025, with `k = year - 2014`: high 100 + k, low
954    /// 90 + k, close 95 + k. Each row gives the block before the one holding 2020, its H, L, C and
955    /// from them P = (H + L + C) / 3 and R1 = 2P - L.
956    #[test]
957    fn periods_of_several_years_start_in_years_divisible_by_their_length() {
958        let bars: Vec<Bar> = (2014..=2025)
959            .map(|year| {
960                let k = f64::from(year - 2014);
961                let time = Utc.with_ymd_and_hms(year, 1, 2, 0, 0, 0).unwrap();
962                Bar::new(time, 95.0 + k, 100.0 + k, 90.0 + k, 95.0 + k, 1.0)
963            })
964            .collect();
965        for (timeframe, p, r1) in [
966            // 2018-2019: 105, 94, 100.
967            (PivotTimeframe::TwoYears, 299.0 / 3.0, 316.0 / 3.0),
968            // 2016-2018: 104, 92, 99.
969            (PivotTimeframe::ThreeYears, 295.0 / 3.0, 314.0 / 3.0),
970            // 2015-2019: 105, 91, 100.
971            (PivotTimeframe::FiveYears, 296.0 / 3.0, 319.0 / 3.0),
972            // 2014-2019: 105, 90, 100.
973            (PivotTimeframe::TenYears, 295.0 / 3.0, 320.0 / 3.0),
974        ] {
975            let mut pivots = PivotPoints::new().with_timeframe(timeframe);
976            pivots.calculate(&bars);
977            let level = |line| pivots.values().value(line, 6).unwrap();
978            assert!((level(0) - p).abs() < 1e-9, "{timeframe:?} P {}", level(0));
979            assert!(
980                (level(1) - r1).abs() < 1e-9,
981                "{timeframe:?} R1 {}",
982                level(1)
983            );
984        }
985    }
986
987    /// The daily bars of 2 to 6 March have high 13, low 7 and close 11, so the week of 9 March has
988    /// P = 31/3, R1 = 2P - L = 41/3 and S1 = 2P - H = 23/3, whatever its hourly bars and whether
989    /// the chart holds that earlier week at all.
990    #[test]
991    fn daily_bars_supply_the_previous_period_of_an_intraday_chart() {
992        let day = |d| Utc.with_ymd_and_hms(2026, 3, d, 0, 0, 0).unwrap();
993        let daily: Vec<Bar> = [
994            (2, 10.0, 8.0, 9.0),
995            (3, 12.0, 9.0, 11.0),
996            (4, 11.0, 7.0, 8.0),
997            (5, 13.0, 9.0, 12.0),
998            (6, 12.0, 10.0, 11.0),
999            (9, 60.0, 30.0, 45.0),
1000            (10, 60.0, 30.0, 45.0),
1001        ]
1002        .into_iter()
1003        .map(|(d, high, low, close)| Bar::new(day(d), close, high, low, close, 1.0))
1004        .collect();
1005        let hourly: Vec<Bar> = (10..13)
1006            .map(|h| {
1007                let time = Utc.with_ymd_and_hms(2026, 3, 9, h, 0, 0).unwrap();
1008                Bar::new(time, 45.0, 50.0, 40.0, 45.0, 1.0)
1009            })
1010            .collect();
1011        let study = || {
1012            let mut pivots = PivotPoints::new().with_timeframe(PivotTimeframe::Weekly);
1013            pivots.set_timeframe(Timeframe::Hour1);
1014            pivots
1015        };
1016        let mut pivots = study().with_daily_bars(true);
1017        let requests = pivots.data_requests();
1018        assert_eq!(requests.len(), 1);
1019        assert_eq!(requests[0].timeframe, Some(Timeframe::Day1));
1020        pivots.update_data(
1021            &hourly,
1022            0,
1023            &[crate::studies::StudyData {
1024                request: requests[0].clone(),
1025                bars: &daily,
1026                clock: crate::model::BarClock::new(Timeframe::Day1),
1027                instrument: None,
1028                timeframe: Timeframe::Day1,
1029            }],
1030        );
1031        let mut on_daily = PivotPoints::new().with_timeframe(PivotTimeframe::Weekly);
1032        on_daily.set_timeframe(Timeframe::Day1);
1033        on_daily.calculate(&daily);
1034        for bar in 0..3 {
1035            for (line, want) in [(0, 31.0 / 3.0), (1, 41.0 / 3.0), (2, 23.0 / 3.0)] {
1036                let got = pivots.values().value(line, bar).unwrap();
1037                assert!((got - want).abs() < 1e-9, "line {line} bar {bar}: {got}");
1038            }
1039            for line in 0..11 {
1040                assert_eq!(
1041                    pivots.values().value(line, bar),
1042                    on_daily.values().value(line, 5),
1043                    "line {line} bar {bar}"
1044                );
1045            }
1046        }
1047        // A tick on the newest daily bar keeps the levels; a corrected earlier week moves them.
1048        let mut ticked = daily.clone();
1049        ticked[6].close = 44.0;
1050        let mut corrected = daily.clone();
1051        corrected[0].low = 1.0;
1052        for (series, p) in [(&ticked, 31.0 / 3.0), (&corrected, 25.0 / 3.0)] {
1053            pivots.update_data(
1054                &hourly,
1055                3,
1056                &[crate::studies::StudyData {
1057                    request: requests[0].clone(),
1058                    bars: series,
1059                    clock: crate::model::BarClock::new(Timeframe::Day1),
1060                    instrument: None,
1061                    timeframe: Timeframe::Day1,
1062                }],
1063            );
1064            assert!((pivots.values().value(0, 2).unwrap() - p).abs() < 1e-9);
1065        }
1066
1067        // Without the option the first period has no levels; without daily bars from the host
1068        // chart prices do not stand in.
1069        let mut own = study();
1070        assert!(own.data_requests().is_empty());
1071        own.calculate(&hourly);
1072        let mut missing = study().with_daily_bars(true);
1073        missing.update_data(&hourly, 0, &[]);
1074        for study in [&own, &missing] {
1075            assert!((0..3).all(|bar| study.values().value(0, bar).is_none()));
1076        }
1077    }
1078}