indexes_rs/v2/parabolic_sar/
main.rs

1use crate::v2::parabolic_sar::types::{ParabolicSARConfig, ParabolicSARError, ParabolicSARInput, ParabolicSAROutput, ParabolicSARState, TrendDirection};
2
3/// Parabolic SAR (Stop and Reverse) Indicator
4///
5/// Parabolic SAR is a trend-following indicator that provides stop-loss levels
6/// and potential reversal points. It accelerates when the trend continues.
7///
8/// Formula:
9/// - Uptrend: SAR = Previous SAR + AF × (EP - Previous SAR)
10/// - Downtrend: SAR = Previous SAR + AF × (EP - Previous SAR)
11///
12/// Where:
13/// - AF = Acceleration Factor (starts at 0.02, increments by 0.02, max 0.20)
14/// - EP = Extreme Point (highest high in uptrend, lowest low in downtrend)
15pub struct ParabolicSAR {
16    pub state: ParabolicSARState,
17}
18
19impl ParabolicSAR {
20    /// Create a new Parabolic SAR calculator with default configuration
21    pub fn new() -> Self {
22        Self::with_config(ParabolicSARConfig::default())
23    }
24
25    /// Create a new Parabolic SAR calculator with custom acceleration parameters
26    pub fn with_acceleration(start: f64, increment: f64, maximum: f64) -> Result<Self, ParabolicSARError> {
27        let config = ParabolicSARConfig {
28            acceleration_start: start,
29            acceleration_increment: increment,
30            acceleration_maximum: maximum,
31        };
32
33        // Validate configuration
34        if start <= 0.0 || increment <= 0.0 || maximum <= start {
35            return Err(ParabolicSARError::InvalidAcceleration);
36        }
37
38        Ok(Self::with_config(config))
39    }
40
41    /// Create a new Parabolic SAR calculator with custom configuration
42    pub fn with_config(config: ParabolicSARConfig) -> Self {
43        Self {
44            state: ParabolicSARState::new(config),
45        }
46    }
47
48    /// Calculate Parabolic SAR for the given input
49    pub fn calculate(&mut self, input: ParabolicSARInput) -> Result<ParabolicSAROutput, ParabolicSARError> {
50        // Validate input
51        self.validate_input(&input)?;
52        self.validate_config()?;
53
54        let result = if self.state.is_first {
55            self.handle_first_calculation(input)
56        } else if self.state.is_second {
57            self.handle_second_calculation(input)
58        } else {
59            self.handle_normal_calculation(input)
60        };
61
62        // Update state for next calculation
63        self.update_state_after_calculation(input);
64
65        result
66    }
67
68    /// Calculate Parabolic SAR for a batch of inputs
69    pub fn calculate_batch(&mut self, inputs: &[ParabolicSARInput]) -> Result<Vec<ParabolicSAROutput>, ParabolicSARError> {
70        inputs.iter().map(|input| self.calculate(*input)).collect()
71    }
72
73    /// Reset the calculator state
74    pub fn reset(&mut self) {
75        self.state = ParabolicSARState::new(self.state.config);
76    }
77
78    /// Get current state (for serialization/debugging)
79    pub fn get_state(&self) -> &ParabolicSARState {
80        &self.state
81    }
82
83    /// Restore state (for deserialization)
84    pub fn set_state(&mut self, state: ParabolicSARState) {
85        self.state = state;
86    }
87
88    /// Get current trend direction
89    pub fn current_trend(&self) -> Option<TrendDirection> {
90        self.state.trend
91    }
92
93    /// Get current acceleration factor
94    pub fn current_acceleration_factor(&self) -> f64 {
95        self.state.acceleration_factor
96    }
97
98    // Private helper methods
99
100    fn validate_input(&self, input: &ParabolicSARInput) -> Result<(), ParabolicSARError> {
101        // Check for valid prices
102        if !input.high.is_finite() || !input.low.is_finite() {
103            return Err(ParabolicSARError::InvalidPrice);
104        }
105
106        // Check HL relationship
107        if input.high < input.low {
108            return Err(ParabolicSARError::InvalidHL);
109        }
110
111        // Check close if provided
112        if let Some(close) = input.close {
113            if !close.is_finite() {
114                return Err(ParabolicSARError::InvalidPrice);
115            }
116            if close < input.low || close > input.high {
117                return Err(ParabolicSARError::CloseOutOfRange);
118            }
119        }
120
121        Ok(())
122    }
123
124    fn validate_config(&self) -> Result<(), ParabolicSARError> {
125        let config = &self.state.config;
126
127        if config.acceleration_start <= 0.0 || config.acceleration_increment <= 0.0 || config.acceleration_maximum <= config.acceleration_start {
128            return Err(ParabolicSARError::InvalidAcceleration);
129        }
130
131        Ok(())
132    }
133
134    fn handle_first_calculation(&mut self, input: ParabolicSARInput) -> Result<ParabolicSAROutput, ParabolicSARError> {
135        // First calculation - just store data, no SAR yet
136        // Initial trend determination will happen on second calculation
137        self.state.is_first = false;
138        self.state.is_second = true;
139
140        Ok(ParabolicSAROutput {
141            sar: input.low,            // Placeholder - will be properly calculated next period
142            trend: TrendDirection::Up, // Placeholder
143            acceleration_factor: self.state.config.acceleration_start,
144            extreme_point: input.high,
145            trend_reversal: false,
146            trend_periods: 1,
147        })
148    }
149
150    fn handle_second_calculation(&mut self, input: ParabolicSARInput) -> Result<ParabolicSAROutput, ParabolicSARError> {
151        // Second calculation - determine initial trend and set initial SAR
152        self.state.is_second = false;
153
154        let prev_high = self.state.previous_high.unwrap();
155        let prev_low = self.state.previous_low.unwrap();
156
157        // Determine initial trend direction
158        let trend = if input.high > prev_high { TrendDirection::Up } else { TrendDirection::Down };
159
160        // Set initial SAR and extreme point
161        let (sar, extreme_point) = match trend {
162            TrendDirection::Up => (prev_low, input.high.max(prev_high)),
163            TrendDirection::Down => (prev_high, input.low.min(prev_low)),
164        };
165
166        self.state.trend = Some(trend);
167        self.state.current_sar = Some(sar);
168        self.state.extreme_point = Some(extreme_point);
169        self.state.trend_periods = 1;
170
171        Ok(ParabolicSAROutput {
172            sar,
173            trend,
174            acceleration_factor: self.state.acceleration_factor,
175            extreme_point,
176            trend_reversal: false,
177            trend_periods: self.state.trend_periods,
178        })
179    }
180
181    fn handle_normal_calculation(&mut self, input: ParabolicSARInput) -> Result<ParabolicSAROutput, ParabolicSARError> {
182        let current_trend = self.state.trend.unwrap();
183        let current_sar = self.state.current_sar.unwrap();
184        let current_ep = self.state.extreme_point.unwrap();
185
186        // Check for trend reversal
187        let trend_reversal = match current_trend {
188            TrendDirection::Up => input.low <= current_sar,
189            TrendDirection::Down => input.high >= current_sar,
190        };
191
192        if trend_reversal {
193            self.handle_trend_reversal(input, current_trend, current_ep)
194        } else {
195            self.handle_trend_continuation(input, current_trend, current_sar, current_ep)
196        }
197    }
198
199    fn handle_trend_reversal(&mut self, input: ParabolicSARInput, old_trend: TrendDirection, old_ep: f64) -> Result<ParabolicSAROutput, ParabolicSARError> {
200        // Trend reversal - flip direction
201        let new_trend = match old_trend {
202            TrendDirection::Up => TrendDirection::Down,
203            TrendDirection::Down => TrendDirection::Up,
204        };
205
206        // New SAR is the old extreme point
207        let new_sar = old_ep;
208
209        // New extreme point
210        let new_ep = match new_trend {
211            TrendDirection::Up => input.high,
212            TrendDirection::Down => input.low,
213        };
214
215        // Reset acceleration factor
216        self.state.acceleration_factor = self.state.config.acceleration_start;
217        self.state.trend = Some(new_trend);
218        self.state.current_sar = Some(new_sar);
219        self.state.extreme_point = Some(new_ep);
220        self.state.trend_periods = 1;
221
222        Ok(ParabolicSAROutput {
223            sar: new_sar,
224            trend: new_trend,
225            acceleration_factor: self.state.acceleration_factor,
226            extreme_point: new_ep,
227            trend_reversal: true,
228            trend_periods: self.state.trend_periods,
229        })
230    }
231
232    fn handle_trend_continuation(&mut self, input: ParabolicSARInput, trend: TrendDirection, current_sar: f64, current_ep: f64) -> Result<ParabolicSAROutput, ParabolicSARError> {
233        // Check if we have a new extreme point
234        let (new_ep, ep_updated) = match trend {
235            TrendDirection::Up => {
236                if input.high > current_ep {
237                    (input.high, true)
238                } else {
239                    (current_ep, false)
240                }
241            }
242            TrendDirection::Down => {
243                if input.low < current_ep {
244                    (input.low, true)
245                } else {
246                    (current_ep, false)
247                }
248            }
249        };
250
251        // Update acceleration factor if we have a new extreme point
252        if ep_updated {
253            self.state.acceleration_factor = (self.state.acceleration_factor + self.state.config.acceleration_increment).min(self.state.config.acceleration_maximum);
254        }
255
256        // Calculate new SAR
257        let mut new_sar = current_sar + self.state.acceleration_factor * (new_ep - current_sar);
258
259        // Apply SAR rules to prevent SAR from moving into the price range
260        new_sar = match trend {
261            TrendDirection::Up => {
262                // In uptrend, SAR cannot be above the low of current or previous period
263                let prev_low = self.state.previous_low.unwrap_or(input.low);
264                new_sar.min(input.low).min(prev_low)
265            }
266            TrendDirection::Down => {
267                // In downtrend, SAR cannot be below the high of current or previous period
268                let prev_high = self.state.previous_high.unwrap_or(input.high);
269                new_sar.max(input.high).max(prev_high)
270            }
271        };
272
273        self.state.current_sar = Some(new_sar);
274        self.state.extreme_point = Some(new_ep);
275        self.state.trend_periods += 1;
276
277        Ok(ParabolicSAROutput {
278            sar: new_sar,
279            trend,
280            acceleration_factor: self.state.acceleration_factor,
281            extreme_point: new_ep,
282            trend_reversal: false,
283            trend_periods: self.state.trend_periods,
284        })
285    }
286
287    fn update_state_after_calculation(&mut self, input: ParabolicSARInput) {
288        self.state.previous_high = Some(input.high);
289        self.state.previous_low = Some(input.low);
290        if let Some(close) = input.close {
291            self.state.previous_close = Some(close);
292        }
293    }
294}
295
296impl Default for ParabolicSAR {
297    fn default() -> Self {
298        Self::new()
299    }
300}
301
302/// Convenience function to calculate Parabolic SAR for HL data without maintaining state
303pub fn calculate_parabolic_sar_simple(
304    highs: &[f64],
305    lows: &[f64],
306    acceleration_start: Option<f64>,
307    acceleration_increment: Option<f64>,
308    acceleration_maximum: Option<f64>,
309) -> Result<Vec<f64>, ParabolicSARError> {
310    if highs.len() != lows.len() {
311        return Err(ParabolicSARError::InvalidInput("Highs and lows must have same length".to_string()));
312    }
313
314    if highs.is_empty() {
315        return Ok(Vec::new());
316    }
317
318    let config = ParabolicSARConfig {
319        acceleration_start: acceleration_start.unwrap_or(0.02),
320        acceleration_increment: acceleration_increment.unwrap_or(0.02),
321        acceleration_maximum: acceleration_maximum.unwrap_or(0.20),
322    };
323
324    let mut sar_calculator = ParabolicSAR::with_config(config);
325    let mut results = Vec::with_capacity(highs.len());
326
327    for i in 0..highs.len() {
328        let input = ParabolicSARInput {
329            high: highs[i],
330            low: lows[i],
331            close: None,
332        };
333        let output = sar_calculator.calculate(input)?;
334        results.push(output.sar);
335    }
336
337    Ok(results)
338}