egui_charts/drawings/domain/
alert_levels.rs1use super::{Drawing, DrawingToolType, PitchforkConfig};
4use crate::model::{Bar, math::Moments};
5use crate::scales::PriceScaleMode;
6
7impl Drawing {
8 pub fn alert_levels_with_bars(
13 &self,
14 bar: f64,
15 scale: PriceScaleMode,
16 bars: &[Bar],
17 ) -> Vec<(usize, String, f64)> {
18 if self.tool_type != DrawingToolType::RegressionTrend {
19 return self.alert_levels(bar, scale);
20 }
21 let &[a, b, ..] = self.chart_points.as_slice() else {
22 return Vec::new();
23 };
24 if !bar.is_finite() || !a.bar_idx.is_finite() || !b.bar_idx.is_finite() {
25 return Vec::new();
26 }
27 let Some((first, bars)) =
28 crate::drawings::rendering::bars_between(bars, a.bar_idx, b.bar_idx)
29 else {
30 return Vec::new();
31 };
32 let Some(fit) = Moments::of(bars).fit() else {
33 return Vec::new();
34 };
35 if !fit.intercept.is_finite() || !fit.slope.is_finite() || !fit.sigma.is_finite() {
36 return Vec::new();
37 }
38 let deviations = self.regression_trend.unwrap_or_default().deviations();
39 let last = (bars.len() - 1) as f64;
40 [
41 ("Median", 0.0),
42 ("Upper", deviations),
43 ("Lower", -deviations),
44 ]
45 .into_iter()
46 .enumerate()
47 .filter_map(|(level, (name, k))| {
48 let start = fit.intercept + k * fit.sigma;
49 let end = start + fit.slope * last;
50 axis_line(
51 [first as f64, start],
52 [first as f64 + last, end],
53 bar,
54 scale,
55 )
56 .map(|price| (level, name.to_owned(), price))
57 })
58 .collect()
59 }
60
61 pub(super) fn fork_alert_levels(
62 &self,
63 bar: f64,
64 scale: PriceScaleMode,
65 ) -> Vec<(usize, String, f64)> {
66 let &[a, b, c, ..] = self.chart_points.as_slice() else {
67 return Vec::new();
68 };
69 let log = scale == PriceScaleMode::Logarithmic;
70 let points = [a, b, c].map(|p| {
71 [
72 f64::from(p.bar_idx),
73 if log { p.price.ln() } else { p.price },
74 ]
75 });
76 if !bar.is_finite() || points.iter().flatten().any(|v| !v.is_finite()) {
77 return Vec::new();
78 }
79 let [a, b, c] = points;
80 let between =
81 |a: [f64; 2], b: [f64; 2], t: f64| [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
82 let middle = between(b, c, 0.5);
83 let origin = match self.tool_type {
84 DrawingToolType::SchiffPitchfork => [a[0], (a[1] + b[1]) / 2.0],
85 DrawingToolType::ModifiedSchiffPitchfork => between(a, b, 0.5),
86 DrawingToolType::InsidePitchfork => middle,
87 _ => a,
88 };
89 let direction = if self.tool_type == DrawingToolType::InsidePitchfork {
90 let mid = between(a, b, 0.5);
91 [c[0] - mid[0], c[1] - mid[1]]
92 } else {
93 [middle[0] - origin[0], middle[1] - origin[1]]
94 };
95 let config = self
96 .pitchfork
97 .clone()
98 .unwrap_or_else(PitchforkConfig::legacy);
99 let fan = self.tool_type == DrawingToolType::Pitchfan;
100 let mut levels = Vec::new();
101 for (index, level) in config.levels.iter().enumerate().filter(|(_, l)| {
102 l.visible && (0.0..=crate::drawings::validation::MAX_PITCHFORK_LEVEL).contains(&l.value)
103 }) {
104 for (side, anchor) in
105 [b, c]
106 .into_iter()
107 .enumerate()
108 .take(if level.value == 0.0 { 1 } else { 2 })
109 {
110 let value = f64::from(level.value);
111 let (start, end) = if fan {
112 (origin, between(middle, anchor, value))
113 } else {
114 let start = between(origin, anchor, value);
115 (start, [start[0] + direction[0], start[1] + direction[1]])
116 };
117 let dx = end[0] - start[0];
118 let back = !fan
119 && if dx < 0.0 {
120 self.extend_right
121 } else {
122 self.extend_left
123 };
124 if dx == 0.0 || (!back && (bar - start[0]) * dx < 0.0) {
125 continue;
126 }
127 let axis = start[1] + (bar - start[0]) * (end[1] - start[1]) / dx;
128 let price = if log { axis.exp() } else { axis };
129 if price.is_finite() && (!log || price > 0.0) {
130 let name = if level.value == 0.0 {
131 "Median".to_owned()
132 } else {
133 format!(
134 "{}: {} line",
135 level.value,
136 if side == 0 { "first" } else { "second" }
137 )
138 };
139 levels.push((2 * index + side, name, price));
140 }
141 }
142 }
143 levels
144 }
145}
146
147fn axis_line(a: [f64; 2], b: [f64; 2], bar: f64, scale: PriceScaleMode) -> Option<f64> {
149 let log = scale == PriceScaleMode::Logarithmic;
150 let [from, to] = [a[1], b[1]].map(|p| if log { p.ln() } else { p });
151 if !from.is_finite() || !to.is_finite() || a[0] == b[0] {
152 return None;
153 }
154 let value = from + (bar - a[0]) * (to - from) / (b[0] - a[0]);
155 let price = if log { value.exp() } else { value };
156 (price.is_finite() && (!log || price > 0.0)).then_some(price)
157}