finance_solution/stocks/ta/
supertrend.rs1use 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#[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 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 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#[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 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 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
370pub 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 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}