mantis_ta/indicators/trend/
parabolic_sar.rs1use crate::indicators::Indicator;
2use crate::types::Candle;
3
4#[derive(Debug, Clone)]
37pub struct ParabolicSar {
38 af_start: f64,
39 af_step: f64,
40 af_max: f64,
41 bar_count: usize,
42 first_high: f64,
43 first_low: f64,
44 first_close: f64,
45 is_uptrend: bool,
46 sar: f64,
47 ep: f64,
48 af: f64,
49 prev1_high: f64,
50 prev1_low: f64,
51 prev2_high: f64,
52 prev2_low: f64,
53}
54
55impl ParabolicSar {
56 pub fn new(af_start: f64, af_step: f64, af_max: f64) -> Self {
57 assert!(af_start > 0.0, "af_start must be > 0");
58 assert!(af_step > 0.0, "af_step must be > 0");
59 assert!(af_max >= af_start, "af_max must be >= af_start");
60 Self {
61 af_start,
62 af_step,
63 af_max,
64 bar_count: 0,
65 first_high: 0.0,
66 first_low: 0.0,
67 first_close: 0.0,
68 is_uptrend: true,
69 sar: 0.0,
70 ep: 0.0,
71 af: af_start,
72 prev1_high: 0.0,
73 prev1_low: 0.0,
74 prev2_high: 0.0,
75 prev2_low: 0.0,
76 }
77 }
78
79 #[inline]
80 fn update(&mut self, high: f64, low: f64, close: f64) -> Option<f64> {
81 self.bar_count += 1;
82
83 if self.bar_count == 1 {
84 self.first_high = high;
85 self.first_low = low;
86 self.first_close = close;
87 return None;
88 }
89
90 if self.bar_count == 2 {
91 self.is_uptrend = close >= self.first_close;
92 if self.is_uptrend {
93 self.sar = self.first_low;
94 self.ep = high.max(self.first_high);
95 } else {
96 self.sar = self.first_high;
97 self.ep = low.min(self.first_low);
98 }
99 self.af = self.af_start;
100 self.prev2_high = high;
104 self.prev2_low = low;
105 self.prev1_high = high;
106 self.prev1_low = low;
107 return Some(self.sar);
108 }
109
110 let mut new_sar = self.sar + self.af * (self.ep - self.sar);
111 if self.is_uptrend {
112 new_sar = new_sar.min(self.prev1_low).min(self.prev2_low);
113 } else {
114 new_sar = new_sar.max(self.prev1_high).max(self.prev2_high);
115 }
116
117 if self.is_uptrend && low < new_sar {
118 new_sar = self.ep.max(high).max(self.prev1_high);
119 self.is_uptrend = false;
120 self.ep = low;
121 self.af = self.af_start;
122 } else if !self.is_uptrend && high > new_sar {
123 new_sar = self.ep.min(low).min(self.prev1_low);
124 self.is_uptrend = true;
125 self.ep = high;
126 self.af = self.af_start;
127 } else if self.is_uptrend {
128 if high > self.ep {
129 self.ep = high;
130 self.af = (self.af + self.af_step).min(self.af_max);
131 }
132 } else if low < self.ep {
133 self.ep = low;
134 self.af = (self.af + self.af_step).min(self.af_max);
135 }
136
137 self.sar = new_sar;
138 self.prev2_high = self.prev1_high;
139 self.prev2_low = self.prev1_low;
140 self.prev1_high = high;
141 self.prev1_low = low;
142
143 Some(self.sar)
144 }
145}
146
147impl Indicator for ParabolicSar {
148 type Output = f64;
149
150 fn next(&mut self, candle: &Candle) -> Option<Self::Output> {
151 self.update(candle.high, candle.low, candle.close)
152 }
153
154 fn reset(&mut self) {
155 self.bar_count = 0;
156 self.first_high = 0.0;
157 self.first_low = 0.0;
158 self.first_close = 0.0;
159 self.is_uptrend = true;
160 self.sar = 0.0;
161 self.ep = 0.0;
162 self.af = self.af_start;
163 self.prev1_high = 0.0;
164 self.prev1_low = 0.0;
165 self.prev2_high = 0.0;
166 self.prev2_low = 0.0;
167 }
168
169 fn warmup_period(&self) -> usize {
170 2
171 }
172
173 fn clone_boxed(&self) -> Box<dyn Indicator<Output = Self::Output>> {
174 Box::new(self.clone())
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 fn candle(i: i64, high: f64, low: f64, close: f64) -> Candle {
183 Candle {
184 timestamp: i,
185 open: close,
186 high,
187 low,
188 close,
189 volume: 0.0,
190 }
191 }
192
193 #[test]
194 fn parabolic_sar_emits_after_warmup() {
195 let candles: Vec<Candle> = (0..10)
196 .map(|i| {
197 let price = 100.0 + i as f64;
198 candle(i, price + 1.0, price - 1.0, price)
199 })
200 .collect();
201
202 let out = ParabolicSar::new(0.02, 0.02, 0.2).calculate(&candles);
203 assert!(out[0].is_none());
204 assert!(out.iter().skip(1).all(|v| v.is_some()));
205 }
206
207 #[test]
208 fn parabolic_sar_warmup_period_is_two() {
209 let sar = ParabolicSar::new(0.02, 0.02, 0.2);
210 assert_eq!(sar.warmup_period(), 2);
211 }
212
213 #[test]
214 fn parabolic_sar_reset_restores_fresh_state() {
215 let candles: Vec<Candle> = (0..5)
216 .map(|i| {
217 let price = 100.0 + i as f64;
218 candle(i, price + 1.0, price - 1.0, price)
219 })
220 .collect();
221
222 let mut sar = ParabolicSar::new(0.02, 0.02, 0.2);
223 assert!(sar.next(&candles[0]).is_none());
224 assert!(sar.next(&candles[1]).is_some());
225
226 sar.reset();
227 assert_eq!(sar.next(&candles[0]), None);
228 }
229
230 #[test]
231 #[should_panic(expected = "af_start must be > 0")]
232 fn parabolic_sar_rejects_non_positive_af_start() {
233 ParabolicSar::new(0.0, 0.02, 0.2);
234 }
235
236 #[test]
237 #[should_panic(expected = "af_max must be >= af_start")]
238 fn parabolic_sar_rejects_af_max_below_af_start() {
239 ParabolicSar::new(0.1, 0.02, 0.05);
240 }
241}