Skip to main content

egui_charts/studies/builtin/
projected_pivot_points.rs

1//! Projected Pivot Points.
2//!
3//! The pivot levels the next period will use if the current one ended now. The confirmed levels
4//! are those of [`PivotPoints`](super::PivotPoints): lines, constant within a period, from the
5//! previous period. The projected levels are drawn beyond the last bar, from the period so far.
6//!
7//! # Construction
8//!
9//! With H, L and C the high, low and latest close of the period so far, the projected levels are
10//! those of the chosen [`PivotMethod`] for a previous period of high H, low L and close C, and,
11//! for the methods that use the open of the previous period, its open. The methods that weight
12//! the open of the period being projected (Woodie) take C as its open, the latest price known.
13//! Each projected level is a box from the last bar as wide as the period so far has bars, so the
14//! projection is state-distinct from the confirmed lines: it moves with every bar until the
15//! period ends, and its levels become the confirmed lines of the next period. A level the
16//! method does not define is absent. Confirmed and projected levels carry the labels of Pivot
17//! Points Standard. Overlay indicator with the inputs of Pivot Points Standard.
18//!
19//! # Example
20//!
21//! ```rust
22//! use chrono::{TimeZone, Utc};
23//! use egui_charts::model::Bar;
24//! use egui_charts::studies::{Indicator, PivotTimeframe, ProjectedPivotPoints};
25//!
26//! // Two hourly bars of one day: P = (12 + 8 + 11) / 3 for the next day.
27//! let bars: Vec<Bar> = [(10.0, 8.0, 9.0), (12.0, 9.0, 11.0)]
28//!     .into_iter()
29//!     .enumerate()
30//!     .map(|(i, (high, low, close))| {
31//!         let time = Utc.with_ymd_and_hms(2026, 3, 2, 9 + i as u32, 0, 0).unwrap();
32//!         Bar::new(time, close, high, low, close, 1.0)
33//!     })
34//!     .collect();
35//! let mut pivots = ProjectedPivotPoints::new().with_timeframe(PivotTimeframe::Daily);
36//! pivots.calculate(&bars);
37//! assert!((pivots.projections()[0].top - 31.0 / 3.0).abs() < 1e-12);
38//! ```
39
40use super::pivot_points::{PivotLabelSide, PivotMethod, PivotPoints, PivotTimeframe};
41use crate::model::{Bar, Instrument, Timeframe};
42use crate::studies::{Indicator, IndicatorValues, StudyDataRequest, StudyLabel, StudyProjection};
43use egui::Color32;
44
45/// Projected Pivot Points: the next period's pivot levels from the period so far.
46#[derive(Clone)]
47pub struct ProjectedPivotPoints {
48    pivots: PivotPoints,
49    projections: Vec<StudyProjection>,
50}
51
52impl ProjectedPivotPoints {
53    /// Traditional pivots, timeframe Auto, for the last 15 periods.
54    pub fn new() -> Self {
55        Self {
56            pivots: PivotPoints::new(),
57            projections: Vec::new(),
58        }
59    }
60
61    /// The level formulas.
62    pub fn with_method(mut self, method: PivotMethod) -> Self {
63        self.pivots = self.pivots.with_method(method);
64        self
65    }
66
67    /// The calendar period of each set of pivots.
68    pub fn with_timeframe(mut self, timeframe: PivotTimeframe) -> Self {
69        self.pivots = self.pivots.with_timeframe(timeframe);
70        self
71    }
72
73    /// The number of most recent periods that show confirmed pivots, raised to 1 or more.
74    pub fn with_pivots_back(mut self, pivots_back: usize) -> Self {
75        self.pivots = self.pivots.with_pivots_back(pivots_back);
76        self
77    }
78
79    /// Whether each level is labelled with its name (P, R1, S1 and so on) and with its price.
80    /// Names only by default.
81    pub fn with_labels(mut self, names: bool, prices: bool) -> Self {
82        self.pivots = self.pivots.with_labels(names, prices);
83        self
84    }
85
86    /// The end of each level its label sits at; the left end by default.
87    pub fn with_label_side(mut self, side: PivotLabelSide) -> Self {
88        self.pivots = self.pivots.with_label_side(side);
89        self
90    }
91
92    /// Takes the previous period's values from the feed's daily bars, as
93    /// [`PivotPoints::with_daily_bars`] does.
94    pub fn with_daily_bars(mut self, daily: bool) -> Self {
95        self.pivots = self.pivots.with_daily_bars(daily);
96        self
97    }
98
99    fn project(&mut self, data: &[Bar]) {
100        self.projections.clear();
101        if let Some((method, period, start)) = self.pivots.latest_period() {
102            let last = data.len() - 1;
103            let width = data.len() - start;
104            let levels = PivotPoints::levels(method, period, period.close);
105            self.projections.extend(
106                levels
107                    .into_iter()
108                    .enumerate()
109                    .filter(|(_, price)| price.is_finite())
110                    .map(|(plot, price)| StudyProjection {
111                        bars: last as f64..(last + width) as f64,
112                        top: price,
113                        bottom: price,
114                        plot,
115                    }),
116            );
117            self.pivots.label_projection(last..last + width, &levels);
118        }
119    }
120}
121
122/// Construct with the conventional default parameters.
123impl Default for ProjectedPivotPoints {
124    fn default() -> Self {
125        Self::new()
126    }
127}
128
129impl Indicator for ProjectedPivotPoints {
130    fn inputs(&self) -> Vec<crate::studies::StudyInput> {
131        self.pivots.inputs()
132    }
133
134    fn set_input(
135        &mut self,
136        id: &str,
137        value: crate::studies::InputValue,
138    ) -> Result<(), crate::studies::StudyError> {
139        self.pivots.set_input(id, value)?;
140        self.projections.clear();
141        Ok(())
142    }
143
144    fn name(&self) -> &str {
145        "Projected Pivot Points"
146    }
147
148    fn desc(&self) -> &str {
149        "Projected Pivot Points - Next period's pivot levels from the period so far"
150    }
151
152    fn calculate(&mut self, data: &[Bar]) {
153        self.update(data, 0);
154    }
155
156    fn update(&mut self, data: &[Bar], from: usize) {
157        self.pivots.update(data, from);
158        self.project(data);
159    }
160
161    fn data_requests(&self) -> Vec<StudyDataRequest> {
162        self.pivots.data_requests()
163    }
164
165    fn update_data(
166        &mut self,
167        data: &[Bar],
168        from: usize,
169        sources: &[crate::studies::StudyData<'_>],
170    ) {
171        self.pivots.update_data(data, from, sources);
172        self.project(data);
173    }
174
175    fn is_incremental(&self) -> bool {
176        true
177    }
178
179    fn set_timeframe(&mut self, timeframe: Timeframe) {
180        self.pivots.set_timeframe(timeframe);
181    }
182
183    fn set_instrument(&mut self, instrument: Option<&Instrument>) -> bool {
184        self.pivots.set_instrument(instrument)
185    }
186
187    fn plot_color(&self, line: usize, bar: usize) -> Option<Color32> {
188        self.pivots.plot_color(line, bar)
189    }
190
191    fn projections(&self) -> &[StudyProjection] {
192        &self.projections
193    }
194
195    fn labels(&self) -> &[StudyLabel] {
196        self.pivots.labels()
197    }
198
199    fn set_label_precision(&mut self, precision: Option<usize>) {
200        self.pivots.set_label_precision(precision);
201    }
202
203    fn values(&self) -> &IndicatorValues {
204        self.pivots.values()
205    }
206
207    fn colors(&self) -> Vec<Color32> {
208        self.pivots.colors()
209    }
210
211    fn set_colors(&mut self, colors: Vec<Color32>) {
212        self.pivots.set_colors(colors);
213    }
214
215    fn is_overlay(&self) -> bool {
216        true
217    }
218
219    fn is_visible(&self) -> bool {
220        self.pivots.is_visible()
221    }
222
223    fn set_visible(&mut self, visible: bool) {
224        self.pivots.set_visible(visible);
225    }
226
227    fn clone_box(&self) -> Box<dyn Indicator> {
228        Box::new(self.clone())
229    }
230
231    fn line_names(&self) -> Vec<String> {
232        self.pivots.line_names()
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239    use chrono::{TimeZone, Utc};
240
241    /// Hourly bars of two days, daily periods. The first day has high 12, low 8 and close 11. By
242    /// the third bar of the second day its high is 14, low 9 and close 13, so the projection is
243    /// P = (14 + 9 + 13) / 3 = 12, R1 = 2P - L = 15 and S1 = 2P - H = 10, three bars wide from the
244    /// last bar, while the confirmed lines hold the first day's P = 31/3.
245    #[test]
246    fn projects_from_the_period_so_far_beside_the_confirmed_levels() {
247        let hour = |day, h| Utc.with_ymd_and_hms(2026, 3, day, h, 0, 0).unwrap();
248        let data: Vec<Bar> = [
249            (hour(2, 9), 10.0, 8.0, 9.0),
250            (hour(2, 10), 12.0, 9.0, 11.0),
251            (hour(3, 9), 11.0, 9.0, 10.0),
252            (hour(3, 10), 14.0, 10.0, 12.0),
253            (hour(3, 11), 13.0, 10.0, 13.0),
254        ]
255        .into_iter()
256        .map(|(time, high, low, close)| Bar::new(time, close, high, low, close, 1.0))
257        .collect();
258        let mut pivots = ProjectedPivotPoints::new().with_timeframe(PivotTimeframe::Daily);
259        pivots.calculate(&data);
260        let projected: Vec<_> = pivots.projections()[..3]
261            .iter()
262            .map(|p| (p.bars.clone(), p.top, p.plot))
263            .collect();
264        assert_eq!(
265            projected,
266            [
267                (4.0..7.0, 12.0, 0),
268                (4.0..7.0, 15.0, 1),
269                (4.0..7.0, 10.0, 2)
270            ]
271        );
272        assert!((pivots.values().value(0, 4).unwrap() - 31.0 / 3.0).abs() < 1e-12);
273        // A bar later moves the projection, the confirmed lines staying.
274        let mut more = data.clone();
275        more.push(Bar::new(hour(3, 12), 13.0, 15.0, 12.0, 13.0, 1.0));
276        pivots.update(&more, 5);
277        assert_eq!(pivots.projections()[0].bars, 5.0..9.0);
278        assert!((pivots.projections()[0].top - (15.0 + 9.0 + 13.0) / 3.0).abs() < 1e-12);
279    }
280}