Skip to main content

finance_solution/stocks/ta/
sar.rs

1//! # Parabolic SAR (Stop and Reverse)
2//!
3//! Classic Welles Wilder trailing stop:
4//!
5//! ```text
6//! SAR_t = SAR_{t−1} + AF * (EP − SAR_{t−1})
7//! ```
8//!
9//! with acceleration factor `AF` starting at `start`, stepping by `increment` up to `maximum`
10//! when EP makes new extremes, and flipping long/short when price crosses SAR.
11//!
12//! Default: `(0.02, 0.02, 0.20)` ([`SarParams::standard`]).
13//!
14//! First bar is warm-up (`None`); SAR starts at bar 1.
15//!
16//! ---
17//!
18//! ## Trading perspective
19//!
20//! | Reading | Habit (classic) |
21//! |---------|-----------------|
22//! | SAR below price (dir +1) | Long / trail under |
23//! | SAR above price (dir −1) | Short / trail over |
24//! | Flip | Stop-and-reverse system signal |
25//!
26//! SAR is **aggressive** in chop (many flips). Often filtered by ADX or a slow MA.
27//!
28//! ## vs Supertrend
29//!
30//! | | SAR | Supertrend |
31//! |--|-----|------------|
32//! | Mechanism | AF toward extreme point | ATR bands around mid |
33//! | Feel | Can hug price tightly | Smoother ATR trail |
34//! | Choppy markets | Whipsaws more | Mult tuning helps |
35//!
36//! Many desks pick **one** primary trail (SAR *or* Supertrend), not both as simultaneous
37//! entry signals.
38//!
39//! ## Pairs well with
40//!
41//! - **ADX** — only reverse with SAR when ADX shows trend (or only *enter* with SAR + ADX).
42//! - **RSI** — avoid long SAR flips into overbought extremes without confirmation.
43//! - **ATR** — independent stop size check (SAR distance ≠ risk budget).
44//!
45//! ---
46//!
47//! ## Engineering
48//!
49//! [`SarParams`] → [`sar`] / [`SarState`] → [`sar_solution`]. Batch via state.  
50//! Initial long/short seed uses a simple first-bar extreme heuristic (documented convention;
51//! platforms differ slightly on the very first flip).
52
53use crate::stocks::ta::common::{opt_cell, require_hlc};
54use crate::util::error::{require_finite, FinanceError, FinanceResult};
55use crate::{columns_with_strings, print_table_locale_opt};
56
57/// Parabolic SAR acceleration parameters.
58#[derive(Clone, Copy, Debug, PartialEq)]
59pub struct SarParams {
60    pub start: f64,
61    pub increment: f64,
62    pub maximum: f64,
63}
64
65impl SarParams {
66    pub const fn new(start: f64, increment: f64, maximum: f64) -> Self {
67        Self {
68            start,
69            increment,
70            maximum,
71        }
72    }
73
74    /// Classic `(0.02, 0.02, 0.20)`.
75    pub const fn standard() -> Self {
76        Self {
77            start: 0.02,
78            increment: 0.02,
79            maximum: 0.20,
80        }
81    }
82}
83
84#[derive(Clone, Copy, Debug, PartialEq)]
85pub struct ValidatedSar {
86    params: SarParams,
87}
88
89impl ValidatedSar {
90    pub fn new(params: SarParams) -> FinanceResult<Self> {
91        require_finite("start", params.start)?;
92        require_finite("increment", params.increment)?;
93        require_finite("maximum", params.maximum)?;
94        if params.start <= 0.0 || params.increment <= 0.0 || params.maximum < params.start {
95            return Err(FinanceError::Unsolvable {
96                message: "SAR requires start>0, increment>0, maximum>=start",
97            });
98        }
99        Ok(Self { params })
100    }
101
102    pub fn params(self) -> SarParams {
103        self.params
104    }
105
106    pub fn compute(self, high: &[f64], low: &[f64], close: &[f64]) -> FinanceResult<SarSeries> {
107        sar_validated(high, low, close, self)
108    }
109}
110
111#[derive(Clone, Debug, PartialEq)]
112pub struct SarSeries {
113    pub sar: Vec<Option<f64>>,
114    /// `+1` long (SAR below), `−1` short (SAR above) when defined.
115    pub direction: Vec<Option<i8>>,
116    pub params: SarParams,
117}
118
119#[derive(Clone, Copy, Debug, PartialEq)]
120pub struct SarBarOutput {
121    pub sar: f64,
122    pub direction: i8,
123}
124
125/// Incremental Parabolic SAR.
126#[derive(Clone, Debug)]
127pub struct SarState {
128    params: SarParams,
129    // After first bar setup:
130    is_long: bool,
131    af: f64,
132    ep: f64,
133    sar: f64,
134    prev_high: f64,
135    prev_low: f64,
136    prev2_high: Option<f64>,
137    prev2_low: Option<f64>,
138    started: bool,
139    last: Option<SarBarOutput>,
140}
141
142impl SarState {
143    pub fn new(params: SarParams) -> FinanceResult<Self> {
144        let _ = ValidatedSar::new(params)?;
145        Ok(Self {
146            params,
147            is_long: true,
148            af: params.start,
149            ep: 0.0,
150            sar: 0.0,
151            prev_high: 0.0,
152            prev_low: 0.0,
153            prev2_high: None,
154            prev2_low: None,
155            started: false,
156            last: None,
157        })
158    }
159
160    pub fn from_history(
161        params: SarParams,
162        high: &[f64],
163        low: &[f64],
164        close: &[f64],
165    ) -> FinanceResult<Self> {
166        let mut s = Self::new(params)?;
167        let _ = s.push_bars(high, low, close)?;
168        Ok(s)
169    }
170
171    pub fn params(&self) -> SarParams {
172        self.params
173    }
174
175    pub fn reset(&mut self) {
176        *self = Self::new(self.params).expect("params already valid");
177    }
178
179    pub fn push(&mut self, high: f64, low: f64, close: f64) -> FinanceResult<Option<SarBarOutput>> {
180        require_finite("high", high)?;
181        require_finite("low", low)?;
182        require_finite("close", close)?;
183        if high < low {
184            return Err(FinanceError::InvalidCashflow {
185                message: "high must be >= low for each bar",
186            });
187        }
188
189        if !self.started {
190            // Seed on first bar only (no SAR yet).
191            self.prev_high = high;
192            self.prev_low = low;
193            self.started = true;
194            self.last = None;
195            return Ok(None);
196        }
197
198        if self.last.is_none() {
199            // Initialize SAR at second bar from first bar extremes.
200            self.is_long = close >= self.prev_high; // prefer long if ambiguous
201            if high > self.prev_high {
202                self.is_long = true;
203            } else if low < self.prev_low {
204                self.is_long = false;
205            }
206            if self.is_long {
207                self.sar = self.prev_low;
208                self.ep = high.max(self.prev_high);
209            } else {
210                self.sar = self.prev_high;
211                self.ep = low.min(self.prev_low);
212            }
213            self.af = self.params.start;
214            let dir = if self.is_long { 1 } else { -1 };
215            let bar = SarBarOutput {
216                sar: self.sar,
217                direction: dir,
218            };
219            self.prev2_high = Some(self.prev_high);
220            self.prev2_low = Some(self.prev_low);
221            self.prev_high = high;
222            self.prev_low = low;
223            self.last = Some(bar);
224            return Ok(Some(bar));
225        }
226
227        // Advance SAR
228        let mut sar = self.sar + self.af * (self.ep - self.sar);
229
230        if self.is_long {
231            // SAR cannot be above prior two lows
232            sar = sar.min(self.prev_low);
233            if let Some(l2) = self.prev2_low {
234                sar = sar.min(l2);
235            }
236            if low < sar {
237                // flip to short
238                self.is_long = false;
239                sar = self.ep;
240                self.ep = low;
241                self.af = self.params.start;
242            } else {
243                if high > self.ep {
244                    self.ep = high;
245                    self.af = (self.af + self.params.increment).min(self.params.maximum);
246                }
247            }
248        } else {
249            sar = sar.max(self.prev_high);
250            if let Some(h2) = self.prev2_high {
251                sar = sar.max(h2);
252            }
253            if high > sar {
254                // flip to long
255                self.is_long = true;
256                sar = self.ep;
257                self.ep = high;
258                self.af = self.params.start;
259            } else if low < self.ep {
260                self.ep = low;
261                self.af = (self.af + self.params.increment).min(self.params.maximum);
262            }
263        }
264
265        self.sar = sar;
266        let dir = if self.is_long { 1 } else { -1 };
267        let bar = SarBarOutput {
268            sar,
269            direction: dir,
270        };
271        self.prev2_high = Some(self.prev_high);
272        self.prev2_low = Some(self.prev_low);
273        self.prev_high = high;
274        self.prev_low = low;
275        self.last = Some(bar);
276        Ok(Some(bar))
277    }
278
279    pub fn push_bars(
280        &mut self,
281        high: &[f64],
282        low: &[f64],
283        close: &[f64],
284    ) -> FinanceResult<Vec<Option<SarBarOutput>>> {
285        require_hlc(high, low, close)?;
286        let mut out = Vec::with_capacity(close.len());
287        for i in 0..close.len() {
288            out.push(self.push(high[i], low[i], close[i])?);
289        }
290        Ok(out)
291    }
292
293    pub fn last(&self) -> Option<SarBarOutput> {
294        self.last
295    }
296}
297
298pub fn sar(
299    high: &[f64],
300    low: &[f64],
301    close: &[f64],
302    params: SarParams,
303) -> FinanceResult<SarSeries> {
304    ValidatedSar::new(params)?.compute(high, low, close)
305}
306
307fn sar_validated(
308    high: &[f64],
309    low: &[f64],
310    close: &[f64],
311    eng: ValidatedSar,
312) -> FinanceResult<SarSeries> {
313    let mut st = SarState::new(eng.params)?;
314    let bars = st.push_bars(high, low, close)?;
315    let n = bars.len();
316    let mut sar = vec![None; n];
317    let mut direction = vec![None; n];
318    for (i, b) in bars.into_iter().enumerate() {
319        if let Some(bar) = b {
320            sar[i] = Some(bar.sar);
321            direction[i] = Some(bar.direction);
322        }
323    }
324    Ok(SarSeries {
325        sar,
326        direction,
327        params: eng.params,
328    })
329}
330
331#[derive(Clone, Debug)]
332pub struct SarSolution {
333    series: SarSeries,
334    close: Vec<f64>,
335    formula: String,
336}
337
338impl SarSolution {
339    pub fn series(&self) -> &SarSeries {
340        &self.series
341    }
342    pub fn formula(&self) -> &str {
343        &self.formula
344    }
345
346    pub fn print_table(&self) {
347        self.print_table_locale_opt(None, None);
348    }
349
350    pub fn print_table_locale(&self, locale: &num_format::Locale, precision: usize) {
351        self.print_table_locale_opt(Some(locale), Some(precision));
352    }
353
354    fn print_table_locale_opt(
355        &self,
356        locale: Option<&num_format::Locale>,
357        precision: Option<usize>,
358    ) {
359        let columns = columns_with_strings(&[
360            ("period", "i", true),
361            ("close", "f", true),
362            ("sar", "f", true),
363            ("dir", "i", true),
364        ]);
365        let data = self
366            .close
367            .iter()
368            .enumerate()
369            .map(|(i, c)| {
370                let d = self.series.direction[i]
371                    .map(|x| x.to_string())
372                    .unwrap_or_else(|| "n/a".to_string());
373                vec![
374                    i.to_string(),
375                    c.to_string(),
376                    opt_cell(self.series.sar[i]),
377                    d,
378                ]
379            })
380            .collect();
381        print_table_locale_opt(&columns, data, locale, precision);
382    }
383}
384
385/// # Examples
386/// ```
387/// use finance_solution::stocks::ta::{sar_solution, SarParams};
388/// let n = 30usize;
389/// let h: Vec<_> = (0..n).map(|i| 101.0 + i as f64 * 0.1).collect();
390/// let l: Vec<_> = (0..n).map(|i| 99.0 + i as f64 * 0.1).collect();
391/// let c: Vec<_> = (0..n).map(|i| 100.0 + i as f64 * 0.1).collect();
392/// let sol = sar_solution(&h, &l, &c, SarParams::standard()).unwrap();
393/// assert!(sol.formula().contains("0.02"));
394/// ```
395pub fn sar_solution(
396    high: &[f64],
397    low: &[f64],
398    close: &[f64],
399    params: SarParams,
400) -> FinanceResult<SarSolution> {
401    let series = sar(high, low, close, params)?;
402    Ok(SarSolution {
403        series,
404        close: close.to_vec(),
405        formula: format!(
406            "Parabolic SAR AF start={} step={} max={}",
407            params.start, params.increment, params.maximum
408        ),
409    })
410}
411
412#[cfg(test)]
413mod tests {
414    use super::*;
415
416    #[test]
417    fn produces_sar() {
418        let n = 40usize;
419        let h: Vec<_> = (0..n).map(|i| 101.0 + i as f64 * 0.15).collect();
420        let l: Vec<_> = (0..n).map(|i| 99.0 + i as f64 * 0.15).collect();
421        let c: Vec<_> = (0..n).map(|i| 100.0 + i as f64 * 0.15).collect();
422        let s = sar(&h, &l, &c, SarParams::standard()).unwrap();
423        assert!(s.sar[0].is_none());
424        assert!(s.sar[1].is_some());
425        assert!(s.sar.iter().filter(|x| x.is_some()).count() > 20);
426    }
427
428    #[test]
429    fn state_parity() {
430        let n = 30usize;
431        let h: Vec<_> = (0..n).map(|i| 12.0 + (i as f64 * 0.1).sin()).collect();
432        let l: Vec<_> = (0..n).map(|i| 10.0 + (i as f64 * 0.1).sin()).collect();
433        let c: Vec<_> = (0..n).map(|i| 11.0 + (i as f64 * 0.1).sin()).collect();
434        let p = SarParams::standard();
435        let batch = sar(&h, &l, &c, p).unwrap();
436        let mut st = SarState::new(p).unwrap();
437        for i in 0..n {
438            let o = st.push(h[i], l[i], c[i]).unwrap();
439            match (o, batch.sar[i], batch.direction[i]) {
440                (None, None, None) => {}
441                (Some(bar), Some(v), Some(d)) => {
442                    assert!((bar.sar - v).abs() < 1e-9, "i={i}");
443                    assert_eq!(bar.direction, d);
444                }
445                other => panic!("i={i}: {other:?}"),
446            }
447        }
448    }
449
450    #[test]
451    fn bad_params_err() {
452        assert!(ValidatedSar::new(SarParams::new(0.0, 0.02, 0.2)).is_err());
453    }
454}