egui_charts/studies/builtin/
relative_volume_at_time.rs1use crate::model::{Bar, BarClock, ExchangeSessionProvider, Instrument, Session, TimezoneId};
45use crate::studies::palette;
46use crate::studies::stepper::Stepper;
47use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
48use chrono::{Datelike, NaiveDate};
49use egui::Color32;
50
51const DAY_MS: i64 = 86_400_000;
52
53#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
55pub enum RelativeVolumeAnchor {
56 #[default]
58 Day,
59 Week,
61 Month,
63}
64
65impl RelativeVolumeAnchor {
66 fn locate(self, day: NaiveDate) -> ((i32, u32), u32) {
68 match self {
69 Self::Day => ((day.year(), day.ordinal()), 0),
70 Self::Week => (
71 (day.iso_week().year(), day.iso_week().week()),
72 day.weekday().num_days_from_monday(),
73 ),
74 Self::Month => ((day.year(), day.month()), day.day0()),
75 }
76 }
77}
78
79#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
81pub enum RelativeVolumeMode {
82 #[default]
84 Cumulative,
85 Regular,
87}
88
89#[derive(Clone)]
97pub struct RelativeVolumeAtTime {
98 length: usize,
99 anchor: RelativeVolumeAnchor,
100 mode: RelativeVolumeMode,
101 instrument: Option<(TimezoneId, Option<Session>)>,
102 daily: bool,
104 values: IndicatorValues,
105 stepper: Stepper<State>,
106 periods: std::collections::VecDeque<Vec<(i64, f64)>>,
107 undo: Undo,
108 colors: [Color32; 2],
109 visible: bool,
110}
111
112#[derive(Clone, Default)]
113struct State {
114 source: usize,
115 key: Option<(i32, u32)>,
116 cumulative: f64,
117 calendar_date: Option<NaiveDate>,
118 opens: Vec<(chrono::DateTime<chrono::Utc>, NaiveDate)>,
119}
120
121#[derive(Clone, Default)]
122struct Undo {
123 appended: bool,
124 new_period: bool,
125 expired: Option<Vec<(i64, f64)>>,
126}
127
128impl RelativeVolumeAtTime {
129 pub fn new(length: usize) -> Self {
131 Self {
132 length: length.max(1),
133 anchor: RelativeVolumeAnchor::Day,
134 mode: RelativeVolumeMode::Cumulative,
135 instrument: None,
136 daily: false,
137 values: IndicatorValues::new(),
138 stepper: Stepper::default(),
139 periods: std::collections::VecDeque::new(),
140 undo: Undo::default(),
141 colors: [
142 palette::GREEN.gamma_multiply_u8(77),
143 palette::DOWN.gamma_multiply_u8(77),
144 ],
145 visible: true,
146 }
147 }
148
149 pub fn with_anchor(mut self, anchor: RelativeVolumeAnchor) -> Self {
151 self.anchor = anchor;
152 self
153 }
154
155 pub fn with_mode(mut self, mode: RelativeVolumeMode) -> Self {
157 self.mode = mode;
158 self
159 }
160}
161
162impl Default for RelativeVolumeAtTime {
163 fn default() -> Self {
164 Self::new(10)
165 }
166}
167
168impl Indicator for RelativeVolumeAtTime {
169 crate::studies::inputs::study_inputs!(self;
170 anchor: choice("Anchor", [RelativeVolumeAnchor::Day, RelativeVolumeAnchor::Week, RelativeVolumeAnchor::Month]),
171 length: length("Length", 1, usize::MAX),
172 mode: choice("Calculation mode", [RelativeVolumeMode::Cumulative, RelativeVolumeMode::Regular])
173 ; {});
174
175 fn name(&self) -> &str {
176 "Relative Volume at Time"
177 }
178
179 fn desc(&self) -> &str {
180 "Relative Volume at Time - Volume against its average at the same time of past periods"
181 }
182
183 fn plot_style(&self, _line: usize) -> crate::studies::PlotStyle {
184 crate::studies::PlotStyle {
185 color: self.colors[0],
186 width: 1.0,
187 pattern: crate::model::LineStyle::Solid,
188 plot_type: crate::studies::PlotType::Columns,
189 visible: true,
190 }
191 }
192
193 fn plot_color(&self, _line: usize, bar: usize) -> Option<Color32> {
194 let value = self.values.value(0, bar)?;
195 Some(self.colors[usize::from(value <= 1.0)])
196 }
197
198 fn pane(&self) -> crate::studies::StudyPane {
199 crate::studies::StudyPane {
200 levels: vec![(1.0, palette::GRAY.gamma_multiply_u8(128), "Equality")],
201 ..Default::default()
202 }
203 }
204
205 fn calculate(&mut self, data: &[Bar]) {
206 self.update(data, 0);
207 }
208
209 fn update(&mut self, data: &[Bar], from: usize) {
210 let len = self.values.len();
211 if len > 0 && from == len && data.len() == len {
212 return;
213 }
214 let start = if len > 0 && from >= len - 1 && data.len() >= len {
215 from.min(len)
216 } else {
217 0
218 };
219 if start == 0 {
220 self.periods.clear();
221 } else if start < len {
222 if self.undo.appended {
223 self.periods.back_mut().unwrap().pop();
224 }
225 if self.undo.new_period {
226 self.periods.pop_back();
227 }
228 if let Some(expired) = self.undo.expired.take() {
229 self.periods.push_front(expired);
230 }
231 }
232 self.undo = Undo::default();
233 let provider = (!self.daily).then(|| {
234 let (zone, session) = self
235 .instrument
236 .clone()
237 .unwrap_or_else(|| (TimezoneId::utc(), None));
238 ExchangeSessionProvider::new(zone, session)
239 });
240 self.stepper
241 .run(data, from, &mut self.values, |state, bar| {
242 let source = state.source;
243 state.source += 1;
244 if !bar.volume.is_finite() {
245 return IndicatorValue::None;
246 }
247 if !self.daily && state.calendar_date != Some(bar.time.date_naive()) {
248 state.opens = provider.as_ref().unwrap().opens([bar.time]);
249 state.calendar_date = Some(bar.time.date_naive());
250 }
251 let (open, day) = if self.daily {
252 (bar.time, bar.time.date_naive())
253 } else {
254 match state.opens.partition_point(|(open, _)| *open <= bar.time) {
255 0 => (bar.time, bar.time.date_naive()),
256 n => state.opens[n - 1],
257 }
258 };
259 let (period, days) = self.anchor.locate(day);
260 let offset = i64::from(days) * DAY_MS + (bar.time - open).num_milliseconds();
261 let last = source + 1 == data.len();
262 if state.key != Some(period) {
263 state.key = Some(period);
264 state.cumulative = 0.0;
265 self.periods.push_back(Vec::new());
266 let expired = if self.periods.len() > self.length.saturating_add(1) {
267 self.periods.pop_front()
268 } else {
269 None
270 };
271 if last {
272 self.undo.new_period = true;
273 self.undo.expired = expired;
274 }
275 }
276 state.cumulative += bar.volume;
277 let current = match self.mode {
278 RelativeVolumeMode::Cumulative => state.cumulative,
279 RelativeVolumeMode::Regular => bar.volume,
280 };
281 self.periods.back_mut().unwrap().push((offset, current));
282 if last {
283 self.undo.appended = true;
284 }
285 if self.periods.len() <= self.length {
286 return IndicatorValue::None;
287 }
288 let total: f64 = self
289 .periods
290 .iter()
291 .take(self.length)
292 .map(|past| match past.partition_point(|&(o, _)| o <= offset) {
293 0 => 0.0,
294 n => past[n - 1].1,
295 })
296 .sum();
297 let average = total / self.length as f64;
298 if average != 0.0 {
299 IndicatorValue::Single(current / average)
300 } else {
301 IndicatorValue::None
302 }
303 });
304 }
305
306 fn is_incremental(&self) -> bool {
307 true
308 }
309
310 fn set_instrument(&mut self, instrument: Option<&Instrument>) -> bool {
311 let next = instrument.map(|instrument| {
312 (
313 instrument.time_zone.clone().unwrap_or_else(TimezoneId::utc),
314 instrument.session.clone(),
315 )
316 });
317 let changed = next != self.instrument;
318 self.instrument = next;
319 changed
320 }
321
322 fn set_bar_clock(&mut self, clock: &BarClock) -> bool {
323 let daily = clock.timeframe().is_some_and(|timeframe| {
324 timeframe.duration_ms() >= crate::model::Timeframe::Day1.duration_ms()
325 });
326 let changed = daily != self.daily;
327 self.daily = daily;
328 changed
329 }
330
331 fn values(&self) -> &IndicatorValues {
332 &self.values
333 }
334
335 fn colors(&self) -> Vec<Color32> {
336 self.colors.to_vec()
337 }
338
339 fn set_colors(&mut self, colors: Vec<Color32>) {
340 for (old, new) in self.colors.iter_mut().zip(colors) {
341 *old = new;
342 }
343 }
344
345 fn is_overlay(&self) -> bool {
346 false
347 }
348
349 fn is_visible(&self) -> bool {
350 self.visible
351 }
352
353 fn set_visible(&mut self, visible: bool) {
354 self.visible = visible;
355 }
356
357 fn clone_box(&self) -> Box<dyn Indicator> {
358 Box::new(self.clone())
359 }
360
361 fn line_names(&self) -> Vec<String> {
362 vec![format!("RVOL({})", self.length)]
363 }
364}