1use crate::indicators::incremental::ta_atr::TaATR;
2use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
3use crate::indicators::volatility::ATR;
4use crate::traits::Next;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
9pub enum SuperTrendInitialDirection {
10 #[default]
12 Up,
13 Down,
16 FromClose,
18}
19
20impl std::str::FromStr for SuperTrendInitialDirection {
21 type Err = String;
22 fn from_str(s: &str) -> Result<Self, Self::Err> {
23 match s.to_ascii_lowercase().as_str() {
24 "up" => Ok(Self::Up),
25 "down" => Ok(Self::Down),
26 "from_close" | "fromclose" => Ok(Self::FromClose),
27 other => Err(format!(
28 "unknown initial_direction: '{other}' (expected 'up', 'down', or 'from_close')"
29 )),
30 }
31 }
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
36pub enum SuperTrendAtrSmoothing {
37 #[default]
39 Ema,
40 Wilder,
43}
44
45impl std::str::FromStr for SuperTrendAtrSmoothing {
46 type Err = String;
47 fn from_str(s: &str) -> Result<Self, Self::Err> {
48 match s.to_ascii_lowercase().as_str() {
49 "ema" => Ok(Self::Ema),
50 "wilder" | "rma" => Ok(Self::Wilder),
51 other => Err(format!(
52 "unknown smoothing: '{other}' (expected 'ema' or 'wilder')"
53 )),
54 }
55 }
56}
57
58#[derive(Debug, Clone)]
59enum AtrBackend {
60 Ema(ATR),
61 Wilder(TaATR),
62}
63
64impl AtrBackend {
65 fn next(&mut self, hlc: (f64, f64, f64)) -> f64 {
66 match self {
67 Self::Ema(atr) => atr.next(hlc),
68 Self::Wilder(ta_atr) => ta_atr.next(hlc),
69 }
70 }
71}
72
73pub const SUPERTREND_METADATA: IndicatorMetadata = IndicatorMetadata {
74 name: "SuperTrend",
75 description: "Trend-following indicator that combines ATR for volatility bands to identify the primary market direction.",
76 usage: "Use as a primary trend-following indicator and dynamic stop-loss. A SuperTrend flip from bearish to bullish (or vice versa) provides a clear, rule-based entry and exit signal.",
77 keywords: &["trend", "atr", "stop-loss", "classic", "breakout"],
78 ehlers_summary: "SuperTrend computes upper and lower ATR-based bands around the midpoint of each bar. The active line flips from upper to lower (and vice versa) only when price closes beyond the band, providing a clean directional bias and a trailing stop level in one indicator. — TradingView Community",
79 params: &[
80 ParamDef {
81 name: "period",
82 default: "10",
83 description: "ATR length",
84 },
85 ParamDef {
86 name: "multiplier",
87 default: "3.0",
88 description: "ATR multiplier",
89 },
90 ParamDef {
91 name: "initial_direction",
92 default: "up",
93 description: "Initial trend direction before reversal ('up', 'down', or 'from_close')",
94 },
95 ParamDef {
96 name: "smoothing",
97 default: "ema",
98 description: "ATR smoothing algorithm ('ema' default, or 'wilder'). Note: SuperTrend defaults to EMA, whereas standalone atr() defaults to Wilder.",
99 },
100 ],
101 formula_source: "https://www.tradingview.com/script/7zF0a4f8-SuperTrend-by-Mobius/",
102 formula_latex: r#"
103\[
104\text{SuperTrend} = \begin{cases}
105\text{LowerBand} & \text{if trend is up} \\
106\text{UpperBand} & \text{if trend is down}
107\end{cases}
108\]
109"#,
110 gold_standard_file: "supertrend_10_3.json",
111 category: "Classic",
112};
113
114#[derive(Debug, Clone)]
121pub struct SuperTrend {
122 atr: AtrBackend,
123 multiplier: f64,
124 prev_close: Option<f64>,
125 prev_upper_band: Option<f64>,
126 prev_lower_band: Option<f64>,
127 direction: i8, initial_direction: SuperTrendInitialDirection,
129 initialized_direction: bool,
130}
131
132impl SuperTrend {
133 pub fn new(period: usize, multiplier: f64) -> Self {
134 Self::with_options(
135 period,
136 multiplier,
137 SuperTrendInitialDirection::Up,
138 SuperTrendAtrSmoothing::Ema,
139 )
140 }
141
142 pub fn with_options(
143 period: usize,
144 multiplier: f64,
145 initial_direction: SuperTrendInitialDirection,
146 atr_smoothing: SuperTrendAtrSmoothing,
147 ) -> Self {
148 let initial_dir_val = match initial_direction {
149 SuperTrendInitialDirection::Up => 1,
150 SuperTrendInitialDirection::Down => -1,
151 SuperTrendInitialDirection::FromClose => 1,
152 };
153 let atr = match atr_smoothing {
154 SuperTrendAtrSmoothing::Ema => AtrBackend::Ema(ATR::new(period)),
155 SuperTrendAtrSmoothing::Wilder => AtrBackend::Wilder(TaATR::new(period)),
156 };
157 Self {
158 atr,
159 multiplier,
160 prev_close: None,
161 prev_upper_band: None,
162 prev_lower_band: None,
163 direction: initial_dir_val,
164 initial_direction,
165 initialized_direction: false,
166 }
167 }
168}
169
170impl Next<(f64, f64, f64)> for SuperTrend {
171 type Output = (f64, i8);
172
173 fn next(&mut self, (high, low, close): (f64, f64, f64)) -> Self::Output {
174 if high.is_nan() || low.is_nan() || close.is_nan() {
175 return (f64::NAN, self.direction);
176 }
177
178 let atr = self.atr.next((high, low, close));
179 if atr.is_nan() {
180 return (f64::NAN, self.direction);
181 }
182
183 let mid = (high + low) / 2.0;
184
185 if !self.initialized_direction {
186 self.direction = match self.initial_direction {
187 SuperTrendInitialDirection::Up => 1,
188 SuperTrendInitialDirection::Down => -1,
189 SuperTrendInitialDirection::FromClose => {
190 if close >= mid {
191 1
192 } else {
193 -1
194 }
195 }
196 };
197 self.initialized_direction = true;
198 }
199
200 let basic_upper = mid + self.multiplier * atr;
201 let basic_lower = mid - self.multiplier * atr;
202
203 let upper_band = match self.prev_upper_band {
204 Some(prev_upper) => {
205 if basic_upper < prev_upper || self.prev_close.unwrap_or(0.0) > prev_upper {
206 basic_upper
207 } else {
208 prev_upper
209 }
210 }
211 None => basic_upper,
212 };
213
214 let lower_band = match self.prev_lower_band {
215 Some(prev_lower) => {
216 if basic_lower > prev_lower || self.prev_close.unwrap_or(0.0) < prev_lower {
217 basic_lower
218 } else {
219 prev_lower
220 }
221 }
222 None => basic_lower,
223 };
224
225 if self.direction == -1 && close > upper_band {
226 self.direction = 1;
227 } else if self.direction == 1 && close < lower_band {
228 self.direction = -1;
229 }
230
231 let supertrend = if self.direction == 1 {
232 lower_band
233 } else {
234 upper_band
235 };
236
237 self.prev_close = Some(close);
238 self.prev_upper_band = Some(upper_band);
239 self.prev_lower_band = Some(lower_band);
240
241 (supertrend, self.direction)
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248 use proptest::prelude::*;
249 use serde::Deserialize;
250 use std::fs;
251 use std::path::Path;
252
253 #[derive(Debug, Deserialize)]
254 struct SuperTrendCase {
255 high: Vec<f64>,
256 low: Vec<f64>,
257 close: Vec<f64>,
258 expected_st: Vec<f64>,
259 expected_dir: Vec<i8>,
260 }
261
262 #[test]
263 fn test_supertrend_gold_standard() {
264 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
265 let manifest_path = Path::new(&manifest_dir);
266 let path = manifest_path.join("tests/gold_standard/supertrend_10_3.json");
267 let path = if path.exists() {
268 path
269 } else {
270 manifest_path
271 .parent()
272 .unwrap()
273 .join("tests/gold_standard/supertrend_10_3.json")
274 };
275 let content = fs::read_to_string(path).unwrap();
276 let case: SuperTrendCase = serde_json::from_str(&content).unwrap();
277
278 let mut st = SuperTrend::new(10, 3.0);
279 for i in 0..case.high.len() {
280 let (val, dir) = st.next((case.high[i], case.low[i], case.close[i]));
281 approx::assert_relative_eq!(val, case.expected_st[i], epsilon = 1e-6);
282 assert_eq!(dir, case.expected_dir[i]);
283 }
284 }
285
286 #[test]
287 fn test_supertrend_nan_inputs_emit_nan() {
288 let mut st = SuperTrend::new(3, 2.0);
289 let c = [1.0, 2.0, 3.0, f64::NAN, 5.0, 6.0, 7.0];
290 let mut results = Vec::new();
291 for &val in &c {
292 results.push(st.next((val + 1.0, val - 1.0, val)));
293 }
294 assert!(
296 results[3].0.is_nan(),
297 "SuperTrend should emit NaN on NaN input, got {}",
298 results[3].0
299 );
300 }
301
302 #[test]
303 fn test_supertrend_wilder_down_startup_and_breakout() {
304 let period = 3;
306 let mult = 2.0;
307 let mut st = SuperTrend::with_options(
308 period,
309 mult,
310 SuperTrendInitialDirection::Down,
311 SuperTrendAtrSmoothing::Wilder,
312 );
313
314 let r0 = st.next((10.0, 8.0, 9.0));
317 assert!(r0.0.is_nan());
318 let r1 = st.next((11.0, 9.0, 10.0));
319 assert!(r1.0.is_nan());
320 let r2 = st.next((11.0, 9.0, 10.0));
321 assert!(r2.0.is_nan());
322
323 let r3 = st.next((10.0, 8.0, 9.5));
332 assert!(!r3.0.is_nan());
333 approx::assert_relative_eq!(r3.0, 13.0);
334 assert_eq!(r3.1, -1);
335
336 let r4 = st.next((16.0, 14.0, 15.0));
339 assert_eq!(
340 r4.1, 1,
341 "Should flip to uptrend upon breakout above upper band"
342 );
343 }
344
345 fn supertrend_batch(
346 data: Vec<(f64, f64, f64)>,
347 period: usize,
348 multiplier: f64,
349 ) -> Vec<(f64, i8)> {
350 let mut st = SuperTrend::new(period, multiplier);
351 data.into_iter().map(|x| st.next(x)).collect()
352 }
353
354 proptest! {
355 #[test]
356 fn test_supertrend_parity(input in prop::collection::vec((0.0..100.0, 0.0..100.0, 0.0..100.0), 1..100)) {
357 let mut adj_input = Vec::with_capacity(input.len());
358 for (h, l, c) in input {
359 let h_f: f64 = h;
360 let l_f: f64 = l;
361 let c_f: f64 = c;
362 let high = h_f.max(l_f).max(c_f);
363 let low = l_f.min(h_f).min(c_f);
364 adj_input.push((high, low, c_f));
365 }
366
367 let period = 10;
368 let multiplier = 3.0;
369 let mut st = SuperTrend::new(period, multiplier);
370 let mut streaming_results = Vec::with_capacity(adj_input.len());
371 for &val in &adj_input {
372 streaming_results.push(st.next(val));
373 }
374
375 let batch_results = supertrend_batch(adj_input, period, multiplier);
376
377 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
378 approx::assert_relative_eq!(s.0, b.0, epsilon = 1e-6);
379 assert_eq!(s.1, b.1);
380 }
381 }
382 }
383}