Skip to main content

egui_charts/drawings/domain/
alert_levels.rs

1//! Alert levels for drawings whose lines depend on three anchors or a fitted bar range.
2
3use super::{Drawing, DrawingToolType, PitchforkConfig};
4use crate::model::{Bar, math::Moments};
5use crate::scales::PriceScaleMode;
6
7impl Drawing {
8    /// [`Self::alert_levels`] with the main-series bars needed by a regression drawing. The
9    /// fitted median is level 0, upper band 1 and lower band 2. Regression uses the same closes,
10    /// residual deviation and axis interpolation between fitted endpoints as its painted lines.
11    /// Empty when the anchored range has fewer than two bars or invalid geometry.
12    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
147/// A straight painted segment in axis coordinates, extended to the requested bar.
148fn 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}