Skip to main content

egui_charts/studies/builtin/
vwap_auto_anchored.rs

1//! VWAP Auto Anchored.
2//!
3//! A volume weighted average price drawn only from one anchor bar to the latest bar. The anchor
4//! is the first bar of the last anchor period in the data (a session, week, month, quarter,
5//! year, decade or century of trading days), or the bar with the highest high, the lowest low
6//! or the highest volume among the last `length` bars, the most recent of equal bars. Auto
7//! takes the period from the chart interval: the session below a day, the month at a day,
8//! the quarter up to 10 days, the year up to 60 days and the decade above. The anchor moves
9//! as bars arrive, and the line is redrawn from the new anchor.
10//!
11//! # Calculation
12//! VWAP = Sum(Source * Volume) / Sum(Volume) from the anchor bar, Source = (High + Low + Close) / 3
13//! by default; the source itself while the volume sum is zero.
14//!
15//! # Example
16//! ```
17//! # let bars: Vec<egui_charts::model::Bar> = (0..60).map(|i| { let p = 100.0 + (i as f64 * 0.3).sin() * 5.0; egui_charts::model::Bar::new(chrono::Utc::now(), p, p + 1.0, p - 1.0, p + 0.5, 1_000.0) }).collect();
18//! use egui_charts::studies::{AutoAnchor, Indicator, VwapAutoAnchored};
19//!
20//! // From the highest high of the last 50 bars.
21//! let mut vwap = VwapAutoAnchored::new()
22//!     .with_anchor(AutoAnchor::HighestHigh)
23//!     .with_length(50);
24//! vwap.calculate(&bars);
25//! assert!(vwap.values().value(0, bars.len() - 1).is_some());
26//! ```
27
28use super::anchor_period::{AnchorPeriod, ExchangeDays};
29use crate::model::{Bar, Instrument, PriceSource, Timeframe};
30use crate::studies::stepper::Stepper;
31use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
32use egui::Color32;
33
34/// How [`VwapAutoAnchored`] finds its anchor bar.
35#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
36#[non_exhaustive]
37pub enum AutoAnchor {
38    /// The period by chart interval: session below a day, month at a day, quarter up to 10
39    /// days, year up to 60 days, decade above.
40    #[default]
41    Auto,
42    /// The highest high among the last `length` bars.
43    HighestHigh,
44    /// The lowest low among the last `length` bars.
45    LowestLow,
46    /// The highest volume among the last `length` bars.
47    HighestVolume,
48    /// The start of the last trading session.
49    Session,
50    /// The start of the last ISO week.
51    Week,
52    /// The start of the last calendar month.
53    Month,
54    /// The start of the last calendar quarter.
55    Quarter,
56    /// The start of the last calendar year.
57    Year,
58    /// The start of the last decade.
59    Decade,
60    /// The start of the last century.
61    Century,
62}
63
64/// VWAP from an automatically found anchor bar to the latest bar.
65///
66/// Periods are counted in exchange trading days, as
67/// [`VolumeWeightedAvgPrice`](super::VolumeWeightedAvgPrice) counts them. The chart supplies
68/// its interval for [`AutoAnchor::Auto`] and the instrument's time zone and session; direct
69/// callers set them with [`Indicator::set_timeframe`] (one minute until then) and
70/// [`Indicator::set_instrument`] (UTC until then). Overlay indicator.
71#[derive(Clone)]
72pub struct VwapAutoAnchored {
73    anchor: AutoAnchor,
74    length: usize,
75    source: PriceSource,
76    interval: Timeframe,
77    days: ExchangeDays,
78    values: IndicatorValues,
79    stepper: Stepper<State>,
80    intraday: bool,
81    shown_start: usize,
82    shown_indices: Vec<usize>,
83    colors: Vec<Color32>,
84    visible: bool,
85}
86
87#[derive(Clone, Copy)]
88struct Point {
89    source: usize,
90    high: f64,
91    low: f64,
92    volume: f64,
93    price: f64,
94}
95
96#[derive(Clone, Default)]
97struct State {
98    source: usize,
99    previous_date: Option<chrono::NaiveDate>,
100    intraday: bool,
101    period: Option<(i32, u32)>,
102    start: usize,
103    volume: f64,
104    weighted: f64,
105    candidates: std::collections::VecDeque<Point>,
106}
107
108impl VwapAutoAnchored {
109    /// Anchor Auto, length 20, source hlc3.
110    pub fn new() -> Self {
111        Self {
112            anchor: AutoAnchor::Auto,
113            length: 20,
114            source: PriceSource::HLC3,
115            interval: Timeframe::Min1,
116            days: ExchangeDays::default(),
117            values: IndicatorValues::new(),
118            stepper: Stepper::default(),
119            intraday: false,
120            shown_start: 0,
121            shown_indices: Vec::new(),
122            colors: vec![Color32::from_rgb(30, 136, 229)],
123            visible: true,
124        }
125    }
126
127    /// How the anchor bar is found.
128    pub fn with_anchor(mut self, anchor: AutoAnchor) -> Self {
129        self.anchor = anchor;
130        self
131    }
132
133    /// The bars searched by the highest high, lowest low and highest volume anchors,
134    /// raised to 1 or more.
135    pub fn with_length(mut self, length: usize) -> Self {
136        self.length = length.max(1);
137        self
138    }
139
140    /// The price averaged. The anchor search reads the high, low or volume whatever the source.
141    pub fn with_source(mut self, source: PriceSource) -> Self {
142        self.source = source;
143        self
144    }
145
146    fn period(&self) -> Option<AnchorPeriod> {
147        let day = Timeframe::Day1.duration_ms();
148        Some(match self.anchor {
149            AutoAnchor::Auto => match self.interval.duration_ms() {
150                ms if ms < day => AnchorPeriod::Session,
151                ms if ms <= day => AnchorPeriod::Month,
152                ms if ms <= 10 * day => AnchorPeriod::Quarter,
153                ms if ms <= 60 * day => AnchorPeriod::Year,
154                _ => AnchorPeriod::Decade,
155            },
156            AutoAnchor::Session => AnchorPeriod::Session,
157            AutoAnchor::Week => AnchorPeriod::Week,
158            AutoAnchor::Month => AnchorPeriod::Month,
159            AutoAnchor::Quarter => AnchorPeriod::Quarter,
160            AutoAnchor::Year => AnchorPeriod::Year,
161            AutoAnchor::Decade => AnchorPeriod::Decade,
162            AutoAnchor::Century => AnchorPeriod::Century,
163            _ => return None,
164        })
165    }
166}
167
168impl Default for VwapAutoAnchored {
169    fn default() -> Self {
170        Self::new()
171    }
172}
173
174impl Indicator for VwapAutoAnchored {
175    crate::studies::inputs::study_inputs!(self;
176        anchor: choice("Anchor Period", [AutoAnchor::Auto, AutoAnchor::HighestHigh, AutoAnchor::LowestLow, AutoAnchor::HighestVolume, AutoAnchor::Session, AutoAnchor::Week, AutoAnchor::Month, AutoAnchor::Quarter, AutoAnchor::Year, AutoAnchor::Decade, AutoAnchor::Century]),
177        length: length("Length", 1, usize::MAX),
178        source: choice("Source", PriceSource::all())
179    ; {});
180
181    fn name(&self) -> &str {
182        "VWAP Auto Anchored"
183    }
184
185    fn desc(&self) -> &str {
186        "VWAP Auto Anchored - Volume weighted average price from an automatically found anchor"
187    }
188
189    fn calculate(&mut self, data: &[Bar]) {
190        self.update(data, 0);
191    }
192
193    fn update(&mut self, data: &[Bar], mut from: usize) {
194        let period = self.period();
195        let price_source = self.source;
196        let usable = |bar: &Bar| {
197            price_source
198                .compute(bar.open, bar.high, bar.low, bar.close)
199                .is_finite()
200                && bar.high.is_finite()
201                && bar.low.is_finite()
202                && bar.volume.is_finite()
203        };
204        let len = self.values.len();
205        if len == 0 || from < len - 1 || data.len() < len {
206            self.shown_start = 0;
207        }
208        loop {
209            self.stepper
210                .run(data, from, &mut self.values, |state, bar| {
211                    let source = state.source;
212                    state.source += 1;
213                    if !usable(bar) {
214                        return IndicatorValue::None;
215                    }
216                    let price = price_source.compute(bar.open, bar.high, bar.low, bar.close);
217                    let Some(period) = period else {
218                        state.candidates.push_back(Point {
219                            source,
220                            high: bar.high,
221                            low: bar.low,
222                            volume: bar.volume,
223                            price,
224                        });
225                        if state.candidates.len() > self.length {
226                            state.candidates.pop_front();
227                        }
228                        return IndicatorValue::None;
229                    };
230                    let date = bar.time.date_naive();
231                    state.intraday |= state.previous_date == Some(date);
232                    state.previous_date = Some(date);
233                    let key = Some(period.key(self.days.day(bar, !self.intraday)));
234                    if key != state.period {
235                        state.period = key;
236                        state.start = source;
237                        state.volume = 0.0;
238                        state.weighted = 0.0;
239                    }
240                    state.volume += bar.volume;
241                    state.weighted += price * bar.volume;
242                    IndicatorValue::Single(if state.volume == 0.0 {
243                        price
244                    } else {
245                        state.weighted / state.volume
246                    })
247                });
248            if period.is_none() || self.intraday == self.stepper.state().intraday {
249                break;
250            }
251            self.intraday = self.stepper.state().intraday;
252            self.shown_start = 0;
253            from = 0;
254        }
255        if period.is_some() {
256            let start = self.stepper.state().start;
257            if start > self.shown_start {
258                for bar in self.shown_start..start {
259                    self.values.set(bar, IndicatorValue::None);
260                }
261            } else if start < self.shown_start {
262                // Withdrawing a new anchor period restores the previously visible line.
263                let (mut volume, mut weighted) = (0.0, 0.0);
264                for (i, bar) in data.iter().enumerate().take(self.shown_start).skip(start) {
265                    if !usable(bar) {
266                        continue;
267                    }
268                    let price = price_source.compute(bar.open, bar.high, bar.low, bar.close);
269                    volume += bar.volume;
270                    weighted += price * bar.volume;
271                    self.values.set_value(
272                        0,
273                        i,
274                        if volume == 0.0 {
275                            price
276                        } else {
277                            weighted / volume
278                        },
279                    );
280                }
281            }
282            self.shown_start = start;
283        } else {
284            for &bar in &self.shown_indices {
285                if bar < self.values.len() {
286                    self.values.set(bar, IndicatorValue::None);
287                }
288            }
289            self.shown_indices.clear();
290            let candidates = &self.stepper.state().candidates;
291            let key = |point: &Point| match self.anchor {
292                AutoAnchor::HighestHigh => point.high,
293                AutoAnchor::LowestLow => -point.low,
294                _ => point.volume,
295            };
296            let start = (0..candidates.len())
297                .max_by(|&a, &b| key(&candidates[a]).total_cmp(&key(&candidates[b])))
298                .unwrap_or(candidates.len());
299            let (mut volume, mut weighted) = (0.0, 0.0);
300            for point in candidates.iter().skip(start) {
301                volume += point.volume;
302                weighted += point.price * point.volume;
303                self.values.set_value(
304                    0,
305                    point.source,
306                    if volume == 0.0 {
307                        point.price
308                    } else {
309                        weighted / volume
310                    },
311                );
312                self.shown_indices.push(point.source);
313            }
314        }
315    }
316
317    fn is_incremental(&self) -> bool {
318        true
319    }
320
321    fn set_timeframe(&mut self, timeframe: Timeframe) {
322        self.interval = timeframe;
323    }
324
325    fn set_instrument(&mut self, instrument: Option<&Instrument>) -> bool {
326        self.days.set(instrument)
327    }
328
329    fn values(&self) -> &IndicatorValues {
330        &self.values
331    }
332
333    fn colors(&self) -> Vec<Color32> {
334        self.colors.clone()
335    }
336
337    fn set_colors(&mut self, colors: Vec<Color32>) {
338        if let Some(color) = colors.first() {
339            self.colors = vec![*color];
340        }
341    }
342
343    fn is_overlay(&self) -> bool {
344        true
345    }
346
347    fn is_visible(&self) -> bool {
348        self.visible
349    }
350
351    fn set_visible(&mut self, visible: bool) {
352        self.visible = visible;
353    }
354
355    fn clone_box(&self) -> Box<dyn Indicator> {
356        Box::new(self.clone())
357    }
358
359    fn line_names(&self) -> Vec<String> {
360        vec!["VWAP".to_string()]
361    }
362}