1use super::{ATR, ZigZag};
4use crate::drawings::{ChartPoint, Drawing, DrawingToolType, FibonacciConfig, FibonacciLevel};
5use crate::model::Bar;
6use crate::studies::{
7 Indicator, IndicatorValue, IndicatorValues, InputKind, InputValue, StudyInput,
8};
9use egui::Color32;
10
11const ATR_MULTIPLE: f64 = 3.0;
13const ATR_PERIOD: usize = 10;
14
15const MAX_FIB_LEVELS: usize = 24;
17
18const LEVEL_FIELDS: [&str; 5] = [
20 "fib_visible",
21 "fib_ratio",
22 "fib_label",
23 "fib_color",
24 "fib_price",
25];
26
27fn level_slot(id: &str) -> Option<usize> {
29 let (field, index) = id.rsplit_once('_')?;
30 if LEVEL_FIELDS.contains(&field) {
31 index.parse().ok()
32 } else {
33 None
34 }
35}
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub enum AutoDrawingKind {
40 TrendLine,
42 ParallelChannel,
44 FibonacciRetracement,
46 FibonacciExtension,
48 Pitchfork,
50}
51
52impl AutoDrawingKind {
53 pub(crate) const ALL: [Self; 5] = [
54 Self::TrendLine,
55 Self::ParallelChannel,
56 Self::FibonacciRetracement,
57 Self::FibonacciExtension,
58 Self::Pitchfork,
59 ];
60
61 fn fibonacci(self) -> bool {
63 matches!(self, Self::FibonacciRetracement | Self::FibonacciExtension)
64 }
65
66 fn default_fibonacci(self) -> FibonacciConfig {
68 let mut fibonacci = if self == Self::FibonacciExtension {
69 FibonacciConfig::extension_default()
70 } else {
71 FibonacciConfig::default()
72 };
73 for level in &mut fibonacci.levels {
74 let hex = match level.value {
75 0.0 | 1.0 => [120, 123, 134, 255],
76 0.236 | 2.618 => [244, 67, 54, 255],
77 0.382 => [129, 199, 132, 255],
78 0.5 => [76, 175, 80, 255],
79 0.618 => [0, 150, 136, 255],
80 0.786 => [100, 181, 246, 255],
81 1.618 => [41, 98, 255, 255],
82 _ => level.color,
83 };
84 level.color = hex;
85 if level.value == 1.272 || level.value == 2.0 {
86 level.visible = false;
87 }
88 }
89 fibonacci.show_background = true;
90 fibonacci.background_opacity = 38;
91 fibonacci.extend_right = true;
92 fibonacci
93 }
94
95 fn tool(self) -> DrawingToolType {
96 match self {
97 Self::TrendLine => DrawingToolType::TrendLine,
98 Self::ParallelChannel => DrawingToolType::ParallelChannel,
99 Self::FibonacciRetracement => DrawingToolType::FibonacciRetracement,
100 Self::FibonacciExtension => DrawingToolType::FibonacciExtension,
101 Self::Pitchfork => DrawingToolType::Pitchfork,
102 }
103 }
104}
105
106#[derive(Clone)]
118pub struct AutoDrawing {
119 kind: AutoDrawingKind,
120 zigzag: ZigZag,
121 atr_swings: bool,
123 atr_multiple: f64,
124 atr_period: usize,
125 atr: ATR,
126 fibonacci: FibonacciConfig,
127 fork_colors: [Color32; 2],
128 drawings: Vec<Drawing>,
129 values: IndicatorValues,
130}
131
132impl AutoDrawing {
133 pub fn new(kind: AutoDrawingKind, deviation_percent: f64, depth: usize) -> Self {
138 Self {
139 kind,
140 zigzag: ZigZag::new(deviation_percent, depth).with_color(
141 if kind == AutoDrawingKind::Pitchfork {
142 crate::studies::palette::BRIGHT_RED
143 } else {
144 crate::studies::palette::BLUE
145 },
146 ),
147 atr_swings: false,
148 atr_multiple: ATR_MULTIPLE,
149 atr_period: ATR_PERIOD,
150 atr: ATR::new(ATR_PERIOD),
151 fibonacci: kind.default_fibonacci(),
152 fork_colors: [
153 crate::studies::palette::GREEN,
154 crate::studies::palette::BLUE,
155 ],
156 drawings: Vec::new(),
157 values: IndicatorValues::new(),
158 }
159 }
160
161 pub fn with_atr(mut self, multiple: f64, period: usize) -> Self {
168 self.atr_swings = true;
169 self.atr_multiple = super::clamp_param(multiple, 0.0, f64::MAX);
170 self.atr_period = period.max(1);
171 self.atr = ATR::new(self.atr_period);
172 self
173 }
174
175 fn fork_config(&self) -> crate::drawings::PitchforkConfig {
176 let mut config = crate::drawings::PitchforkConfig::legacy();
177 let colors = [
178 self.zigzag.colors()[0],
179 self.fork_colors[0],
180 self.fork_colors[1],
181 ];
182 for level in &mut config.levels {
183 level.visible = level.value <= 1.0;
184 level.color = colors[if level.value == 0.0 {
185 0
186 } else if level.value == 0.5 {
187 1
188 } else {
189 2
190 }]
191 .to_srgba_unmultiplied();
192 }
193 config.background_opacity = 38;
194 config
195 }
196
197 pub fn with_fibonacci_config(mut self, mut config: FibonacciConfig) -> Self {
201 config.levels.retain(|level| level.value.is_finite());
202 config.levels.truncate(MAX_FIB_LEVELS);
203 for level in &mut config.levels {
204 level.label = level.label.replace(char::is_control, " ").trim().to_owned();
205 }
206 self.fibonacci = config;
207 self
208 }
209}
210
211impl Indicator for AutoDrawing {
212 fn name(&self) -> &str {
213 match self.kind {
214 AutoDrawingKind::TrendLine => "Auto Trend Line",
215 AutoDrawingKind::ParallelChannel => "Auto Parallel Channel",
216 AutoDrawingKind::FibonacciRetracement => "Auto Fibonacci Retracement",
217 AutoDrawingKind::FibonacciExtension => "Auto Fibonacci Extension",
218 AutoDrawingKind::Pitchfork => "Auto Pitchfork",
219 }
220 }
221
222 fn desc(&self) -> &str {
223 "Drawing anchored on Zig Zag swing extremes"
224 }
225
226 fn inputs(&self) -> Vec<StudyInput> {
230 let mut inputs = vec![
231 StudyInput::new(
232 "swing_size",
233 "Swing size",
234 InputValue::Choice(usize::from(!self.atr_swings)),
235 InputKind::Choice(["ATR multiple", "Percent"].map(str::to_owned).to_vec()),
236 )
237 .with_default(InputValue::Choice(0)),
238 StudyInput::new(
239 "atr_multiple",
240 "ATR multiple",
241 InputValue::Number(self.atr_multiple),
242 InputKind::Number(0.0, f64::MAX),
243 )
244 .with_default(InputValue::Number(ATR_MULTIPLE)),
245 StudyInput::new(
246 "atr_period",
247 "ATR period",
248 InputValue::Length(self.atr_period),
249 InputKind::Length(1, usize::MAX),
250 )
251 .with_default(InputValue::Length(ATR_PERIOD)),
252 ];
253 inputs.extend(
254 self.zigzag
255 .inputs()
256 .into_iter()
257 .filter(|input| matches!(input.id.as_str(), "deviation_percent" | "depth")),
258 );
259 if !self.kind.fibonacci() {
260 return inputs;
261 }
262 let config = &self.fibonacci;
263 let defaults = self.kind.default_fibonacci();
264 let toggle = |id: String, label: String, value: bool, default: bool| {
265 StudyInput::new(id, label, InputValue::Toggle(value), InputKind::Toggle)
266 .with_default(InputValue::Toggle(default))
267 };
268 let colour = |[r, g, b, a]: [u8; 4]| Color32::from_rgba_unmultiplied(r, g, b, a);
269 let optional = |id, label, value: Option<bool>, default: Option<bool>| {
270 let index = |value| match value {
271 None => 0,
272 Some(false) => 1,
273 Some(true) => 2,
274 };
275 StudyInput::new(
276 id,
277 label,
278 InputValue::Choice(index(value)),
279 InputKind::Choice(["Default", "Hidden", "Shown"].map(str::to_owned).to_vec()),
280 )
281 .with_default(InputValue::Choice(index(default)))
282 };
283 inputs.extend([
284 optional(
285 "fib_show_prices",
286 "Price labels",
287 config.show_prices,
288 defaults.show_prices,
289 ),
290 optional(
291 "fib_show_levels",
292 "Level labels",
293 config.show_levels,
294 defaults.show_levels,
295 ),
296 toggle(
297 "fib_levels_as_percent".into(),
298 "Levels as percentages".into(),
299 config.levels_as_percent,
300 defaults.levels_as_percent,
301 ),
302 toggle(
303 "fib_labels_on_left".into(),
304 "Labels on left".into(),
305 config.labels_on_left,
306 defaults.labels_on_left,
307 ),
308 toggle(
309 "fib_reverse".into(),
310 "Reverse".into(),
311 config.reverse,
312 defaults.reverse,
313 ),
314 toggle(
315 "fib_extend_left".into(),
316 "Extend left".into(),
317 config.extend_left,
318 defaults.extend_left,
319 ),
320 toggle(
321 "fib_extend_right".into(),
322 "Extend right".into(),
323 config.extend_right,
324 defaults.extend_right,
325 ),
326 toggle(
327 "fib_background".into(),
328 "Background".into(),
329 config.show_background,
330 defaults.show_background,
331 ),
332 StudyInput::new(
333 "fib_background_opacity",
334 "Background opacity",
335 InputValue::Length(config.background_opacity.into()),
336 InputKind::Length(0, 255),
337 )
338 .with_default(InputValue::Length(defaults.background_opacity.into())),
339 toggle(
340 "fib_logarithmic".into(),
341 "Logarithmic levels".into(),
342 config.logarithmic,
343 defaults.logarithmic,
344 ),
345 StudyInput::new(
346 "fib_level_count",
347 "Levels",
348 InputValue::Length(config.levels.len()),
349 InputKind::Length(0, MAX_FIB_LEVELS),
350 )
351 .with_default(InputValue::Length(defaults.levels.len())),
352 ]);
353 for (i, level) in config.levels.iter().enumerate() {
354 let default = defaults.levels.get(i).unwrap_or(level);
356 let n = i + 1;
357 inputs.extend([
358 toggle(
359 format!("fib_visible_{i}"),
360 format!("Level {n} visible"),
361 level.visible,
362 default.visible,
363 ),
364 StudyInput::new(
365 format!("fib_ratio_{i}"),
366 format!("Level {n} ratio"),
367 InputValue::Number(level.value.into()),
368 InputKind::Number((-f32::MAX).into(), f32::MAX.into()),
369 )
370 .with_default(InputValue::Number(default.value.into())),
371 StudyInput::new(
372 format!("fib_label_{i}"),
373 format!("Level {n} label"),
374 InputValue::Text(level.label.clone()),
375 InputKind::Text,
376 )
377 .with_default(InputValue::Text(default.label.clone())),
378 StudyInput::new(
379 format!("fib_color_{i}"),
380 format!("Level {n} colour"),
381 InputValue::Color(colour(level.color)),
382 InputKind::Color,
383 )
384 .with_default(InputValue::Color(colour(default.color))),
385 toggle(
386 format!("fib_price_{i}"),
387 format!("Level {n} price visible"),
388 level.show_price,
389 default.show_price,
390 ),
391 ]);
392 }
393 inputs
394 }
395
396 fn set_input(&mut self, id: &str, value: InputValue) -> Result<(), crate::studies::StudyError> {
401 if matches!(id, "deviation_percent" | "depth") {
402 self.zigzag.set_input(id, value)?;
403 self.calculate(&[]);
404 return Ok(());
405 }
406 if self.kind.fibonacci()
407 && level_slot(id)
408 .is_some_and(|i| (self.fibonacci.levels.len()..MAX_FIB_LEVELS).contains(&i))
409 {
410 return Ok(());
411 }
412 let schema = self
413 .inputs()
414 .into_iter()
415 .find(|input| input.id == id)
416 .ok_or_else(|| crate::studies::StudyError::UnknownInput(id.into()))?;
417 if !schema.accepts(&value) {
418 return Err(crate::studies::StudyError::invalid_input(id));
419 }
420 let config = &mut self.fibonacci;
421 match (id, value) {
422 ("swing_size", InputValue::Choice(index)) => self.atr_swings = index == 0,
423 ("atr_multiple", InputValue::Number(multiple)) => self.atr_multiple = multiple,
424 ("atr_period", InputValue::Length(period)) => {
425 self.atr_period = period;
426 self.atr = ATR::new(period);
427 }
428 ("fib_show_prices", InputValue::Choice(index)) => {
429 config.show_prices = [None, Some(false), Some(true)][index]
430 }
431 ("fib_show_levels", InputValue::Choice(index)) => {
432 config.show_levels = [None, Some(false), Some(true)][index]
433 }
434 ("fib_levels_as_percent", InputValue::Toggle(on)) => config.levels_as_percent = on,
435 ("fib_labels_on_left", InputValue::Toggle(on)) => config.labels_on_left = on,
436 ("fib_reverse", InputValue::Toggle(on)) => config.reverse = on,
437 ("fib_extend_left", InputValue::Toggle(on)) => config.extend_left = on,
438 ("fib_extend_right", InputValue::Toggle(on)) => config.extend_right = on,
439 ("fib_background", InputValue::Toggle(on)) => config.show_background = on,
440 ("fib_background_opacity", InputValue::Length(opacity)) => {
442 config.background_opacity = opacity as u8;
443 }
444 ("fib_logarithmic", InputValue::Toggle(on)) => config.logarithmic = on,
445 ("fib_level_count", InputValue::Length(count)) => {
446 let defaults = self.kind.default_fibonacci().levels;
447 let color = self.zigzag.colors()[0].to_srgba_unmultiplied();
448 config.levels.truncate(count);
449 while config.levels.len() < count {
450 let level = defaults
451 .get(config.levels.len())
452 .cloned()
453 .unwrap_or_else(|| FibonacciLevel::new(1.0, "100%", color));
454 config.levels.push(level);
455 }
456 }
457 (id, value) => {
458 let level = &mut config.levels[level_slot(id)
459 .ok_or_else(|| crate::studies::StudyError::UnknownInput(id.into()))?];
460 match (id.rsplit_once('_').map(|(field, _)| field), value) {
461 (Some("fib_visible"), InputValue::Toggle(on)) => level.visible = on,
462 (Some("fib_ratio"), InputValue::Number(ratio)) => level.value = ratio as f32,
463 (Some("fib_label"), InputValue::Text(label)) => level.label = label,
464 (Some("fib_color"), InputValue::Color(color)) => {
465 level.color = color.to_srgba_unmultiplied();
466 }
467 (Some("fib_price"), InputValue::Toggle(on)) => level.show_price = on,
468 _ => return Err(crate::studies::StudyError::UnknownInput(id.into())),
469 }
470 }
471 }
472 self.calculate(&[]);
473 Ok(())
474 }
475
476 fn calculate(&mut self, data: &[Bar]) {
477 self.update(data, 0);
478 }
479
480 fn update(&mut self, data: &[Bar], from: usize) {
481 if from == 0 {
482 self.values = IndicatorValues::missing(data.len());
483 } else {
484 for pivot in self.zigzag.pivots().iter().rev().take(4) {
486 if pivot.bar_index < self.values.len() {
487 self.values.set(pivot.bar_index, IndicatorValue::None);
488 }
489 }
490 self.values.resize(data.len(), IndicatorValue::None);
491 }
492 if self.atr_swings {
493 self.atr.update(data, from);
494 }
495 let (atr, multiple) = (&self.atr, self.atr_multiple);
496 let by_atr = |source: usize| {
497 atr.values()
498 .value(0, source)
499 .map_or(f64::NAN, |atr| 100.0 * multiple * atr / data[source].close)
500 };
501 self.zigzag.update_swings(
502 data,
503 from,
504 self.atr_swings.then_some(&by_atr as &dyn Fn(usize) -> f64),
505 );
506 self.drawings.clear();
507 let pivots = self.zigzag.pivots();
508 let pivots = if self.kind == AutoDrawingKind::FibonacciRetracement {
509 &pivots[..pivots.len().saturating_sub(1)]
510 } else {
511 pivots
512 };
513 let needed = if self.kind == AutoDrawingKind::FibonacciRetracement {
514 2
515 } else {
516 3
517 };
518 let Some(start) = pivots.len().checked_sub(needed) else {
519 return;
520 };
521 let recent = &pivots[start..];
522 let order: &[usize] = match self.kind {
523 AutoDrawingKind::TrendLine => &[0, 2],
524 AutoDrawingKind::ParallelChannel => &[0, 2, 1],
525 AutoDrawingKind::FibonacciRetracement => &[0, 1],
526 AutoDrawingKind::FibonacciExtension | AutoDrawingKind::Pitchfork => &[0, 1, 2],
527 };
528 let mut drawing = Drawing::new(0, self.kind.tool());
529 drawing.completed = true;
530 drawing.color = self.zigzag.colors()[0].to_srgba_unmultiplied();
531 drawing.chart_points = order
532 .iter()
533 .map(|&i| {
534 let pivot = recent[i];
535 self.values.set_value(0, pivot.bar_index, pivot.price);
536 ChartPoint::new(pivot.bar_index as f32, pivot.price)
537 })
538 .collect();
539 if self.kind.fibonacci() {
540 drawing.fib_config = Some(self.fibonacci.clone());
541 }
542 if self.kind == AutoDrawingKind::Pitchfork {
543 drawing.pitchfork = Some(self.fork_config());
544 }
545 self.drawings.push(drawing);
546 }
547
548 fn is_incremental(&self) -> bool {
549 true
550 }
551
552 fn values(&self) -> &IndicatorValues {
553 &self.values
554 }
555 fn drawings(&self) -> &[Drawing] {
556 &self.drawings
557 }
558 fn colors(&self) -> Vec<Color32> {
559 let mut colors = self.zigzag.colors();
560 if self.kind == AutoDrawingKind::Pitchfork {
561 colors.extend(self.fork_colors);
562 }
563 colors
564 }
565 fn set_colors(&mut self, colors: Vec<Color32>) {
566 if self.kind == AutoDrawingKind::Pitchfork
567 && let Some(&color) = colors.first()
568 {
569 self.fork_colors = [
570 colors.get(1).copied().unwrap_or(color),
571 colors.get(2).copied().unwrap_or(color),
572 ];
573 }
574 self.zigzag.set_colors(colors);
575 if self.kind == AutoDrawingKind::Pitchfork {
576 let config = self.fork_config();
577 for drawing in &mut self.drawings {
578 drawing.pitchfork = Some(config.clone());
579 }
580 }
581 }
582 fn set_color(&mut self, line: usize, color: Color32) {
583 if line == 0 {
584 self.set_colors(vec![color]);
585 }
586 }
587 fn is_overlay(&self) -> bool {
588 true
589 }
590 fn is_visible(&self) -> bool {
591 self.zigzag.is_visible()
592 }
593 fn set_visible(&mut self, visible: bool) {
594 self.zigzag.set_visible(visible);
595 }
596 fn clone_box(&self) -> Box<dyn Indicator> {
597 Box::new(self.clone())
598 }
599}
600
601#[cfg(test)]
602mod tests {
603 use super::*;
604 use crate::studies::{ConfiguredStudy, IndicatorFactory};
605
606 fn bars() -> Vec<Bar> {
607 [
610 (101.0, 100.0),
611 (105.0, 104.0),
612 (108.0, 107.0),
613 (113.0, 112.0),
614 (110.0, 104.0),
615 (103.0, 100.0),
616 (107.0, 106.0),
617 (110.0, 109.0),
618 (114.0, 113.0),
619 (118.0, 117.0),
620 ]
621 .into_iter()
622 .enumerate()
623 .map(|(i, (h, l))| {
624 Bar::new(
625 chrono::DateTime::from_timestamp(i as i64 * 60, 0).unwrap(),
626 l,
627 h,
628 l,
629 l,
630 1.0,
631 )
632 })
633 .collect()
634 }
635
636 #[test]
638 fn automatic_drawings_follow_shared_swings() {
639 for (kind, tool, initial, updated) in [
640 (
641 AutoDrawingKind::TrendLine,
642 DrawingToolType::TrendLine,
643 vec![(3, 113.0), (9, 118.0)],
644 vec![(5, 100.0), (12, 93.0)],
645 ),
646 (
647 AutoDrawingKind::ParallelChannel,
648 DrawingToolType::ParallelChannel,
649 vec![(3, 113.0), (9, 118.0), (5, 100.0)],
650 vec![(5, 100.0), (12, 93.0), (9, 122.0)],
651 ),
652 (
653 AutoDrawingKind::FibonacciRetracement,
654 DrawingToolType::FibonacciRetracement,
655 vec![(3, 113.0), (5, 100.0)],
656 vec![(5, 100.0), (9, 122.0)],
657 ),
658 (
659 AutoDrawingKind::FibonacciExtension,
660 DrawingToolType::FibonacciExtension,
661 vec![(3, 113.0), (5, 100.0), (9, 118.0)],
662 vec![(5, 100.0), (9, 122.0), (12, 93.0)],
663 ),
664 (
665 AutoDrawingKind::Pitchfork,
666 DrawingToolType::Pitchfork,
667 vec![(3, 113.0), (5, 100.0), (9, 118.0)],
668 vec![(5, 100.0), (9, 122.0), (12, 93.0)],
669 ),
670 ] {
671 let mut data = bars();
672 let name = AutoDrawing::new(kind, 5.0, 3).name().to_owned();
673 let mut study = ConfiguredStudy::new(IndicatorFactory::new().create(&name).unwrap());
674 study
675 .set_input("swing_size", InputValue::Choice(1))
676 .unwrap();
677 study.set_input("depth", InputValue::Length(3)).unwrap();
678 let check = |study: &ConfiguredStudy, expected: &[(usize, f64)]| {
679 assert_eq!(study.drawings().len(), 1, "{name}");
680 let drawing = &study.drawings()[0];
681 assert_eq!(drawing.tool_type, tool);
682 let points: Vec<_> = drawing
683 .chart_points
684 .iter()
685 .map(|p| (p.bar_idx as usize, p.price))
686 .collect();
687 assert_eq!(points, expected, "{name}");
688 if kind == AutoDrawingKind::FibonacciRetracement {
689 let mut drawing = drawing.clone();
690 drawing.update_screen_coords(|b| b, |p| p as f32);
691 assert_eq!(drawing.level_ys[0], expected[1].1 as f32);
693 assert_eq!(
694 drawing.level_ys[3],
695 ((expected[0].1 + expected[1].1) / 2.0) as f32
696 );
697 assert_eq!(drawing.level_ys[6], expected[0].1 as f32);
698 }
699 };
700 study.calculate(&data[..1]);
701 assert!(study.drawings().is_empty());
702 study.calculate(&data);
703 check(&study, &initial);
704 data[9].high = 122.0;
705 study.update(&data, 9);
706 let moved: Vec<_> = initial
707 .iter()
708 .map(|&(bar, p)| (bar, if bar == 9 { 122.0 } else { p }))
709 .collect();
710 check(&study, &moved);
711 for (i, low) in [95.0, 94.0, 93.0].into_iter().enumerate() {
712 data.push(Bar::new(
713 chrono::DateTime::from_timestamp((10 + i) as i64 * 60, 0).unwrap(),
714 low,
715 100.0,
716 low,
717 low,
718 1.0,
719 ));
720 }
721 study.update(&data, 10);
722 check(&study, &updated);
723 study
724 .set_input("deviation_percent", InputValue::Number(50.0))
725 .unwrap();
726 study.calculate(&data);
727 assert!(study.drawings().is_empty());
728 study
729 .set_input("deviation_percent", InputValue::Number(5.0))
730 .unwrap();
731 study.set_input("depth", InputValue::Length(100)).unwrap();
732 study.calculate(&data);
733 assert!(study.drawings().is_empty());
734 study.calculate(&[]);
735 assert!(study.drawings().is_empty() && study.values().is_empty());
736 }
737 }
738
739 #[test]
743 fn automatic_drawings_size_swings_by_average_true_range() {
744 let series = |count: usize, seconds: i64, h: f64, mid: &dyn Fn(usize) -> f64| {
745 (0..count)
746 .map(|i| {
747 let mid = mid(i);
748 let time = chrono::DateTime::from_timestamp(i as i64 * seconds, 0).unwrap();
749 Bar::new(time, mid, mid + h, mid - h, mid, 1.0)
750 })
751 .collect::<Vec<_>>()
752 };
753 let minute = series(50, 60, 1.0 / 64.0, &|i| {
756 let t = if i % 20 < 10 { i % 20 } else { 20 - i % 20 };
757 100.0 + t as f64 / 32.0
758 });
759 let daily = series(52, 86_400, 1.0, &|i| match i {
762 0..=11 => 100.0 + i as f64,
763 12..=26 => 111.0 - (i - 11) as f64,
764 27..=40 => 96.0 + (i - 26) as f64,
765 _ => 110.0 - 0.375 * (i - 40) as f64,
766 });
767 let (low, high) = (99.984375, 100.328125);
768 for (label, bars, split, expected) in [
769 (
770 "minute",
771 minute,
772 25,
773 [
774 vec![(20, low), (40, low)],
775 vec![(20, low), (40, low), (30, high)],
776 vec![(20, low), (30, high)],
777 vec![(20, low), (30, high), (40, low)],
778 vec![(20, low), (30, high), (40, low)],
779 ],
780 ),
781 (
782 "daily",
783 daily,
784 45,
785 [
786 vec![(11, 112.0), (40, 111.0)],
787 vec![(11, 112.0), (40, 111.0), (26, 95.0)],
788 vec![(11, 112.0), (26, 95.0)],
789 vec![(11, 112.0), (26, 95.0), (40, 111.0)],
790 vec![(11, 112.0), (26, 95.0), (40, 111.0)],
791 ],
792 ),
793 ] {
794 for (kind, expected) in AutoDrawingKind::ALL.into_iter().zip(expected) {
795 let name = AutoDrawing::new(kind, 5.0, 10).name().to_owned();
796 let mut study = IndicatorFactory::new().create(&name).unwrap();
797 let points = |study: &dyn Indicator| -> Vec<(usize, f64)> {
798 study.drawings()[0]
799 .chart_points
800 .iter()
801 .map(|p| (p.bar_idx as usize, p.price))
802 .collect()
803 };
804 study.calculate(&bars);
805 assert_eq!(points(study.as_ref()), expected, "{label} {name}");
806 study.calculate(&bars[..split]);
807 study.update(&bars, split);
808 assert_eq!(points(study.as_ref()), expected, "{label} {name} updated");
809 }
810 }
811 }
812
813 #[test]
815 fn automatic_drawing_uses_the_study_renderer_and_hit_test() {
816 use crate::chart::coords::ChartMapping;
817 use crate::chart::indicators::hit_test_indicator;
818 use crate::chart::rendering::{IndicatorRenderer, RenderContext};
819 use crate::scales::{PriceMap, PriceRange, PriceTransform};
820 use egui::{Pos2, Rect};
821 let rect = Rect::from_min_max(Pos2::ZERO, Pos2::new(400.0, 300.0));
822 let price = PriceMap::new(
823 PriceTransform::default(),
824 PriceRange::new(90.0, 130.0),
825 0.0,
826 300.0,
827 false,
828 );
829 let coords = ChartMapping::new(rect, 30.0, 0, 9, 0.0, price);
830 let data = bars();
831 let mut automatic = AutoDrawing::new(AutoDrawingKind::TrendLine, 5.0, 3);
832 automatic.calculate(&data);
833 let a = Pos2::new(coords.idx_to_x(3), price.price_to_y(113.0));
834 let b = Pos2::new(coords.idx_to_x(9), price.price_to_y(118.0));
835 let midpoint = a.lerp(b, 0.5);
836 for (drawing_visible, plot_visible) in [(true, true), (true, false), (false, true)] {
837 automatic.drawings[0].visible = drawing_visible;
838 let mut study = ConfiguredStudy::new(Box::new(automatic.clone()));
839 let mut settings = study.settings();
840 settings.plots[0].visible = plot_visible;
841 study.apply_settings(&[], settings).unwrap();
842 let output = egui::Context::default().run_ui(egui::RawInput::default(), |ui| {
843 IndicatorRenderer::render_study(
844 &RenderContext::new(ui.painter(), rect),
845 &study,
846 &data,
847 0,
848 &price,
849 &coords,
850 );
851 });
852 let painted = output.shapes.iter().any(|s| matches!(&s.shape, egui::Shape::LineSegment{points,..} if points[0].distance(a)<0.01 && points[1].distance(b)<0.01));
853 output.drop_without_applying_deltas();
854 let visible = drawing_visible && plot_visible;
855 assert_eq!(painted, visible);
856 assert_eq!(
857 hit_test_indicator(midpoint, &study, 0, 0..10, &coords, |p| price.price_to_y(p))
858 .is_some(),
859 visible
860 );
861 assert_eq!(
862 crate::studies::plots::drawing_prices(&study, 0..10)
863 .next()
864 .is_some(),
865 visible
866 );
867 }
868
869 automatic.drawings[0].visible = true;
871 let mut study = ConfiguredStudy::new(Box::new(automatic));
872 study.set_scale(crate::studies::StudyScale::Independent);
873 let zoomed = ChartMapping::new(rect, 400.0, 6, 7, 0.0, price);
874 let mapped = crate::chart::rendering::study_mapping(&study, &zoomed);
875 for value in [113.0, 118.0] {
876 assert!((0.0..=300.0).contains(&mapped.price_to_y(value)));
877 }
878
879 let mut extension = AutoDrawing::new(AutoDrawingKind::FibonacciExtension, 5.0, 3);
881 extension.calculate(&data);
882 let prices: Vec<_> = crate::studies::plots::drawing_prices(&extension, 10..12).collect();
883 assert!(prices.iter().any(|p| (p - 83.966).abs() < 1e-4));
885 let mut state = crate::studies::ChartState::new(crate::model::BarData::from_bars(data));
886 state.time_scale_mut().set_width(400.0);
887 let transform = state.fit_price_scale([&extension as &dyn Indicator]);
888 let range = state.price_scale().range();
889 assert!(transform.to_price(range.min) <= 83.966);
890 assert!(transform.to_price(range.max) >= 118.0);
891 let mut extension = ConfiguredStudy::new(Box::new(extension));
892 let mut hidden = extension.settings();
893 hidden.plots[0].visible = false;
894 extension.apply_settings(&[], hidden).unwrap();
895 assert!(
896 crate::studies::plots::drawing_prices(&extension, 10..12)
897 .next()
898 .is_none()
899 );
900
901 let mut extension = AutoDrawing::new(AutoDrawingKind::FibonacciExtension, 5.0, 3)
903 .with_fibonacci_config(FibonacciConfig {
904 show_background: false,
905 extend_right: false,
906 ..FibonacciConfig::extension_default()
907 });
908 extension.calculate(&bars());
909 for (visible, reaches) in [
910 (0..3, false),
911 (3..5, true),
912 (5..6, true),
913 (6..9, false),
914 (9..10, false),
915 ] {
916 assert_eq!(
917 crate::studies::plots::drawing_prices(&extension, visible.clone())
918 .next()
919 .is_some(),
920 reaches,
921 "{visible:?}"
922 );
923 }
924 }
925}