Skip to main content

finance_solution/stocks/ta/
supertrend.rs

1//! # Supertrend
2//!
3//! ATR-based trailing band (common breakout / stop overlay):
4//!
5//! ```text
6//! mid = (high + low) / 2
7//! basic_upper = mid + mult * ATR
8//! basic_lower = mid − mult * ATR
9//! final bands stick until price closes through the opposite band
10//! Supertrend = final_lower in uptrend, final_upper in downtrend
11//! ```
12//!
13//! Default: ATR period **10**, mult **3.0** ([`SupertrendParams::standard`]).
14//!
15//! Output: [`SupertrendBar`] with `value` and `direction` (`+1` uptrend / `−1` downtrend).
16//!
17//! ---
18//!
19//! ## Trading perspective
20//!
21//! | Event | Habit (classic) |
22//! |-------|-----------------|
23//! | Direction flips to +1 | Long regime / trail under price |
24//! | Direction flips to −1 | Short regime / trail over price |
25//! | Price pulls to ST line | Support/resistance screen in trend |
26//!
27//! Supertrend is a **stop + regime** tool, not an oscillator. Mult↑ → fewer flips, wider trail.
28//!
29//! ## vs Parabolic SAR / Donchian / Keltner
30//!
31//! | | Supertrend | SAR | Donchian | Keltner |
32//! |--|------------|-----|----------|---------|
33//! | Driver | ATR × mid | Acceleration on extremes | Pure HH/LL | EMA + ATR |
34//! | Flip style | Close through band | Touch SAR | Channel break | Band tag |
35//! | Best for | ATR-scaled trail | Fast reverse systems | Breakout structure | Channel mean reversion |
36//!
37//! ## Pairs well with
38//!
39//! - **ADX** — enter Supertrend flips only when ADX rising / above threshold.
40//! - **Volume (OBV/RVOL)** — confirm breakout volume.
41//! - **Higher-timeframe MA** — only long Supertrend with HTF trend.
42//!
43//! ---
44//!
45//! ## Engineering
46//!
47//! [`SupertrendParams`] → [`supertrend`] / [`SupertrendState`] → [`supertrend_solution`].  
48//! Uses shared [`AtrState`] (Wilder). Batch via state.  
49//! First defined bar seeds direction from close vs mid (implementation convention).
50
51use crate::stocks::ta::atr::{AtrParams, AtrState};
52use crate::stocks::ta::common::{opt_cell, require_hlc};
53use crate::util::error::{require_finite, FinanceError, FinanceResult};
54use crate::{columns_with_strings, print_table_locale_opt};
55
56/// Supertrend pack: Wilder ATR period + band multiplier.
57#[derive(Clone, Copy, Debug, PartialEq)]
58pub struct SupertrendParams {
59    pub atr_period: usize,
60    pub multiplier: f64,
61}
62
63impl SupertrendParams {
64    pub const fn new(atr_period: usize, multiplier: f64) -> Self {
65        Self {
66            atr_period,
67            multiplier,
68        }
69    }
70
71    /// Common `(10, 3.0)`.
72    pub const fn standard() -> Self {
73        Self {
74            atr_period: 10,
75            multiplier: 3.0,
76        }
77    }
78}
79
80#[derive(Clone, Copy, Debug, PartialEq)]
81pub struct ValidatedSupertrend {
82    params: SupertrendParams,
83}
84
85impl ValidatedSupertrend {
86    pub fn new(params: SupertrendParams) -> FinanceResult<Self> {
87        crate::util::primitives::PeriodLength::new(params.atr_period)?;
88        require_finite("multiplier", params.multiplier)?;
89        if params.multiplier <= 0.0 {
90            return Err(FinanceError::Unsolvable {
91                message: "supertrend multiplier must be positive",
92            });
93        }
94        Ok(Self { params })
95    }
96
97    pub fn params(self) -> SupertrendParams {
98        self.params
99    }
100
101    pub fn compute(
102        self,
103        high: &[f64],
104        low: &[f64],
105        close: &[f64],
106    ) -> FinanceResult<SupertrendSeries> {
107        supertrend_validated(high, low, close, self)
108    }
109}
110
111#[derive(Clone, Copy, Debug, PartialEq)]
112pub struct SupertrendBar {
113    pub value: f64,
114    /// `+1` uptrend (line under price), `−1` downtrend (line over price).
115    pub direction: i8,
116}
117
118#[derive(Clone, Debug, PartialEq)]
119pub struct SupertrendSeries {
120    pub value: Vec<Option<f64>>,
121    pub direction: Vec<Option<i8>>,
122    pub params: SupertrendParams,
123}
124
125/// Incremental Supertrend.
126#[derive(Clone, Debug)]
127pub struct SupertrendState {
128    params: SupertrendParams,
129    atr: AtrState,
130    prev_close: Option<f64>,
131    final_upper: Option<f64>,
132    final_lower: Option<f64>,
133    /// Last direction: +1 / −1
134    direction: Option<i8>,
135    last: Option<SupertrendBar>,
136}
137
138impl SupertrendState {
139    pub fn new(params: SupertrendParams) -> FinanceResult<Self> {
140        let _ = ValidatedSupertrend::new(params)?;
141        Ok(Self {
142            params,
143            atr: AtrState::new(AtrParams::new(params.atr_period))?,
144            prev_close: None,
145            final_upper: None,
146            final_lower: None,
147            direction: None,
148            last: None,
149        })
150    }
151
152    pub fn from_history(
153        params: SupertrendParams,
154        high: &[f64],
155        low: &[f64],
156        close: &[f64],
157    ) -> FinanceResult<Self> {
158        let mut s = Self::new(params)?;
159        let _ = s.push_bars(high, low, close)?;
160        Ok(s)
161    }
162
163    pub fn params(&self) -> SupertrendParams {
164        self.params
165    }
166
167    pub fn reset(&mut self) {
168        self.atr.reset();
169        self.prev_close = None;
170        self.final_upper = None;
171        self.final_lower = None;
172        self.direction = None;
173        self.last = None;
174    }
175
176    pub fn push(
177        &mut self,
178        high: f64,
179        low: f64,
180        close: f64,
181    ) -> FinanceResult<Option<SupertrendBar>> {
182        require_finite("high", high)?;
183        require_finite("low", low)?;
184        require_finite("close", close)?;
185        if high < low {
186            return Err(FinanceError::InvalidCashflow {
187                message: "high must be >= low for each bar",
188            });
189        }
190        let atr_v = self.atr.push(high, low, close)?;
191        let out = match atr_v {
192            None => {
193                self.prev_close = Some(close);
194                self.last = None;
195                None
196            }
197            Some(atr) => {
198                let mid = 0.5 * (high + low);
199                let basic_u = mid + self.params.multiplier * atr;
200                let basic_l = mid - self.params.multiplier * atr;
201                let prev_c = self.prev_close.unwrap_or(close);
202
203                let fu = match self.final_upper {
204                    None => basic_u,
205                    Some(prev_u) => {
206                        if basic_u < prev_u || prev_c > prev_u {
207                            basic_u
208                        } else {
209                            prev_u
210                        }
211                    }
212                };
213                let fl = match self.final_lower {
214                    None => basic_l,
215                    Some(prev_l) => {
216                        if basic_l > prev_l || prev_c < prev_l {
217                            basic_l
218                        } else {
219                            prev_l
220                        }
221                    }
222                };
223                self.final_upper = Some(fu);
224                self.final_lower = Some(fl);
225
226                let dir = match self.direction {
227                    None => {
228                        // First defined bar: close above mid → up
229                        if close >= mid {
230                            1
231                        } else {
232                            -1
233                        }
234                    }
235                    Some(1) => {
236                        if close < fl {
237                            -1
238                        } else {
239                            1
240                        }
241                    }
242                    Some(_) => {
243                        if close > fu {
244                            1
245                        } else {
246                            -1
247                        }
248                    }
249                };
250                self.direction = Some(dir);
251                let value = if dir > 0 { fl } else { fu };
252                let bar = SupertrendBar {
253                    value,
254                    direction: dir,
255                };
256                self.prev_close = Some(close);
257                self.last = Some(bar);
258                Some(bar)
259            }
260        };
261        Ok(out)
262    }
263
264    pub fn push_bars(
265        &mut self,
266        high: &[f64],
267        low: &[f64],
268        close: &[f64],
269    ) -> FinanceResult<Vec<Option<SupertrendBar>>> {
270        require_hlc(high, low, close)?;
271        let mut out = Vec::with_capacity(close.len());
272        for i in 0..close.len() {
273            out.push(self.push(high[i], low[i], close[i])?);
274        }
275        Ok(out)
276    }
277
278    pub fn last(&self) -> Option<SupertrendBar> {
279        self.last
280    }
281}
282
283pub fn supertrend(
284    high: &[f64],
285    low: &[f64],
286    close: &[f64],
287    params: SupertrendParams,
288) -> FinanceResult<SupertrendSeries> {
289    ValidatedSupertrend::new(params)?.compute(high, low, close)
290}
291
292fn supertrend_validated(
293    high: &[f64],
294    low: &[f64],
295    close: &[f64],
296    eng: ValidatedSupertrend,
297) -> FinanceResult<SupertrendSeries> {
298    let mut st = SupertrendState::new(eng.params)?;
299    let bars = st.push_bars(high, low, close)?;
300    let n = bars.len();
301    let mut value = vec![None; n];
302    let mut direction = vec![None; n];
303    for (i, b) in bars.into_iter().enumerate() {
304        if let Some(bar) = b {
305            value[i] = Some(bar.value);
306            direction[i] = Some(bar.direction);
307        }
308    }
309    Ok(SupertrendSeries {
310        value,
311        direction,
312        params: eng.params,
313    })
314}
315
316#[derive(Clone, Debug)]
317pub struct SupertrendSolution {
318    series: SupertrendSeries,
319    close: Vec<f64>,
320    formula: String,
321}
322
323impl SupertrendSolution {
324    pub fn series(&self) -> &SupertrendSeries {
325        &self.series
326    }
327    pub fn formula(&self) -> &str {
328        &self.formula
329    }
330
331    pub fn print_table(&self) {
332        self.print_table_locale_opt(None, None);
333    }
334
335    pub fn print_table_locale(&self, locale: &num_format::Locale, precision: usize) {
336        self.print_table_locale_opt(Some(locale), Some(precision));
337    }
338
339    fn print_table_locale_opt(
340        &self,
341        locale: Option<&num_format::Locale>,
342        precision: Option<usize>,
343    ) {
344        let columns = columns_with_strings(&[
345            ("period", "i", true),
346            ("close", "f", true),
347            ("st", "f", true),
348            ("dir", "i", true),
349        ]);
350        let data = self
351            .close
352            .iter()
353            .enumerate()
354            .map(|(i, c)| {
355                let d = self.series.direction[i]
356                    .map(|x| x.to_string())
357                    .unwrap_or_else(|| "n/a".to_string());
358                vec![
359                    i.to_string(),
360                    c.to_string(),
361                    opt_cell(self.series.value[i]),
362                    d,
363                ]
364            })
365            .collect();
366        print_table_locale_opt(&columns, data, locale, precision);
367    }
368}
369
370/// # Examples
371/// ```
372/// use finance_solution::stocks::ta::{supertrend_solution, SupertrendParams};
373/// let n = 40usize;
374/// let h: Vec<_> = (0..n).map(|i| 101.0 + i as f64 * 0.2).collect();
375/// let l: Vec<_> = (0..n).map(|i| 99.0 + i as f64 * 0.2).collect();
376/// let c: Vec<_> = (0..n).map(|i| 100.0 + i as f64 * 0.2).collect();
377/// let sol = supertrend_solution(&h, &l, &c, SupertrendParams::standard()).unwrap();
378/// assert!(sol.formula().contains("ATR"));
379/// ```
380pub fn supertrend_solution(
381    high: &[f64],
382    low: &[f64],
383    close: &[f64],
384    params: SupertrendParams,
385) -> FinanceResult<SupertrendSolution> {
386    let series = supertrend(high, low, close, params)?;
387    Ok(SupertrendSolution {
388        series,
389        close: close.to_vec(),
390        formula: format!(
391            "Supertrend ATR({}) x {}; ST = final lower (up) / upper (down)",
392            params.atr_period, params.multiplier
393        ),
394    })
395}
396
397#[cfg(test)]
398mod tests {
399    use super::*;
400
401    #[test]
402    fn produces_values() {
403        let n = 40usize;
404        let h: Vec<_> = (0..n).map(|i| 101.0 + i as f64 * 0.2).collect();
405        let l: Vec<_> = (0..n).map(|i| 99.0 + i as f64 * 0.2).collect();
406        let c: Vec<_> = (0..n).map(|i| 100.0 + i as f64 * 0.2).collect();
407        let s = supertrend(&h, &l, &c, SupertrendParams::standard()).unwrap();
408        assert!(s.value.iter().filter(|x| x.is_some()).count() > 10);
409        // Strong uptrend → mostly +1
410        let up = s.direction.iter().filter(|d| **d == Some(1)).count();
411        assert!(up > 5);
412    }
413
414    #[test]
415    fn state_parity() {
416        let n = 35usize;
417        let h: Vec<_> = (0..n).map(|i| 12.0 + i as f64 * 0.05).collect();
418        let l: Vec<_> = (0..n).map(|i| 10.0 + i as f64 * 0.05).collect();
419        let c: Vec<_> = (0..n).map(|i| 11.0 + i as f64 * 0.05).collect();
420        let p = SupertrendParams::standard();
421        let batch = supertrend(&h, &l, &c, p).unwrap();
422        let mut st = SupertrendState::new(p).unwrap();
423        for i in 0..n {
424            let o = st.push(h[i], l[i], c[i]).unwrap();
425            match (o, batch.value[i], batch.direction[i]) {
426                (None, None, None) => {}
427                (Some(bar), Some(v), Some(d)) => {
428                    assert!((bar.value - v).abs() < 1e-9);
429                    assert_eq!(bar.direction, d);
430                }
431                other => panic!("{other:?}"),
432            }
433        }
434    }
435
436    #[test]
437    fn bad_mult_err() {
438        assert!(ValidatedSupertrend::new(SupertrendParams::new(10, 0.0)).is_err());
439    }
440}