Skip to main content

egui_charts/studies/builtin/
parabolic_sar.rs

1//! Parabolic SAR (Stop and Reverse) Indicator
2//!
3//! Developed by J. Welles Wilder Jr., the Parabolic SAR provides potential
4//! entry and exit points by plotting a trailing stop-and-reverse point.
5//!
6//! # Formula
7//! SARn+1 = SARn + AF * (EP - SARn)
8//! Where:
9//! - AF = Acceleration Factor (starts at initial_af, increases by af_step up to max_af)
10//! - EP = Extreme Point (highest high in uptrend, lowest low in downtrend)
11//!
12//! # Interpretation
13//! - SAR below price: Uptrend (SAR acts as support/trailing stop)
14//! - SAR above price: Downtrend (SAR acts as resistance/trailing stop)
15//! - When price crosses SAR: Trend reversal signal
16//!
17//! The SAR is output as two lines, the uptrend SAR and the downtrend SAR; each bar has a
18//! value on exactly one of them, the other is NaN.
19//!
20//! # Example
21//! ```
22//! # let bars: Vec<egui_charts::model::Bar> = (0..60).map(|i| { let p = 100.0 + (i as f64 * 0.3).sin() * 5.0; egui_charts::model::Bar::new(chrono::Utc::now(), p, p + 1.0, p - 1.0, p + 0.5, 1_000.0) }).collect();
23//! use egui_charts::studies::{ParabolicSAR, Indicator};
24//!
25//! let mut psar = ParabolicSAR::new(0.02, 0.02, 0.2);
26//! psar.calculate(&bars);
27//! ```
28
29use super::clamp_param;
30use crate::model::Bar;
31use crate::studies::palette;
32use crate::studies::stepper::Stepper;
33use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
34use egui::Color32;
35
36/// Parabolic SAR indicator
37#[derive(Clone)]
38pub struct ParabolicSAR {
39    initial_af: f64,
40    af_step: f64,
41    max_af: f64,
42    values: IndicatorValues,
43    stepper: Stepper<State>,
44    bullish_color: Color32,
45    bearish_color: Color32,
46    visible: bool,
47}
48
49impl ParabolicSAR {
50    /// Create a new Parabolic SAR indicator
51    ///
52    /// # Arguments
53    /// * `initial_af` - Starting acceleration factor (typically 0.02)
54    /// * `af_step` - AF increment (typically 0.02)
55    /// * `max_af` - Max AF (typically 0.2)
56    ///
57    /// `initial_af` and `af_step` are clamped into 0.001 to 1, NaN to 0.001, and
58    /// `max_af` into `initial_af` to 1, NaN to `initial_af`.
59    pub fn new(initial_af: f64, af_step: f64, max_af: f64) -> Self {
60        let initial_af = clamp_param(initial_af, 0.001, 1.0);
61        Self {
62            initial_af,
63            af_step: clamp_param(af_step, 0.001, 1.0),
64            max_af: clamp_param(max_af, initial_af, 1.0),
65            values: IndicatorValues::new(),
66            stepper: Stepper::default(),
67            bullish_color: palette::BLUE,
68            bearish_color: palette::BLUE,
69            visible: true,
70        }
71    }
72
73    /// Create with default params (0.02, 0.02, 0.2)
74    pub fn default_params() -> Self {
75        Self::new(0.02, 0.02, 0.2)
76    }
77
78    /// Set colors for bullish and bearish SAR points
79    pub fn with_colors(mut self, bullish: Color32, bearish: Color32) -> Self {
80        self.bullish_color = bullish;
81        self.bearish_color = bearish;
82        self
83    }
84}
85
86/// Construct with the conventional default parameters.
87impl Default for ParabolicSAR {
88    fn default() -> Self {
89        Self::new(0.02, 0.02, 0.2)
90    }
91}
92
93#[derive(Clone, Default)]
94struct State {
95    previous: Option<[f64; 3]>,
96    before_previous: Option<[f64; 2]>,
97    sar: f64,
98    ep: f64,
99    af: f64,
100    up: bool,
101}
102
103impl Indicator for ParabolicSAR {
104    crate::studies::inputs::study_inputs!(self;
105        initial_af: number("Initial af", 0.001, 1.0),
106        af_step: number("Af step", 0.001, 1.0),
107        max_af: number("Max af", 0.001, 1.0)
108    ; { self.max_af = self.max_af.max(self.initial_af); });
109
110    fn plot_style(&self, line: usize) -> crate::studies::PlotStyle {
111        crate::studies::PlotStyle {
112            color: self.colors().get(line).copied().unwrap_or(Color32::WHITE),
113            width: 1.0,
114            pattern: crate::model::LineStyle::Solid,
115            plot_type: crate::studies::PlotType::Crosses,
116            visible: true,
117        }
118    }
119
120    fn name(&self) -> &str {
121        "PSAR"
122    }
123
124    fn desc(&self) -> &str {
125        "Parabolic SAR - Trend following stop-and-reverse"
126    }
127
128    fn calculate(&mut self, data: &[Bar]) {
129        self.update(data, 0);
130    }
131
132    fn update(&mut self, data: &[Bar], from: usize) {
133        self.stepper
134            .run(data, from, &mut self.values, |state, bar| {
135                if !bar.high.is_finite() || !bar.low.is_finite() || !bar.close.is_finite() {
136                    return IndicatorValue::None;
137                }
138                let current = [bar.high, bar.low, bar.close];
139                let Some(previous) = state.previous.replace(current) else {
140                    return IndicatorValue::None;
141                };
142                if let Some(before) = state.before_previous {
143                    let mut next = state.sar + state.af * (state.ep - state.sar);
144                    let reversal = if state.up {
145                        next = next.min(previous[1]).min(before[1]);
146                        bar.low < next
147                    } else {
148                        next = next.max(previous[0]).max(before[0]);
149                        bar.high > next
150                    };
151                    if reversal {
152                        state.up = !state.up;
153                        state.sar = state.ep;
154                        state.ep = if state.up { bar.high } else { bar.low };
155                        state.af = self.initial_af;
156                    } else {
157                        state.sar = next;
158                        if (state.up && bar.high > state.ep) || (!state.up && bar.low < state.ep) {
159                            state.ep = if state.up { bar.high } else { bar.low };
160                            state.af = (state.af + self.af_step).min(self.max_af);
161                        }
162                    }
163                } else {
164                    state.up = bar.close >= previous[2];
165                    state.sar = if state.up { previous[1] } else { previous[0] };
166                    state.ep = if state.up {
167                        bar.high.max(previous[0])
168                    } else {
169                        bar.low.min(previous[1])
170                    };
171                    state.af = self.initial_af;
172                }
173                state.before_previous = Some([previous[0], previous[1]]);
174                IndicatorValue::Multiple(if state.up {
175                    vec![state.sar, f64::NAN]
176                } else {
177                    vec![f64::NAN, state.sar]
178                })
179            });
180    }
181
182    fn is_incremental(&self) -> bool {
183        true
184    }
185
186    fn values(&self) -> &IndicatorValues {
187        &self.values
188    }
189
190    fn colors(&self) -> Vec<Color32> {
191        vec![self.bullish_color, self.bearish_color]
192    }
193
194    fn set_colors(&mut self, colors: Vec<Color32>) {
195        if !colors.is_empty() {
196            self.bullish_color = colors[0];
197        }
198        if colors.len() > 1 {
199            self.bearish_color = colors[1];
200        }
201    }
202
203    fn is_overlay(&self) -> bool {
204        true // PSAR is drawn on main chart
205    }
206
207    fn is_visible(&self) -> bool {
208        self.visible
209    }
210
211    fn set_visible(&mut self, visible: bool) {
212        self.visible = visible;
213    }
214
215    fn clone_box(&self) -> Box<dyn Indicator> {
216        Box::new(self.clone())
217    }
218
219    fn line_names(&self) -> Vec<String> {
220        vec!["Up Trend".to_string(), "Down Trend".to_string()]
221    }
222}