1use super::lower_interval;
11use crate::model::LineStyle;
12use crate::model::{Bar, BarClock, Timeframe};
13use crate::scales::StudyFormat;
14use crate::studies::palette;
15use crate::studies::{
16 Indicator, IndicatorValues, InputValue, PlotStyle, PlotType, StudyData, StudyDataRequest,
17 StudyInput,
18};
19use egui::Color32;
20
21#[derive(Clone)]
23pub struct UpDownVolume {
24 clock: BarClock,
25 lower_interval: Option<Timeframe>,
27 values: IndicatorValues,
28 colors: [Color32; 3],
29 visible: bool,
30}
31
32impl UpDownVolume {
33 pub fn new() -> Self {
36 Self {
37 clock: BarClock::new(Timeframe::Min1),
38 lower_interval: None,
39 values: IndicatorValues::new(),
40 colors: [
41 palette::GREEN.gamma_multiply_u8(102),
42 palette::DOWN.gamma_multiply_u8(102),
43 palette::GRAY,
44 ],
45 visible: true,
46 }
47 }
48
49 fn lower_timeframe(&self) -> Option<Timeframe> {
50 lower_interval::lower_timeframe(&self.clock, self.lower_interval)
51 }
52 fn calculate_intrabars(&mut self, data: &[Bar], intrabars: &[Bar], from: usize) {
53 let from = from.min(self.values.len()).min(data.len());
54 if from == 0 || intrabars.is_empty() {
55 self.values = IndicatorValues::missing(data.len());
56 } else {
57 self.values.truncate(from);
58 self.values
59 .resize(data.len(), crate::studies::IndicatorValue::None);
60 }
61 if intrabars.is_empty() {
62 return;
63 }
64 let Some(lower) = self.lower_timeframe() else {
65 return;
66 };
67 let ranges = lower_interval::nest(&self.clock, lower, data, intrabars, from);
68 for (i, range) in (from..).zip(ranges) {
69 if !range.is_empty() && data[i].close.is_finite() {
70 let flow = lower_interval::flow(intrabars, range);
71 for (line, value) in [flow.up, flow.down, flow.up + flow.down]
72 .into_iter()
73 .enumerate()
74 {
75 self.values.set_value(line, i, value);
76 }
77 }
78 }
79 }
80}
81
82impl Default for UpDownVolume {
83 fn default() -> Self {
84 Self::new()
85 }
86}
87
88impl Indicator for UpDownVolume {
89 fn name(&self) -> &str {
90 "Up/Down Volume"
91 }
92 fn desc(&self) -> &str {
93 "Up and down volume from lower-timeframe candles supplied by the host"
94 }
95 fn inputs(&self) -> Vec<StudyInput> {
96 vec![lower_interval::input(self.lower_interval)]
97 }
98 fn set_input(&mut self, id: &str, value: InputValue) -> Result<(), crate::studies::StudyError> {
99 let ("lower_interval", InputValue::Timeframe(chosen)) = (id, value) else {
100 return Err(crate::studies::StudyError::rejected(&self.inputs(), id));
101 };
102 self.lower_interval = chosen;
103 self.calculate(&[]);
104 Ok(())
105 }
106 fn data_requests(&self) -> Vec<StudyDataRequest> {
107 self.lower_timeframe()
108 .map(lower_interval::request)
109 .into_iter()
110 .collect()
111 }
112 fn update_data(&mut self, data: &[Bar], from: usize, sources: &[StudyData<'_>]) {
113 let from = from.min(self.values.len()).min(data.len());
114 let intrabars = match self.lower_timeframe() {
115 Some(lower) => {
116 match lower_interval::reply(&lower_interval::request(lower), from, sources) {
117 Some(bars) => bars,
118 None => return,
119 }
120 }
121 None => &[],
122 };
123 self.calculate_intrabars(data, intrabars, from);
124 }
125 fn set_timeframe(&mut self, timeframe: Timeframe) {
126 self.set_bar_clock(&self.clock.for_timeframe(timeframe));
127 }
128 fn set_bar_clock(&mut self, clock: &BarClock) -> bool {
129 let changed = self.clock != *clock;
130 self.clock = clock.clone();
131 changed
132 }
133 fn calculate(&mut self, data: &[Bar]) {
134 self.calculate_intrabars(data, &[], 0);
135 }
136 fn is_incremental(&self) -> bool {
137 true
138 }
139 fn is_overlay(&self) -> bool {
140 false
141 }
142 fn value_format(&self) -> StudyFormat {
143 StudyFormat::Volume
144 }
145 fn plot_style(&self, line: usize) -> PlotStyle {
146 PlotStyle {
147 color: self.colors.get(line).copied().unwrap_or(Color32::WHITE),
148 width: 1.0,
149 pattern: LineStyle::Solid,
150 plot_type: if line == 2 {
151 PlotType::Crosses
152 } else {
153 PlotType::Columns
154 },
155 visible: true,
156 }
157 }
158 fn plot_color(&self, line: usize, bar: usize) -> Option<Color32> {
159 (line == 2)
160 .then(|| {
161 self.values.value(2, bar).map(|delta| {
162 let [r, g, b, _] =
163 self.colors[usize::from(delta <= 0.0)].to_srgba_unmultiplied();
164 Color32::from_rgb(r, g, b)
165 })
166 })
167 .flatten()
168 }
169 fn values(&self) -> &IndicatorValues {
170 &self.values
171 }
172 fn colors(&self) -> Vec<Color32> {
173 self.colors.to_vec()
174 }
175 fn set_colors(&mut self, colors: Vec<Color32>) {
176 for (old, new) in self.colors.iter_mut().zip(colors) {
177 *old = new;
178 }
179 }
180 fn is_visible(&self) -> bool {
181 self.visible
182 }
183 fn set_visible(&mut self, visible: bool) {
184 self.visible = visible;
185 }
186 fn clone_box(&self) -> Box<dyn Indicator> {
187 Box::new(self.clone())
188 }
189 fn line_names(&self) -> Vec<String> {
190 ["Up Volume", "Down Volume", "Delta"]
191 .map(String::from)
192 .to_vec()
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use crate::model::Bar;
199 use crate::studies::*;
200 use chrono::{TimeZone, Utc};
201
202 #[test]
204 fn up_down_volume_uses_host_intrabar_directions() {
205 use crate::model::{BarClock, Timeframe};
206 use crate::studies::{StudyData, StudyDataRequest};
207 let requested_interval = |study: &dyn Indicator| {
208 study
209 .data_requests()
210 .first()
211 .and_then(|request| request.timeframe)
212 };
213 let update = |study: &mut dyn Indicator, data: &[Bar], timeframe, bars: &[Bar]| {
214 study.update_data(
215 data,
216 0,
217 &[StudyData {
218 request: StudyDataRequest {
219 metric: None,
220 symbol: String::new(),
221 timeframe: Some(timeframe),
222 },
223 bars,
224 clock: BarClock::new(timeframe),
225 instrument: None,
226 timeframe,
227 }],
228 );
229 };
230 let mut study = ConfiguredStudy::new(
231 IndicatorFactory::new()
232 .create("Up/Down Volume")
233 .expect("Up/Down Volume study"),
234 );
235 study.set_timeframe(Timeframe::Day1);
236 assert_eq!(requested_interval(&study), Some(Timeframe::Min5));
237 study.set_bar_clock(&BarClock::new(Timeframe::Min5));
238 assert_eq!(requested_interval(&study), Some(Timeframe::Min1));
239 study.set_bar_clock(&BarClock::new(Timeframe::Ms100));
240 assert_eq!(requested_interval(&study), None);
241 study.set_bar_clock(&BarClock::new(Timeframe::Min5));
242 let bars: Vec<_> = [0, 5, 10, 15]
243 .into_iter()
244 .map(|m| {
245 Bar::new(
246 Utc.timestamp_opt(m * 60, 0).unwrap(),
247 10.0,
248 10.0,
249 10.0,
250 10.0,
251 999.0,
252 )
253 })
254 .collect();
255 let intrabars: Vec<_> = [
256 (0, 10.0, 11.0, 10.0),
257 (1, 11.0, 10.0, 20.0),
258 (2, 10.0, 10.0, 30.0),
259 (5, 9.0, 9.0, 40.0),
260 (6, 9.0, 9.0, 50.0),
261 (15, 12.0, 13.0, 0.0),
262 ]
263 .into_iter()
264 .map(|(m, o, c, v)| {
265 Bar::new(
266 Utc.timestamp_opt(m * 60, 0).unwrap(),
267 o,
268 o.max(c),
269 o.min(c),
270 c,
271 v,
272 )
273 })
274 .collect();
275 update(&mut study, &bars, Timeframe::Min1, &intrabars);
276 for (i, want) in [
278 [Some(40.0), Some(-20.0), Some(20.0)],
279 [Some(50.0), Some(-40.0), Some(10.0)],
280 [None; 3],
281 [Some(0.0); 3],
282 ]
283 .into_iter()
284 .enumerate()
285 {
286 for (line, expected) in want.into_iter().enumerate() {
287 assert_eq!(
288 study.values().value(line, i),
289 expected,
290 "bar {i}, line {line}"
291 );
292 }
293 }
294 update(&mut study, &bars, Timeframe::Min5, &intrabars);
296 assert_eq!(study.values().value(0, 0), None);
297 update(&mut study, &bars, Timeframe::Min1, &intrabars);
298 for invalid in [
299 {
300 let mut invalid = intrabars.clone();
301 invalid[1].time = invalid[0].time;
302 invalid
303 },
304 {
305 let mut invalid = intrabars.clone();
306 invalid[0].volume = -1.0;
307 invalid
308 },
309 {
310 let mut invalid = intrabars.clone();
311 invalid[0].open = f64::NAN;
312 invalid
313 },
314 ] {
315 update(&mut study, &bars, Timeframe::Min1, &invalid);
316 assert_eq!(study.values().value(2, 0), Some(20.0));
317 }
318 let mut corrected = intrabars;
319 corrected[0].volume = 15.0;
320 update(&mut study, &bars, Timeframe::Min1, &corrected);
321 assert_eq!(study.values().value(2, 0), Some(25.0));
322 update(&mut study, &bars, Timeframe::Min1, &[]);
323 assert_eq!(study.values().value(0, 0), None);
324 for (seconds, lower) in [(90, Timeframe::Sec1), (90_000, Timeframe::Hour1)] {
326 let timeframe = Timeframe::Custom(std::num::NonZeroU64::new(seconds).unwrap());
327 study.set_timeframe(timeframe);
328 assert_eq!(requested_interval(&study), Some(lower));
329 let bar = |second| {
330 Bar::new(
331 Utc.timestamp_opt(second, 0).unwrap(),
332 1.0,
333 2.0,
334 1.0,
335 2.0,
336 1.0,
337 )
338 };
339 let seconds = seconds as i64;
340 update(
341 &mut study,
342 &[bar(0), bar(seconds)],
343 lower,
344 &[0, seconds - lower.duration_ms() / 1000, seconds].map(bar),
345 );
346 assert_eq!(study.values().value(0, 0), Some(2.0));
347 assert_eq!(study.values().value(0, 1), Some(1.0));
348 }
349 study.set_bar_clock(&BarClock::new(Timeframe::Min5));
352 let custom = |s| Timeframe::Custom(std::num::NonZeroU64::new(s).unwrap());
353 for (chosen, wanted) in [
354 (Some(Timeframe::Sec30), Timeframe::Sec30),
355 (Some(Timeframe::Min5), Timeframe::Min1),
356 (Some(custom(120)), Timeframe::Min1),
357 (
358 Some(Timeframe::Ticks(std::num::NonZeroU64::MIN)),
359 Timeframe::Min1,
360 ),
361 (None, Timeframe::Min1),
362 ] {
363 study
364 .set_input("lower_interval", InputValue::Timeframe(chosen))
365 .unwrap();
366 assert_eq!(requested_interval(&study), Some(wanted), "{chosen:?}");
367 }
368 study
369 .set_input(
370 "lower_interval",
371 InputValue::Timeframe(Some(Timeframe::Sec30)),
372 )
373 .unwrap();
374 let candle = |s: i64, o: f64, c: f64, v: f64| {
375 Bar::new(
376 Utc.timestamp_opt(s, 0).unwrap(),
377 o,
378 o.max(c),
379 o.min(c),
380 c,
381 v,
382 )
383 };
384 let half_minutes = [
386 candle(0, 10.0, 11.0, 7.0),
387 candle(30, 11.0, 10.0, 3.0),
388 candle(300, 9.0, 9.0, 5.0),
389 ];
390 update(&mut study, &bars, Timeframe::Sec30, &half_minutes);
391 for (i, want) in [
392 [Some(7.0), Some(-3.0), Some(4.0)],
393 [Some(0.0), Some(-5.0), Some(-5.0)],
394 ]
395 .into_iter()
396 .enumerate()
397 {
398 for (line, expected) in want.into_iter().enumerate() {
399 assert_eq!(
400 study.values().value(line, i),
401 expected,
402 "bar {i}, line {line}"
403 );
404 }
405 }
406 update(&mut study, &bars, Timeframe::Min1, &corrected);
408 assert_eq!(study.values().value(2, 0), None);
409 study.set_bar_clock(
413 &BarClock::new(Timeframe::Week1)
414 .with_timezone(crate::model::TimezoneId::fixed_offset(-5 * 3_600).unwrap()),
415 );
416 study
417 .set_input(
418 "lower_interval",
419 InputValue::Timeframe(Some(Timeframe::Day1)),
420 )
421 .unwrap();
422 assert_eq!(requested_interval(&study), Some(Timeframe::Day1));
423 let weeks = [4, 11].map(|d| candle(d * 86_400 + 5 * 3_600, 1.0, 1.0, 999.0));
424 let days = [
425 candle(10 * 86_400, 2.0, 1.0, 4.0),
426 candle(11 * 86_400, 1.0, 2.0, 10.0),
427 ];
428 update(&mut study, &weeks, Timeframe::Day1, &days);
429 for (i, want) in [[0.0, -4.0, -4.0], [10.0, 0.0, 10.0]]
430 .into_iter()
431 .enumerate()
432 {
433 for (line, expected) in want.into_iter().enumerate() {
434 assert_eq!(
435 study.values().value(line, i),
436 Some(expected),
437 "week {i}, line {line}"
438 );
439 }
440 }
441 #[cfg(feature = "widget")]
442 {
443 use crate::model::{BarData, Instrument};
444 use crate::widget::{Chart, test_frames::frame};
445 let mut chart = Chart::new(BarData::from_bars(bars.clone()));
446 chart.set_timeframe(Timeframe::Min5);
447 chart.set_symbol("OLD");
448 chart.set_instrument(Some(Instrument::new("OLD")));
449 chart.add_indicator(Box::new(UpDownVolume::new()));
450 chart.set_symbol("NEW");
451 chart.set_instrument(Some(Instrument::new("NEW")));
452 let request = chart.study_data_requests().pop().unwrap();
453 assert_eq!(request.symbol, "NEW");
454 chart.set_study_data(request, vec![bars[0].clone()], Some(Instrument::new("NEW")));
455 frame(&egui::Context::default(), &mut chart, 0.0, Vec::new());
456 assert_eq!(
457 chart.indicators().next().unwrap().1.values().value(0, 0),
458 Some(999.0)
459 );
460 }
461 }
462}