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' or '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)]
116pub struct SuperTrend {
117 atr: AtrBackend,
118 multiplier: f64,
119 prev_close: Option<f64>,
120 prev_upper_band: Option<f64>,
121 prev_lower_band: Option<f64>,
122 direction: i8, initial_direction: SuperTrendInitialDirection,
124 initialized_direction: bool,
125}
126
127impl SuperTrend {
128 pub fn new(period: usize, multiplier: f64) -> Self {
129 Self::with_options(
130 period,
131 multiplier,
132 SuperTrendInitialDirection::Up,
133 SuperTrendAtrSmoothing::Ema,
134 )
135 }
136
137 pub fn with_options(
138 period: usize,
139 multiplier: f64,
140 initial_direction: SuperTrendInitialDirection,
141 atr_smoothing: SuperTrendAtrSmoothing,
142 ) -> Self {
143 let initial_dir_val = match initial_direction {
144 SuperTrendInitialDirection::Up => 1,
145 SuperTrendInitialDirection::Down => -1,
146 SuperTrendInitialDirection::FromClose => 1,
147 };
148 let atr = match atr_smoothing {
149 SuperTrendAtrSmoothing::Ema => AtrBackend::Ema(ATR::new(period)),
150 SuperTrendAtrSmoothing::Wilder => AtrBackend::Wilder(TaATR::new(period)),
151 };
152 Self {
153 atr,
154 multiplier,
155 prev_close: None,
156 prev_upper_band: None,
157 prev_lower_band: None,
158 direction: initial_dir_val,
159 initial_direction,
160 initialized_direction: false,
161 }
162 }
163}
164
165impl Next<(f64, f64, f64)> for SuperTrend {
166 type Output = (f64, i8);
167
168 fn next(&mut self, (high, low, close): (f64, f64, f64)) -> Self::Output {
169 if high.is_nan() || low.is_nan() || close.is_nan() {
170 return (f64::NAN, self.direction);
171 }
172
173 let atr = self.atr.next((high, low, close));
174 if atr.is_nan() {
175 return (f64::NAN, self.direction);
176 }
177
178 let mid = (high + low) / 2.0;
179
180 if !self.initialized_direction {
181 self.direction = match self.initial_direction {
182 SuperTrendInitialDirection::Up => 1,
183 SuperTrendInitialDirection::Down => -1,
184 SuperTrendInitialDirection::FromClose => {
185 if close >= mid {
186 1
187 } else {
188 -1
189 }
190 }
191 };
192 self.initialized_direction = true;
193 }
194
195 let basic_upper = mid + self.multiplier * atr;
196 let basic_lower = mid - self.multiplier * atr;
197
198 let upper_band = match self.prev_upper_band {
199 Some(prev_upper) => {
200 if basic_upper < prev_upper || self.prev_close.unwrap_or(0.0) > prev_upper {
201 basic_upper
202 } else {
203 prev_upper
204 }
205 }
206 None => basic_upper,
207 };
208
209 let lower_band = match self.prev_lower_band {
210 Some(prev_lower) => {
211 if basic_lower > prev_lower || self.prev_close.unwrap_or(0.0) < prev_lower {
212 basic_lower
213 } else {
214 prev_lower
215 }
216 }
217 None => basic_lower,
218 };
219
220 if self.direction == -1 && close > upper_band {
221 self.direction = 1;
222 } else if self.direction == 1 && close < lower_band {
223 self.direction = -1;
224 }
225
226 let supertrend = if self.direction == 1 {
227 lower_band
228 } else {
229 upper_band
230 };
231
232 self.prev_close = Some(close);
233 self.prev_upper_band = Some(upper_band);
234 self.prev_lower_band = Some(lower_band);
235
236 (supertrend, self.direction)
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243 use proptest::prelude::*;
244 use serde::Deserialize;
245 use std::fs;
246 use std::path::Path;
247
248 #[derive(Debug, Deserialize)]
249 struct SuperTrendCase {
250 high: Vec<f64>,
251 low: Vec<f64>,
252 close: Vec<f64>,
253 expected_st: Vec<f64>,
254 expected_dir: Vec<i8>,
255 }
256
257 #[test]
258 fn test_supertrend_gold_standard() {
259 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
260 let manifest_path = Path::new(&manifest_dir);
261 let path = manifest_path.join("tests/gold_standard/supertrend_10_3.json");
262 let path = if path.exists() {
263 path
264 } else {
265 manifest_path
266 .parent()
267 .unwrap()
268 .join("tests/gold_standard/supertrend_10_3.json")
269 };
270 let content = fs::read_to_string(path).unwrap();
271 let case: SuperTrendCase = serde_json::from_str(&content).unwrap();
272
273 let mut st = SuperTrend::new(10, 3.0);
274 for i in 0..case.high.len() {
275 let (val, dir) = st.next((case.high[i], case.low[i], case.close[i]));
276 approx::assert_relative_eq!(val, case.expected_st[i], epsilon = 1e-6);
277 assert_eq!(dir, case.expected_dir[i]);
278 }
279 }
280
281 #[test]
282 fn test_supertrend_nan_inputs_emit_nan() {
283 let mut st = SuperTrend::new(3, 2.0);
284 let c = [1.0, 2.0, 3.0, f64::NAN, 5.0, 6.0, 7.0];
285 let mut results = Vec::new();
286 for &val in &c {
287 results.push(st.next((val + 1.0, val - 1.0, val)));
288 }
289 assert!(
291 results[3].0.is_nan(),
292 "SuperTrend should emit NaN on NaN input, got {}",
293 results[3].0
294 );
295 }
296
297 #[test]
298 fn test_supertrend_wilder_down_startup_and_breakout() {
299 let period = 3;
301 let mult = 2.0;
302 let mut st = SuperTrend::with_options(
303 period,
304 mult,
305 SuperTrendInitialDirection::Down,
306 SuperTrendAtrSmoothing::Wilder,
307 );
308
309 let r0 = st.next((10.0, 8.0, 9.0));
312 assert!(r0.0.is_nan());
313 let r1 = st.next((11.0, 9.0, 10.0));
314 assert!(r1.0.is_nan());
315 let r2 = st.next((11.0, 9.0, 10.0));
316 assert!(r2.0.is_nan());
317
318 let r3 = st.next((10.0, 8.0, 9.5));
327 assert!(!r3.0.is_nan());
328 approx::assert_relative_eq!(r3.0, 13.0);
329 assert_eq!(r3.1, -1);
330
331 let r4 = st.next((16.0, 14.0, 15.0));
334 assert_eq!(
335 r4.1, 1,
336 "Should flip to uptrend upon breakout above upper band"
337 );
338 }
339
340 fn supertrend_batch(
341 data: Vec<(f64, f64, f64)>,
342 period: usize,
343 multiplier: f64,
344 ) -> Vec<(f64, i8)> {
345 let mut st = SuperTrend::new(period, multiplier);
346 data.into_iter().map(|x| st.next(x)).collect()
347 }
348
349 proptest! {
350 #[test]
351 fn test_supertrend_parity(input in prop::collection::vec((0.0..100.0, 0.0..100.0, 0.0..100.0), 1..100)) {
352 let mut adj_input = Vec::with_capacity(input.len());
353 for (h, l, c) in input {
354 let h_f: f64 = h;
355 let l_f: f64 = l;
356 let c_f: f64 = c;
357 let high = h_f.max(l_f).max(c_f);
358 let low = l_f.min(h_f).min(c_f);
359 adj_input.push((high, low, c_f));
360 }
361
362 let period = 10;
363 let multiplier = 3.0;
364 let mut st = SuperTrend::new(period, multiplier);
365 let mut streaming_results = Vec::with_capacity(adj_input.len());
366 for &val in &adj_input {
367 streaming_results.push(st.next(val));
368 }
369
370 let batch_results = supertrend_batch(adj_input, period, multiplier);
371
372 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
373 approx::assert_relative_eq!(s.0, b.0, epsilon = 1e-6);
374 assert_eq!(s.1, b.1);
375 }
376 }
377 }
378}